2026-08-07 16:25:37 -04:00
|
|
|
|
import asyncio
|
2026-07-03 14:00:10 +03:00
|
|
|
|
import base64
|
2026-04-30 21:49:08 +03:00
|
|
|
|
import hashlib
|
2025-09-03 23:17:07 +03:00
|
|
|
|
import logging
|
2025-09-09 21:40:29 +03:00
|
|
|
|
import math
|
2026-04-23 03:59:55 +03:00
|
|
|
|
import re
|
2026-05-27 09:44:27 +03:00
|
|
|
|
from io import BytesIO
|
2025-09-03 23:17:07 +03:00
|
|
|
|
|
|
|
|
|
|
import torch
|
2025-10-24 08:37:16 +03:00
|
|
|
|
from typing_extensions import override
|
2025-09-03 23:17:07 +03:00
|
|
|
|
|
2026-06-04 11:23:52 +03:00
|
|
|
|
from comfy.utils import common_upscale
|
2026-05-27 09:44:27 +03:00
|
|
|
|
from comfy_api.latest import IO, ComfyExtension, Input, Types
|
2026-01-18 04:52:45 +02:00
|
|
|
|
from comfy_api_nodes.apis.bytedance import (
|
2025-12-08 11:33:46 +02:00
|
|
|
|
RECOMMENDED_PRESETS,
|
|
|
|
|
|
RECOMMENDED_PRESETS_SEEDREAM_4,
|
2026-05-11 12:53:00 +03:00
|
|
|
|
RECOMMENDED_PRESETS_SEEDREAM_4_0,
|
|
|
|
|
|
RECOMMENDED_PRESETS_SEEDREAM_4_5,
|
|
|
|
|
|
RECOMMENDED_PRESETS_SEEDREAM_5_LITE,
|
2026-07-09 00:04:19 +03:00
|
|
|
|
RECOMMENDED_PRESETS_SEEDREAM_5_PRO,
|
2026-04-13 08:26:19 +03:00
|
|
|
|
SEEDANCE2_REF_VIDEO_PIXEL_LIMITS,
|
2025-12-08 11:33:46 +02:00
|
|
|
|
VIDEO_TASKS_EXECUTION_TIME,
|
2026-04-23 03:59:55 +03:00
|
|
|
|
GetAssetResponse,
|
2025-12-08 11:33:46 +02:00
|
|
|
|
Image2VideoTaskCreationRequest,
|
|
|
|
|
|
ImageTaskCreationResponse,
|
2026-07-03 14:00:10 +03:00
|
|
|
|
SeedAudioConfig,
|
|
|
|
|
|
SeedAudioReference,
|
|
|
|
|
|
SeedAudioRequest,
|
|
|
|
|
|
SeedAudioResponse,
|
2026-04-13 08:26:19 +03:00
|
|
|
|
Seedance2TaskCreationRequest,
|
2026-04-23 03:59:55 +03:00
|
|
|
|
SeedanceCreateAssetRequest,
|
|
|
|
|
|
SeedanceCreateAssetResponse,
|
|
|
|
|
|
SeedanceCreateVisualValidateSessionResponse,
|
|
|
|
|
|
SeedanceGetVisualValidateSessionResponse,
|
2026-04-30 21:49:08 +03:00
|
|
|
|
SeedanceVirtualLibraryCreateAssetRequest,
|
2025-12-08 11:33:46 +02:00
|
|
|
|
Seedream4Options,
|
|
|
|
|
|
Seedream4TaskCreationRequest,
|
2026-08-07 16:25:37 -04:00
|
|
|
|
Seedream5LayerOptimizePromptOptions,
|
|
|
|
|
|
Seedream5LayerSeparationRequest,
|
2026-07-10 11:58:22 +03:00
|
|
|
|
Seedream5OptimizePromptOptions,
|
2026-04-13 08:26:19 +03:00
|
|
|
|
TaskAudioContent,
|
|
|
|
|
|
TaskAudioContentUrl,
|
2025-12-08 11:33:46 +02:00
|
|
|
|
TaskCreationResponse,
|
|
|
|
|
|
TaskImageContent,
|
|
|
|
|
|
TaskImageContentUrl,
|
|
|
|
|
|
TaskStatusResponse,
|
|
|
|
|
|
TaskTextContent,
|
2026-04-13 08:26:19 +03:00
|
|
|
|
TaskVideoContent,
|
|
|
|
|
|
TaskVideoContentUrl,
|
2025-12-08 11:33:46 +02:00
|
|
|
|
Text2ImageTaskCreationRequest,
|
|
|
|
|
|
Text2VideoTaskCreationRequest,
|
2026-06-24 17:57:46 +03:00
|
|
|
|
seedance2_price_per_1k_tokens,
|
2026-08-07 21:28:09 +03:00
|
|
|
|
seedance2_reference_limits,
|
2025-12-08 11:33:46 +02:00
|
|
|
|
)
|
2025-10-24 08:37:16 +03:00
|
|
|
|
from comfy_api_nodes.util import (
|
2025-09-03 23:17:07 +03:00
|
|
|
|
ApiEndpoint,
|
2026-07-03 14:00:10 +03:00
|
|
|
|
audio_bytes_to_audio_input,
|
|
|
|
|
|
audio_input_to_mp3,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
download_url_to_image_tensor,
|
|
|
|
|
|
download_url_to_video_output,
|
2026-05-28 11:03:28 +03:00
|
|
|
|
downscale_image_tensor_by_max_side,
|
2026-05-21 21:58:03 +03:00
|
|
|
|
downscale_video_to_max_pixels,
|
2025-10-24 08:37:16 +03:00
|
|
|
|
get_number_of_images,
|
|
|
|
|
|
image_tensor_pair_to_batch,
|
|
|
|
|
|
poll_op,
|
|
|
|
|
|
sync_op,
|
2026-07-03 14:00:10 +03:00
|
|
|
|
tensor_to_base64_string,
|
2026-04-13 08:26:19 +03:00
|
|
|
|
upload_audio_to_comfyapi,
|
|
|
|
|
|
upload_image_to_comfyapi,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
upload_images_to_comfyapi,
|
2026-04-13 08:26:19 +03:00
|
|
|
|
upload_video_to_comfyapi,
|
2026-07-03 14:00:10 +03:00
|
|
|
|
upscale_image_tensor_to_min_pixels,
|
2026-05-21 21:58:03 +03:00
|
|
|
|
upscale_video_to_min_pixels,
|
2026-07-03 14:00:10 +03:00
|
|
|
|
validate_audio_duration,
|
2025-10-30 08:49:03 +02:00
|
|
|
|
validate_image_aspect_ratio,
|
2025-10-24 08:37:16 +03:00
|
|
|
|
validate_image_dimensions,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
validate_string,
|
2026-04-23 03:59:55 +03:00
|
|
|
|
validate_video_dimensions,
|
|
|
|
|
|
validate_video_duration,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
)
|
2026-08-07 16:25:37 -04:00
|
|
|
|
from comfy_api_nodes.util.common_exceptions import ProcessingInterrupted
|
2026-04-23 03:59:55 +03:00
|
|
|
|
from server import PromptServer
|
2025-09-09 21:40:29 +03:00
|
|
|
|
|
|
|
|
|
|
BYTEPLUS_IMAGE_ENDPOINT = "/proxy/byteplus/api/v3/images/generations"
|
2025-09-03 23:17:07 +03:00
|
|
|
|
|
2026-04-23 03:59:55 +03:00
|
|
|
|
_VERIFICATION_POLL_TIMEOUT_SEC = 120
|
|
|
|
|
|
_VERIFICATION_POLL_INTERVAL_SEC = 3
|
|
|
|
|
|
|
2026-02-24 19:03:30 +02:00
|
|
|
|
SEEDREAM_MODELS = {
|
2026-07-09 00:04:19 +03:00
|
|
|
|
"seedream 5.0 pro": "seedream-5-0-pro-260628",
|
2026-02-24 19:03:30 +02:00
|
|
|
|
"seedream 5.0 lite": "seedream-5-0-260128",
|
|
|
|
|
|
"seedream-4-5-251128": "seedream-4-5-251128",
|
|
|
|
|
|
"seedream-4-0-250828": "seedream-4-0-250828",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 12:53:00 +03:00
|
|
|
|
SEEDREAM_PRESETS = {
|
2026-07-09 00:04:19 +03:00
|
|
|
|
"seedream-5-0-pro-260628": RECOMMENDED_PRESETS_SEEDREAM_5_PRO,
|
2026-05-11 12:53:00 +03:00
|
|
|
|
"seedream-5-0-260128": RECOMMENDED_PRESETS_SEEDREAM_5_LITE,
|
|
|
|
|
|
"seedream-4-5-251128": RECOMMENDED_PRESETS_SEEDREAM_4_5,
|
|
|
|
|
|
"seedream-4-0-250828": RECOMMENDED_PRESETS_SEEDREAM_4_0,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-07 16:25:37 -04:00
|
|
|
|
SEEDREAM_LAYER_SEPARATION_MODEL = "seedream-5-0-pro-260628"
|
|
|
|
|
|
|
2025-09-09 21:40:29 +03:00
|
|
|
|
# Long-running tasks endpoints(e.g., video)
|
|
|
|
|
|
BYTEPLUS_TASK_ENDPOINT = "/proxy/byteplus/api/v3/contents/generations/tasks"
|
|
|
|
|
|
BYTEPLUS_TASK_STATUS_ENDPOINT = "/proxy/byteplus/api/v3/contents/generations/tasks" # + /{task_id}
|
2026-04-13 08:26:19 +03:00
|
|
|
|
BYTEPLUS_SEEDANCE2_TASK_STATUS_ENDPOINT = "/proxy/byteplus-seedance2/api/v3/contents/generations/tasks" # + /{task_id}
|
|
|
|
|
|
|
|
|
|
|
|
SEEDANCE_MODELS = {
|
2026-08-07 21:28:09 +03:00
|
|
|
|
"Seedance 2.5": "dreamina-seedance-2-5-260628",
|
2026-04-13 08:26:19 +03:00
|
|
|
|
"Seedance 2.0": "dreamina-seedance-2-0-260128",
|
|
|
|
|
|
"Seedance 2.0 Fast": "dreamina-seedance-2-0-fast-260128",
|
2026-06-25 17:57:04 +03:00
|
|
|
|
"Seedance 2.0 Mini": "dreamina-seedance-2-0-mini",
|
2026-04-13 08:26:19 +03:00
|
|
|
|
}
|
2025-09-03 23:17:07 +03:00
|
|
|
|
|
2026-08-07 21:28:09 +03:00
|
|
|
|
SEEDANCE_MODEL_TOOLTIP = (
|
|
|
|
|
|
"Seedance 2.5 for the newest model, videos up to 30 seconds and mp4/mov output; "
|
|
|
|
|
|
"Seedance 2.0 for maximum quality and 1080p/4k; Fast for speed optimization; "
|
|
|
|
|
|
"Mini for the fastest, lowest-cost generation."
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-20 05:05:01 +02:00
|
|
|
|
DEPRECATED_MODELS = {"seedance-1-0-lite-t2v-250428", "seedance-1-0-lite-i2v-250428"}
|
|
|
|
|
|
|
2026-04-13 08:26:19 +03:00
|
|
|
|
|
2026-03-20 05:05:01 +02:00
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
2025-09-03 23:17:07 +03:00
|
|
|
|
|
2026-04-21 20:45:10 +03:00
|
|
|
|
def _validate_ref_video_pixels(video: Input.Video, model_id: str, resolution: str, index: int) -> None:
|
|
|
|
|
|
"""Validate reference video pixel count against Seedance 2.0 model limits for the selected resolution."""
|
|
|
|
|
|
model_limits = SEEDANCE2_REF_VIDEO_PIXEL_LIMITS.get(model_id)
|
|
|
|
|
|
if not model_limits:
|
|
|
|
|
|
return
|
|
|
|
|
|
limits = model_limits.get(resolution)
|
2026-04-13 08:26:19 +03:00
|
|
|
|
if not limits:
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
w, h = video.get_dimensions()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return
|
|
|
|
|
|
pixels = w * h
|
|
|
|
|
|
min_px = limits.get("min")
|
|
|
|
|
|
max_px = limits.get("max")
|
|
|
|
|
|
if min_px and pixels < min_px:
|
|
|
|
|
|
raise ValueError(
|
2026-05-21 21:58:03 +03:00
|
|
|
|
f"Reference video {index} is too small: {w}x{h} = {pixels:,} total pixels. "
|
|
|
|
|
|
f"Minimum for this model is {min_px:,} total pixels."
|
2026-04-13 08:26:19 +03:00
|
|
|
|
)
|
|
|
|
|
|
if max_px and pixels > max_px:
|
|
|
|
|
|
raise ValueError(
|
2026-05-21 21:58:03 +03:00
|
|
|
|
f"Reference video {index} is too large: {w}x{h} = {pixels:,} total pixels. "
|
|
|
|
|
|
f"Maximum for this model is {max_px:,} total pixels. Try downscaling the video."
|
2026-04-13 08:26:19 +03:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-28 11:03:28 +03:00
|
|
|
|
def _prepare_seedance_image(image: Input.Image) -> Input.Image:
|
|
|
|
|
|
"""Auto-downscale a Seedance image input to the per-side limits, then validate it."""
|
|
|
|
|
|
validate_image_aspect_ratio(image, (2, 5), (5, 2), strict=False) # 0.4 to 2.5
|
|
|
|
|
|
image = downscale_image_tensor_by_max_side(image, max_side=6000)
|
|
|
|
|
|
validate_image_dimensions(image, min_width=300, min_height=300, max_width=6000, max_height=6000)
|
|
|
|
|
|
return image
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-04 11:23:52 +03:00
|
|
|
|
# Supported output aspect ratios, used to pre-size FLF frames to matching pixel pair to avoid the 1080p stretch jump.
|
|
|
|
|
|
SEEDANCE2_RATIO_WH = {
|
|
|
|
|
|
"16:9": (16, 9),
|
|
|
|
|
|
"4:3": (4, 3),
|
|
|
|
|
|
"1:1": (1, 1),
|
|
|
|
|
|
"3:4": (3, 4),
|
|
|
|
|
|
"9:16": (9, 16),
|
|
|
|
|
|
"21:9": (21, 9),
|
|
|
|
|
|
}
|
2026-06-24 17:57:46 +03:00
|
|
|
|
SEEDANCE2_RES_SHORT_SIDE = {"480p": 480, "720p": 720, "1080p": 1080, "4k": 2160}
|
2026-06-04 11:23:52 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _seedance2_target_dims(resolution: str, ratio: str, image: torch.Tensor) -> tuple[int, int]:
|
|
|
|
|
|
"""Exact supported output (width, height) for (resolution, ratio).
|
|
|
|
|
|
|
|
|
|
|
|
The shorter side equals the resolution number (e.g. 1080p 16:9 -> 1920x1080). For ratio
|
|
|
|
|
|
"adaptive" (or any unexpected value) the ratio is derived from the image's own aspect, snapped
|
|
|
|
|
|
to the nearest supported ratio, so the output keeps the frame's orientation.
|
|
|
|
|
|
"""
|
|
|
|
|
|
short = SEEDANCE2_RES_SHORT_SIDE[resolution]
|
|
|
|
|
|
if ratio not in SEEDANCE2_RATIO_WH:
|
|
|
|
|
|
aspect = image.shape[-2] / image.shape[-3] # W / H; tensor is (B, H, W, C)
|
|
|
|
|
|
ratio = min(SEEDANCE2_RATIO_WH, key=lambda k: abs(SEEDANCE2_RATIO_WH[k][0] / SEEDANCE2_RATIO_WH[k][1] - aspect))
|
|
|
|
|
|
rw, rh = SEEDANCE2_RATIO_WH[ratio]
|
|
|
|
|
|
if rw >= rh: # landscape or square: shorter side is the height
|
|
|
|
|
|
out_w, out_h = round(short * rw / rh), short
|
|
|
|
|
|
else: # portrait: shorter side is the width
|
|
|
|
|
|
out_w, out_h = short, round(short * rh / rw)
|
|
|
|
|
|
return out_w - out_w % 2, out_h - out_h % 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _resize_to_exact(image: torch.Tensor, width: int, height: int) -> torch.Tensor:
|
|
|
|
|
|
"""Center-crop to the target aspect and resize to exactly width x height (lanczos)."""
|
|
|
|
|
|
samples = image.movedim(-1, 1) # (B, H, W, C) -> (B, C, H, W)
|
|
|
|
|
|
resized = common_upscale(samples, width, height, "lanczos", "center")
|
|
|
|
|
|
return resized.movedim(1, -1)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-23 03:59:55 +03:00
|
|
|
|
async def _resolve_reference_assets(
|
|
|
|
|
|
cls: type[IO.ComfyNode],
|
|
|
|
|
|
asset_ids: list[str],
|
|
|
|
|
|
) -> tuple[dict[str, str], dict[str, str], dict[str, str]]:
|
|
|
|
|
|
"""Look up each asset, validate Active status, group by asset_type.
|
|
|
|
|
|
|
|
|
|
|
|
Returns (image_assets, video_assets, audio_assets), each mapping asset_id -> "asset://<asset_id>".
|
|
|
|
|
|
"""
|
|
|
|
|
|
image_assets: dict[str, str] = {}
|
|
|
|
|
|
video_assets: dict[str, str] = {}
|
|
|
|
|
|
audio_assets: dict[str, str] = {}
|
|
|
|
|
|
for i, raw_id in enumerate(asset_ids, 1):
|
|
|
|
|
|
asset_id = (raw_id or "").strip()
|
|
|
|
|
|
if not asset_id:
|
|
|
|
|
|
continue
|
|
|
|
|
|
result = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path=f"/proxy/seedance/assets/{asset_id}"),
|
|
|
|
|
|
response_model=GetAssetResponse,
|
|
|
|
|
|
)
|
|
|
|
|
|
if result.status != "Active":
|
|
|
|
|
|
extra = f" {result.error.code}: {result.error.message}" if result.error else ""
|
|
|
|
|
|
raise ValueError(f"Reference asset {i} (Id={asset_id}) is not Active (Status={result.status}).{extra}")
|
|
|
|
|
|
asset_uri = f"asset://{asset_id}"
|
|
|
|
|
|
if result.asset_type == "Image":
|
|
|
|
|
|
image_assets[asset_id] = asset_uri
|
|
|
|
|
|
elif result.asset_type == "Video":
|
|
|
|
|
|
video_assets[asset_id] = asset_uri
|
|
|
|
|
|
elif result.asset_type == "Audio":
|
|
|
|
|
|
audio_assets[asset_id] = asset_uri
|
|
|
|
|
|
return image_assets, video_assets, audio_assets
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_ASSET_REF_RE = re.compile(r"\basset ?(\d{1,2})\b", re.IGNORECASE)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _build_asset_labels(
|
|
|
|
|
|
reference_assets: dict[str, str],
|
|
|
|
|
|
image_asset_uris: dict[str, str],
|
|
|
|
|
|
video_asset_uris: dict[str, str],
|
|
|
|
|
|
audio_asset_uris: dict[str, str],
|
|
|
|
|
|
n_reference_images: int,
|
|
|
|
|
|
n_reference_videos: int,
|
|
|
|
|
|
n_reference_audios: int,
|
|
|
|
|
|
) -> dict[int, str]:
|
|
|
|
|
|
"""Map asset slot number (from 'asset_N' keys) to its positional label.
|
|
|
|
|
|
|
|
|
|
|
|
Asset entries are appended to `content` after the reference_images/videos/audios,
|
|
|
|
|
|
so their 1-indexed labels continue from the count of existing same-type refs:
|
|
|
|
|
|
one reference_images entry + one Image-type asset -> asset labelled "Image 2".
|
|
|
|
|
|
"""
|
|
|
|
|
|
image_n = n_reference_images
|
|
|
|
|
|
video_n = n_reference_videos
|
|
|
|
|
|
audio_n = n_reference_audios
|
|
|
|
|
|
labels: dict[int, str] = {}
|
|
|
|
|
|
for slot_key, raw_id in reference_assets.items():
|
|
|
|
|
|
asset_id = (raw_id or "").strip()
|
|
|
|
|
|
if not asset_id:
|
|
|
|
|
|
continue
|
|
|
|
|
|
try:
|
|
|
|
|
|
slot_num = int(slot_key.rsplit("_", 1)[-1])
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if asset_id in image_asset_uris:
|
|
|
|
|
|
image_n += 1
|
|
|
|
|
|
labels[slot_num] = f"Image {image_n}"
|
|
|
|
|
|
elif asset_id in video_asset_uris:
|
|
|
|
|
|
video_n += 1
|
|
|
|
|
|
labels[slot_num] = f"Video {video_n}"
|
|
|
|
|
|
elif asset_id in audio_asset_uris:
|
|
|
|
|
|
audio_n += 1
|
|
|
|
|
|
labels[slot_num] = f"Audio {audio_n}"
|
|
|
|
|
|
return labels
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _rewrite_asset_refs(prompt: str, labels: dict[int, str]) -> str:
|
|
|
|
|
|
"""Case-insensitively replace 'assetNN' (1-2 digit) tokens with their labels."""
|
|
|
|
|
|
if not labels:
|
|
|
|
|
|
return prompt
|
|
|
|
|
|
|
|
|
|
|
|
def _sub(m: "re.Match[str]") -> str:
|
|
|
|
|
|
return labels.get(int(m.group(1)), m.group(0))
|
|
|
|
|
|
|
|
|
|
|
|
return _ASSET_REF_RE.sub(_sub, prompt)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _obtain_group_id_via_h5_auth(cls: type[IO.ComfyNode]) -> str:
|
|
|
|
|
|
session = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path="/proxy/seedance/visual-validate/sessions", method="POST"),
|
|
|
|
|
|
response_model=SeedanceCreateVisualValidateSessionResponse,
|
|
|
|
|
|
)
|
|
|
|
|
|
logger.warning("Seedance authentication required. Open link: %s", session.h5_link)
|
|
|
|
|
|
|
|
|
|
|
|
h5_text = f"Open this link in your browser and complete face verification:\n\n{session.h5_link}"
|
|
|
|
|
|
|
|
|
|
|
|
result = await poll_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path=f"/proxy/seedance/visual-validate/sessions/{session.session_id}"),
|
|
|
|
|
|
response_model=SeedanceGetVisualValidateSessionResponse,
|
|
|
|
|
|
status_extractor=lambda r: r.status,
|
|
|
|
|
|
completed_statuses=["completed"],
|
|
|
|
|
|
failed_statuses=["failed"],
|
|
|
|
|
|
poll_interval=_VERIFICATION_POLL_INTERVAL_SEC,
|
|
|
|
|
|
max_poll_attempts=(_VERIFICATION_POLL_TIMEOUT_SEC // _VERIFICATION_POLL_INTERVAL_SEC) - 1,
|
|
|
|
|
|
estimated_duration=_VERIFICATION_POLL_TIMEOUT_SEC - 1,
|
|
|
|
|
|
extra_text=h5_text,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if not result.group_id:
|
|
|
|
|
|
raise RuntimeError(f"Seedance session {session.session_id} completed without a group_id")
|
|
|
|
|
|
|
|
|
|
|
|
logger.warning("Seedance authentication complete. New GroupId: %s", result.group_id)
|
|
|
|
|
|
PromptServer.instance.send_progress_text(
|
|
|
|
|
|
f"Authentication complete. New GroupId: {result.group_id}", cls.hidden.unique_id
|
|
|
|
|
|
)
|
|
|
|
|
|
return result.group_id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _resolve_group_id(cls: type[IO.ComfyNode], group_id: str) -> str:
|
|
|
|
|
|
if group_id and group_id.strip():
|
|
|
|
|
|
return group_id.strip()
|
|
|
|
|
|
return await _obtain_group_id_via_h5_auth(cls)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _create_seedance_asset(
|
|
|
|
|
|
cls: type[IO.ComfyNode],
|
|
|
|
|
|
*,
|
|
|
|
|
|
group_id: str,
|
|
|
|
|
|
url: str,
|
|
|
|
|
|
name: str,
|
|
|
|
|
|
asset_type: str,
|
|
|
|
|
|
) -> str:
|
|
|
|
|
|
req = SeedanceCreateAssetRequest(
|
|
|
|
|
|
group_id=group_id,
|
|
|
|
|
|
url=url,
|
|
|
|
|
|
asset_type=asset_type,
|
|
|
|
|
|
name=name or None,
|
|
|
|
|
|
)
|
|
|
|
|
|
result = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path="/proxy/seedance/assets", method="POST"),
|
|
|
|
|
|
response_model=SeedanceCreateAssetResponse,
|
|
|
|
|
|
data=req,
|
|
|
|
|
|
)
|
|
|
|
|
|
return result.asset_id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _wait_for_asset_active(cls: type[IO.ComfyNode], asset_id: str, group_id: str) -> GetAssetResponse:
|
|
|
|
|
|
"""Poll the newly created asset until its status becomes Active."""
|
|
|
|
|
|
return await poll_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path=f"/proxy/seedance/assets/{asset_id}"),
|
|
|
|
|
|
response_model=GetAssetResponse,
|
|
|
|
|
|
status_extractor=lambda r: r.status,
|
|
|
|
|
|
completed_statuses=["Active"],
|
|
|
|
|
|
failed_statuses=["Failed"],
|
|
|
|
|
|
poll_interval=5,
|
|
|
|
|
|
max_poll_attempts=1200,
|
|
|
|
|
|
extra_text=f"Waiting for asset pre-processing...\n\nasset_id: {asset_id}\n\ngroup_id: {group_id}",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-30 21:49:08 +03:00
|
|
|
|
async def _seedance_virtual_library_upload_image_asset(
|
|
|
|
|
|
cls: type[IO.ComfyNode],
|
|
|
|
|
|
image: torch.Tensor,
|
|
|
|
|
|
*,
|
|
|
|
|
|
wait_label: str = "Uploading image",
|
|
|
|
|
|
) -> str:
|
|
|
|
|
|
"""Upload an image into the caller's per-customer Seedance virtual library."""
|
|
|
|
|
|
public_url = await upload_image_to_comfyapi(cls, image, wait_label=wait_label)
|
|
|
|
|
|
normalized = image.detach().cpu().contiguous().to(torch.float32)
|
|
|
|
|
|
digest = hashlib.sha256()
|
|
|
|
|
|
digest.update(str(tuple(normalized.shape)).encode("utf-8"))
|
|
|
|
|
|
digest.update(b"\0")
|
|
|
|
|
|
digest.update(normalized.numpy().tobytes())
|
|
|
|
|
|
image_hash = digest.hexdigest()
|
|
|
|
|
|
create_resp = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path="/proxy/seedance/virtual-library/assets", method="POST"),
|
|
|
|
|
|
response_model=SeedanceCreateAssetResponse,
|
|
|
|
|
|
data=SeedanceVirtualLibraryCreateAssetRequest(url=public_url, hash=image_hash),
|
|
|
|
|
|
)
|
|
|
|
|
|
await _wait_for_asset_active(cls, create_resp.asset_id, group_id="virtual-library")
|
|
|
|
|
|
return f"asset://{create_resp.asset_id}"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-27 09:44:27 +03:00
|
|
|
|
async def _seedance_virtual_library_upload_video_asset(
|
|
|
|
|
|
cls: type[IO.ComfyNode],
|
|
|
|
|
|
video: Input.Video,
|
|
|
|
|
|
*,
|
|
|
|
|
|
wait_label: str = "Uploading video",
|
|
|
|
|
|
) -> str:
|
|
|
|
|
|
buf = BytesIO()
|
|
|
|
|
|
video.save_to(buf, format=Types.VideoContainer.MP4, codec=Types.VideoCodec.H264)
|
|
|
|
|
|
video_hash = hashlib.sha256(buf.getbuffer()).hexdigest()
|
|
|
|
|
|
public_url = await upload_video_to_comfyapi(cls, video, wait_label=wait_label)
|
|
|
|
|
|
create_resp = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path="/proxy/seedance/virtual-library/assets", method="POST"),
|
|
|
|
|
|
response_model=SeedanceCreateAssetResponse,
|
|
|
|
|
|
data=SeedanceVirtualLibraryCreateAssetRequest(url=public_url, hash=video_hash, asset_type="Video"),
|
|
|
|
|
|
)
|
|
|
|
|
|
await _wait_for_asset_active(cls, create_resp.asset_id, group_id="virtual-library")
|
|
|
|
|
|
return f"asset://{create_resp.asset_id}"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-24 17:57:46 +03:00
|
|
|
|
def _seedance2_price_extractor(model_id: str, has_video_input: bool, resolution: str):
|
2026-04-13 08:26:19 +03:00
|
|
|
|
"""Returns a price_extractor closure for Seedance 2.0 poll_op."""
|
2026-06-24 17:57:46 +03:00
|
|
|
|
rate = seedance2_price_per_1k_tokens(model_id, has_video_input, resolution)
|
2026-04-13 08:26:19 +03:00
|
|
|
|
if rate is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def extractor(response: TaskStatusResponse) -> float | None:
|
|
|
|
|
|
if response.usage is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
return response.usage.total_tokens * 1.43 * rate / 1_000.0
|
|
|
|
|
|
|
|
|
|
|
|
return extractor
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-09-03 23:17:07 +03:00
|
|
|
|
def get_image_url_from_response(response: ImageTaskCreationResponse) -> str:
|
|
|
|
|
|
if response.error:
|
|
|
|
|
|
error_msg = f"ByteDance request failed. Code: {response.error['code']}, message: {response.error['message']}"
|
|
|
|
|
|
logging.info(error_msg)
|
|
|
|
|
|
raise RuntimeError(error_msg)
|
|
|
|
|
|
logging.info("ByteDance task succeeded, image URL: %s", response.data[0]["url"])
|
|
|
|
|
|
return response.data[0]["url"]
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-10-14 09:55:56 +03:00
|
|
|
|
class ByteDanceImageNode(IO.ComfyNode):
|
2025-09-03 23:17:07 +03:00
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls):
|
2025-10-14 09:55:56 +03:00
|
|
|
|
return IO.Schema(
|
2025-09-03 23:17:07 +03:00
|
|
|
|
node_id="ByteDanceImageNode",
|
|
|
|
|
|
display_name="ByteDance Image",
|
2026-06-01 21:24:46 -07:00
|
|
|
|
category="partner/image/ByteDance",
|
2025-09-03 23:17:07 +03:00
|
|
|
|
description="Generate images using ByteDance models via api based on prompt",
|
|
|
|
|
|
inputs=[
|
2025-12-08 11:33:46 +02:00
|
|
|
|
IO.Combo.Input("model", options=["seedream-3-0-t2i-250415"]),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.String.Input(
|
2025-09-03 23:17:07 +03:00
|
|
|
|
"prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
tooltip="The text prompt used to generate the image",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Combo.Input(
|
2025-09-03 23:17:07 +03:00
|
|
|
|
"size_preset",
|
|
|
|
|
|
options=[label for label, _, _ in RECOMMENDED_PRESETS],
|
|
|
|
|
|
tooltip="Pick a recommended size. Select Custom to use the width and height below",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Int.Input(
|
2025-09-03 23:17:07 +03:00
|
|
|
|
"width",
|
|
|
|
|
|
default=1024,
|
|
|
|
|
|
min=512,
|
|
|
|
|
|
max=2048,
|
|
|
|
|
|
step=64,
|
|
|
|
|
|
tooltip="Custom width for image. Value is working only if `size_preset` is set to `Custom`",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Int.Input(
|
2025-09-03 23:17:07 +03:00
|
|
|
|
"height",
|
|
|
|
|
|
default=1024,
|
|
|
|
|
|
min=512,
|
|
|
|
|
|
max=2048,
|
|
|
|
|
|
step=64,
|
|
|
|
|
|
tooltip="Custom height for image. Value is working only if `size_preset` is set to `Custom`",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Int.Input(
|
2025-09-03 23:17:07 +03:00
|
|
|
|
"seed",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
max=2147483647,
|
|
|
|
|
|
step=1,
|
2025-10-14 09:55:56 +03:00
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
2025-09-03 23:17:07 +03:00
|
|
|
|
control_after_generate=True,
|
|
|
|
|
|
tooltip="Seed to use for generation",
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Float.Input(
|
2025-09-03 23:17:07 +03:00
|
|
|
|
"guidance_scale",
|
|
|
|
|
|
default=2.5,
|
|
|
|
|
|
min=1.0,
|
|
|
|
|
|
max=10.0,
|
|
|
|
|
|
step=0.01,
|
2025-10-14 09:55:56 +03:00
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
2025-09-03 23:17:07 +03:00
|
|
|
|
tooltip="Higher value makes the image follow the prompt more closely",
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Boolean.Input(
|
2025-09-03 23:17:07 +03:00
|
|
|
|
"watermark",
|
2025-12-20 08:24:37 +02:00
|
|
|
|
default=False,
|
2025-10-24 08:37:16 +03:00
|
|
|
|
tooltip='Whether to add an "AI generated" watermark to the image',
|
2025-09-03 23:17:07 +03:00
|
|
|
|
optional=True,
|
feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI
Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.
Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification
Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype
Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity
Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: remove advanced=True from DynamicCombo.Input (unsupported)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: address review - un-mark model merge, video, image, and training node widgets as advanced
Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)
Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352
* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)
Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1
---------
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
|
|
|
|
advanced=True,
|
2025-09-03 23:17:07 +03:00
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
outputs=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Image.Output(),
|
2025-09-03 23:17:07 +03:00
|
|
|
|
],
|
|
|
|
|
|
hidden=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Hidden.auth_token_comfy_org,
|
|
|
|
|
|
IO.Hidden.api_key_comfy_org,
|
|
|
|
|
|
IO.Hidden.unique_id,
|
2025-09-03 23:17:07 +03:00
|
|
|
|
],
|
|
|
|
|
|
is_api_node=True,
|
2026-01-14 02:18:28 +02:00
|
|
|
|
price_badge=IO.PriceBadge(
|
|
|
|
|
|
expr="""{"type":"usd","usd":0.03}""",
|
|
|
|
|
|
),
|
2026-03-20 05:05:01 +02:00
|
|
|
|
is_deprecated=True,
|
2025-09-03 23:17:07 +03:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
model: str,
|
|
|
|
|
|
prompt: str,
|
|
|
|
|
|
size_preset: str,
|
|
|
|
|
|
width: int,
|
|
|
|
|
|
height: int,
|
|
|
|
|
|
seed: int,
|
|
|
|
|
|
guidance_scale: float,
|
|
|
|
|
|
watermark: bool,
|
2025-10-14 09:55:56 +03:00
|
|
|
|
) -> IO.NodeOutput:
|
2025-09-03 23:17:07 +03:00
|
|
|
|
validate_string(prompt, strip_whitespace=True, min_length=1)
|
|
|
|
|
|
w = h = None
|
|
|
|
|
|
for label, tw, th in RECOMMENDED_PRESETS:
|
|
|
|
|
|
if label == size_preset:
|
|
|
|
|
|
w, h = tw, th
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
if w is None or h is None:
|
|
|
|
|
|
w, h = width, height
|
|
|
|
|
|
if not (512 <= w <= 2048) or not (512 <= h <= 2048):
|
|
|
|
|
|
raise ValueError(
|
2025-10-24 08:37:16 +03:00
|
|
|
|
f"Custom size out of range: {w}x{h}. " "Both width and height must be between 512 and 2048 pixels."
|
2025-09-03 23:17:07 +03:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
payload = Text2ImageTaskCreationRequest(
|
|
|
|
|
|
model=model,
|
|
|
|
|
|
prompt=prompt,
|
|
|
|
|
|
size=f"{w}x{h}",
|
|
|
|
|
|
seed=seed,
|
|
|
|
|
|
guidance_scale=guidance_scale,
|
|
|
|
|
|
watermark=watermark,
|
|
|
|
|
|
)
|
2025-10-24 08:37:16 +03:00
|
|
|
|
response = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path=BYTEPLUS_IMAGE_ENDPOINT, method="POST"),
|
|
|
|
|
|
data=payload,
|
|
|
|
|
|
response_model=ImageTaskCreationResponse,
|
|
|
|
|
|
)
|
2025-10-14 09:55:56 +03:00
|
|
|
|
return IO.NodeOutput(await download_url_to_image_tensor(get_image_url_from_response(response)))
|
2025-09-03 23:17:07 +03:00
|
|
|
|
|
|
|
|
|
|
|
2025-10-14 09:55:56 +03:00
|
|
|
|
class ByteDanceSeedreamNode(IO.ComfyNode):
|
2025-09-11 00:13:18 +03:00
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls):
|
2025-10-14 09:55:56 +03:00
|
|
|
|
return IO.Schema(
|
2025-09-11 00:13:18 +03:00
|
|
|
|
node_id="ByteDanceSeedreamNode",
|
2026-02-26 22:52:10 +02:00
|
|
|
|
display_name="ByteDance Seedream 4.5 & 5.0",
|
2026-06-01 21:24:46 -07:00
|
|
|
|
category="partner/image/ByteDance",
|
2025-09-11 00:13:18 +03:00
|
|
|
|
description="Unified text-to-image generation and precise single-sentence editing at up to 4K resolution.",
|
|
|
|
|
|
inputs=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Combo.Input(
|
2025-09-11 00:13:18 +03:00
|
|
|
|
"model",
|
2026-02-24 19:03:30 +02:00
|
|
|
|
options=list(SEEDREAM_MODELS.keys()),
|
2025-09-11 00:13:18 +03:00
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.String.Input(
|
2025-09-11 00:13:18 +03:00
|
|
|
|
"prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip="Text prompt for creating or editing an image.",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Image.Input(
|
2025-09-11 00:13:18 +03:00
|
|
|
|
"image",
|
|
|
|
|
|
tooltip="Input image(s) for image-to-image generation. "
|
2026-02-24 19:03:30 +02:00
|
|
|
|
"Reference image(s) for single or multi-reference generation.",
|
2025-09-11 00:13:18 +03:00
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Combo.Input(
|
2025-09-11 00:13:18 +03:00
|
|
|
|
"size_preset",
|
|
|
|
|
|
options=[label for label, _, _ in RECOMMENDED_PRESETS_SEEDREAM_4],
|
|
|
|
|
|
tooltip="Pick a recommended size. Select Custom to use the width and height below.",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Int.Input(
|
2025-09-11 00:13:18 +03:00
|
|
|
|
"width",
|
|
|
|
|
|
default=2048,
|
|
|
|
|
|
min=1024,
|
2026-02-24 19:03:30 +02:00
|
|
|
|
max=6240,
|
|
|
|
|
|
step=2,
|
2025-09-11 00:13:18 +03:00
|
|
|
|
tooltip="Custom width for image. Value is working only if `size_preset` is set to `Custom`",
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Int.Input(
|
2025-09-11 00:13:18 +03:00
|
|
|
|
"height",
|
|
|
|
|
|
default=2048,
|
|
|
|
|
|
min=1024,
|
2026-02-24 19:03:30 +02:00
|
|
|
|
max=4992,
|
|
|
|
|
|
step=2,
|
2025-09-11 00:13:18 +03:00
|
|
|
|
tooltip="Custom height for image. Value is working only if `size_preset` is set to `Custom`",
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Combo.Input(
|
2025-09-11 00:13:18 +03:00
|
|
|
|
"sequential_image_generation",
|
|
|
|
|
|
options=["disabled", "auto"],
|
|
|
|
|
|
tooltip="Group image generation mode. "
|
2025-10-24 08:37:16 +03:00
|
|
|
|
"'disabled' generates a single image. "
|
|
|
|
|
|
"'auto' lets the model decide whether to generate multiple related images "
|
|
|
|
|
|
"(e.g., story scenes, character variations).",
|
2025-09-11 00:13:18 +03:00
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Int.Input(
|
2025-09-11 00:13:18 +03:00
|
|
|
|
"max_images",
|
|
|
|
|
|
default=1,
|
|
|
|
|
|
min=1,
|
|
|
|
|
|
max=15,
|
|
|
|
|
|
step=1,
|
2025-10-14 09:55:56 +03:00
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
2025-09-11 00:13:18 +03:00
|
|
|
|
tooltip="Maximum number of images to generate when sequential_image_generation='auto'. "
|
2025-10-24 08:37:16 +03:00
|
|
|
|
"Total images (input + generated) cannot exceed 15.",
|
2025-09-11 00:13:18 +03:00
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Int.Input(
|
2025-09-11 00:13:18 +03:00
|
|
|
|
"seed",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
max=2147483647,
|
|
|
|
|
|
step=1,
|
2025-10-14 09:55:56 +03:00
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
2025-09-11 00:13:18 +03:00
|
|
|
|
control_after_generate=True,
|
|
|
|
|
|
tooltip="Seed to use for generation.",
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Boolean.Input(
|
2025-09-11 00:13:18 +03:00
|
|
|
|
"watermark",
|
2025-12-20 08:24:37 +02:00
|
|
|
|
default=False,
|
2025-10-24 08:37:16 +03:00
|
|
|
|
tooltip='Whether to add an "AI generated" watermark to the image.',
|
2025-09-11 00:13:18 +03:00
|
|
|
|
optional=True,
|
feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI
Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.
Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification
Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype
Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity
Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: remove advanced=True from DynamicCombo.Input (unsupported)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: address review - un-mark model merge, video, image, and training node widgets as advanced
Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)
Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352
* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)
Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1
---------
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
|
|
|
|
advanced=True,
|
2025-09-11 00:13:18 +03:00
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Boolean.Input(
|
2025-09-19 23:04:51 +03:00
|
|
|
|
"fail_on_partial",
|
|
|
|
|
|
default=True,
|
|
|
|
|
|
tooltip="If enabled, abort execution if any requested images are missing or return an error.",
|
|
|
|
|
|
optional=True,
|
feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI
Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.
Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification
Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype
Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity
Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: remove advanced=True from DynamicCombo.Input (unsupported)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: address review - un-mark model merge, video, image, and training node widgets as advanced
Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)
Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352
* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)
Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1
---------
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
|
|
|
|
advanced=True,
|
2025-09-19 23:04:51 +03:00
|
|
|
|
),
|
2025-09-11 00:13:18 +03:00
|
|
|
|
],
|
|
|
|
|
|
outputs=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Image.Output(),
|
2025-09-11 00:13:18 +03:00
|
|
|
|
],
|
|
|
|
|
|
hidden=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Hidden.auth_token_comfy_org,
|
|
|
|
|
|
IO.Hidden.api_key_comfy_org,
|
|
|
|
|
|
IO.Hidden.unique_id,
|
2025-09-11 00:13:18 +03:00
|
|
|
|
],
|
|
|
|
|
|
is_api_node=True,
|
2026-01-14 02:18:28 +02:00
|
|
|
|
price_badge=IO.PriceBadge(
|
|
|
|
|
|
depends_on=IO.PriceBadgeDepends(widgets=["model"]),
|
|
|
|
|
|
expr="""
|
|
|
|
|
|
(
|
2026-02-24 19:03:30 +02:00
|
|
|
|
$price := $contains(widgets.model, "5.0 lite") ? 0.035 :
|
|
|
|
|
|
$contains(widgets.model, "4-5") ? 0.04 : 0.03;
|
2026-01-14 02:18:28 +02:00
|
|
|
|
{
|
|
|
|
|
|
"type":"usd",
|
|
|
|
|
|
"usd": $price,
|
|
|
|
|
|
"format": { "suffix":" x images/Run", "approximate": true }
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
""",
|
|
|
|
|
|
),
|
2026-05-11 12:53:00 +03:00
|
|
|
|
is_deprecated=True,
|
2025-09-11 00:13:18 +03:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
model: str,
|
|
|
|
|
|
prompt: str,
|
2025-12-08 11:33:46 +02:00
|
|
|
|
image: Input.Image | None = None,
|
2025-09-11 00:13:18 +03:00
|
|
|
|
size_preset: str = RECOMMENDED_PRESETS_SEEDREAM_4[0][0],
|
|
|
|
|
|
width: int = 2048,
|
|
|
|
|
|
height: int = 2048,
|
|
|
|
|
|
sequential_image_generation: str = "disabled",
|
|
|
|
|
|
max_images: int = 1,
|
|
|
|
|
|
seed: int = 0,
|
2025-12-20 08:24:37 +02:00
|
|
|
|
watermark: bool = False,
|
2025-09-19 23:04:51 +03:00
|
|
|
|
fail_on_partial: bool = True,
|
2025-10-14 09:55:56 +03:00
|
|
|
|
) -> IO.NodeOutput:
|
2026-02-24 19:03:30 +02:00
|
|
|
|
model = SEEDREAM_MODELS[model]
|
2025-09-11 00:13:18 +03:00
|
|
|
|
validate_string(prompt, strip_whitespace=True, min_length=1)
|
|
|
|
|
|
w = h = None
|
|
|
|
|
|
for label, tw, th in RECOMMENDED_PRESETS_SEEDREAM_4:
|
|
|
|
|
|
if label == size_preset:
|
|
|
|
|
|
w, h = tw, th
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
if w is None or h is None:
|
|
|
|
|
|
w, h = width, height
|
2026-02-24 19:03:30 +02:00
|
|
|
|
|
2025-12-08 11:33:46 +02:00
|
|
|
|
out_num_pixels = w * h
|
|
|
|
|
|
mp_provided = out_num_pixels / 1_000_000.0
|
2026-02-24 19:03:30 +02:00
|
|
|
|
if ("seedream-4-5" in model or "seedream-5-0" in model) and out_num_pixels < 3686400:
|
2025-12-08 11:33:46 +02:00
|
|
|
|
raise ValueError(
|
2026-04-13 08:26:19 +03:00
|
|
|
|
f"Minimum image resolution for the selected model is 3.68MP, " f"but {mp_provided:.2f}MP provided."
|
2025-12-08 11:33:46 +02:00
|
|
|
|
)
|
|
|
|
|
|
if "seedream-4-0" in model and out_num_pixels < 921600:
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
f"Minimum image resolution that the selected model can generate is 0.92MP, "
|
|
|
|
|
|
f"but {mp_provided:.2f}MP provided."
|
|
|
|
|
|
)
|
2026-02-24 19:03:30 +02:00
|
|
|
|
max_pixels = 10_404_496 if "seedream-5-0" in model else 16_777_216
|
|
|
|
|
|
if out_num_pixels > max_pixels:
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
f"Maximum image resolution for the selected model is {max_pixels / 1_000_000:.2f}MP, "
|
|
|
|
|
|
f"but {mp_provided:.2f}MP provided."
|
|
|
|
|
|
)
|
2025-09-11 00:13:18 +03:00
|
|
|
|
n_input_images = get_number_of_images(image) if image is not None else 0
|
2026-02-24 19:03:30 +02:00
|
|
|
|
max_num_of_images = 14 if model == "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."
|
|
|
|
|
|
)
|
2025-09-11 00:13:18 +03:00
|
|
|
|
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."
|
|
|
|
|
|
)
|
|
|
|
|
|
reference_images_urls = []
|
|
|
|
|
|
if n_input_images:
|
|
|
|
|
|
for i in image:
|
2025-10-30 08:49:03 +02:00
|
|
|
|
validate_image_aspect_ratio(i, (1, 3), (3, 1))
|
2025-10-24 08:37:16 +03:00
|
|
|
|
reference_images_urls = await upload_images_to_comfyapi(
|
|
|
|
|
|
cls,
|
2025-09-11 00:13:18 +03:00
|
|
|
|
image,
|
|
|
|
|
|
max_images=n_input_images,
|
|
|
|
|
|
mime_type="image/png",
|
2025-10-24 08:37:16 +03:00
|
|
|
|
)
|
|
|
|
|
|
response = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path=BYTEPLUS_IMAGE_ENDPOINT, method="POST"),
|
|
|
|
|
|
response_model=ImageTaskCreationResponse,
|
|
|
|
|
|
data=Seedream4TaskCreationRequest(
|
|
|
|
|
|
model=model,
|
|
|
|
|
|
prompt=prompt,
|
|
|
|
|
|
image=reference_images_urls,
|
|
|
|
|
|
size=f"{w}x{h}",
|
|
|
|
|
|
seed=seed,
|
|
|
|
|
|
sequential_image_generation=sequential_image_generation,
|
|
|
|
|
|
sequential_image_generation_options=Seedream4Options(max_images=max_images),
|
|
|
|
|
|
watermark=watermark,
|
2026-02-24 19:03:30 +02:00
|
|
|
|
output_format="png" if model == "seedream-5-0-260128" else None,
|
2025-09-11 00:13:18 +03:00
|
|
|
|
),
|
2025-10-24 08:37:16 +03:00
|
|
|
|
)
|
2025-09-11 00:13:18 +03:00
|
|
|
|
if len(response.data) == 1:
|
2025-10-14 09:55:56 +03:00
|
|
|
|
return IO.NodeOutput(await download_url_to_image_tensor(get_image_url_from_response(response)))
|
2025-09-19 23:04:51 +03:00
|
|
|
|
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.")
|
2025-10-14 09:55:56 +03:00
|
|
|
|
return IO.NodeOutput(torch.cat([await download_url_to_image_tensor(i) for i in urls]))
|
2025-09-11 00:13:18 +03:00
|
|
|
|
|
|
|
|
|
|
|
2026-07-09 00:04:19 +03:00
|
|
|
|
def _seedream_model_inputs(
|
|
|
|
|
|
*,
|
|
|
|
|
|
max_ref_images: int,
|
|
|
|
|
|
presets: list,
|
|
|
|
|
|
max_width: int = 6240,
|
|
|
|
|
|
max_height: int = 4992,
|
|
|
|
|
|
supports_batch: bool = True,
|
|
|
|
|
|
):
|
|
|
|
|
|
inputs = [
|
2026-05-11 12:53:00 +03:00
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"size_preset",
|
|
|
|
|
|
options=[label for label, _, _ in presets],
|
|
|
|
|
|
tooltip="Pick a recommended size. Select Custom to use the width and height below.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"width",
|
|
|
|
|
|
default=2048,
|
|
|
|
|
|
min=1024,
|
2026-07-09 00:04:19 +03:00
|
|
|
|
max=max_width,
|
2026-05-11 12:53:00 +03:00
|
|
|
|
step=2,
|
|
|
|
|
|
tooltip="Custom width for image. Value is working only if `size_preset` is set to `Custom`",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"height",
|
|
|
|
|
|
default=2048,
|
|
|
|
|
|
min=1024,
|
2026-07-09 00:04:19 +03:00
|
|
|
|
max=max_height,
|
2026-05-11 12:53:00 +03:00
|
|
|
|
step=2,
|
|
|
|
|
|
tooltip="Custom height for image. Value is working only if `size_preset` is set to `Custom`",
|
|
|
|
|
|
),
|
2026-07-09 00:04:19 +03:00
|
|
|
|
]
|
|
|
|
|
|
if supports_batch:
|
|
|
|
|
|
inputs.append(
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"max_images",
|
|
|
|
|
|
default=1,
|
|
|
|
|
|
min=1,
|
|
|
|
|
|
max=max_ref_images,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
|
|
|
|
|
tooltip="Maximum number of images to generate. With 1, exactly one image is produced. "
|
|
|
|
|
|
"With >1, the model generates between 1 and max_images related images "
|
|
|
|
|
|
"(e.g., story scenes, character variations). "
|
|
|
|
|
|
"Total images (input + generated) cannot exceed 15.",
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
inputs.append(
|
2026-05-11 12:53:00 +03:00
|
|
|
|
IO.Autogrow.Input(
|
|
|
|
|
|
"images",
|
|
|
|
|
|
template=IO.Autogrow.TemplateNames(
|
|
|
|
|
|
IO.Image.Input("image"),
|
|
|
|
|
|
names=[f"image_{i}" for i in range(1, max_ref_images + 1)],
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
),
|
|
|
|
|
|
tooltip=f"Optional reference image(s) for image-to-image or multi-reference generation. "
|
|
|
|
|
|
f"Up to {max_ref_images} images.",
|
2026-07-09 00:04:19 +03:00
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
if supports_batch:
|
|
|
|
|
|
inputs.append(
|
|
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"fail_on_partial",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
tooltip="If enabled, abort execution if any requested images are missing or return an error.",
|
|
|
|
|
|
advanced=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
return inputs
|
2026-05-11 12:53:00 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ByteDanceSeedreamNodeV2(IO.ComfyNode):
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls):
|
|
|
|
|
|
return IO.Schema(
|
|
|
|
|
|
node_id="ByteDanceSeedreamNodeV2",
|
|
|
|
|
|
display_name="ByteDance Seedream 4.5 & 5.0",
|
2026-06-01 21:24:46 -07:00
|
|
|
|
category="partner/image/ByteDance",
|
2026-05-11 12:53:00 +03:00
|
|
|
|
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=[
|
2026-07-09 00:04:19 +03:00
|
|
|
|
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,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-05-11 12:53:00 +03:00
|
|
|
|
IO.DynamicCombo.Option(
|
|
|
|
|
|
"seedream 5.0 lite",
|
|
|
|
|
|
_seedream_model_inputs(max_ref_images=14, presets=RECOMMENDED_PRESETS_SEEDREAM_5_LITE),
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.DynamicCombo.Option(
|
|
|
|
|
|
"seedream-4-5-251128",
|
|
|
|
|
|
_seedream_model_inputs(max_ref_images=10, presets=RECOMMENDED_PRESETS_SEEDREAM_4_5),
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.DynamicCombo.Option(
|
|
|
|
|
|
"seedream-4-0-250828",
|
|
|
|
|
|
_seedream_model_inputs(max_ref_images=10, presets=RECOMMENDED_PRESETS_SEEDREAM_4_0),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"seed",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
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,
|
|
|
|
|
|
),
|
2026-07-10 11:58:22 +03:00
|
|
|
|
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)."
|
|
|
|
|
|
),
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
advanced=True,
|
|
|
|
|
|
),
|
2026-05-11 12:53:00 +03:00
|
|
|
|
],
|
|
|
|
|
|
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(
|
2026-07-09 00:04:19 +03:00
|
|
|
|
depends_on=IO.PriceBadgeDepends(
|
|
|
|
|
|
widgets=["model", "model.size_preset", "model.width", "model.height"]
|
|
|
|
|
|
),
|
2026-05-11 12:53:00 +03:00
|
|
|
|
expr="""
|
|
|
|
|
|
(
|
2026-07-09 00:04:19 +03:00
|
|
|
|
$sp := $lookup(widgets, "model.size_preset");
|
|
|
|
|
|
$px := $lookup(widgets, "model.width") * $lookup(widgets, "model.height");
|
|
|
|
|
|
$isPro := $contains(widgets.model, "5.0 pro");
|
|
|
|
|
|
$price := $isPro
|
|
|
|
|
|
? (
|
|
|
|
|
|
$contains($sp, "custom")
|
|
|
|
|
|
? ($px <= 2360000 ? 0.045 : 0.09)
|
|
|
|
|
|
: ($contains($sp, "1k") ? 0.045 : 0.09)
|
|
|
|
|
|
)
|
|
|
|
|
|
: $contains(widgets.model, "5.0 lite") ? 0.035
|
|
|
|
|
|
: $contains(widgets.model, "4-5") ? 0.04
|
|
|
|
|
|
: 0.03;
|
2026-05-11 12:53:00 +03:00
|
|
|
|
{
|
2026-07-09 00:04:19 +03:00
|
|
|
|
"type": "usd",
|
2026-05-11 12:53:00 +03:00
|
|
|
|
"usd": $price,
|
2026-07-09 00:04:19 +03:00
|
|
|
|
"format": { "suffix": $isPro ? "/Image" : " x images/Run", "approximate": true }
|
2026-05-11 12:53:00 +03:00
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
""",
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
prompt: str,
|
|
|
|
|
|
model: dict,
|
|
|
|
|
|
seed: int = 0,
|
|
|
|
|
|
watermark: bool = False,
|
2026-07-10 11:58:22 +03:00
|
|
|
|
thinking: bool = True,
|
2026-05-11 12:53:00 +03:00
|
|
|
|
) -> IO.NodeOutput:
|
|
|
|
|
|
validate_string(prompt, strip_whitespace=True, min_length=1)
|
|
|
|
|
|
model_id = SEEDREAM_MODELS[model["model"]]
|
|
|
|
|
|
presets = SEEDREAM_PRESETS[model_id]
|
2026-07-09 00:04:19 +03:00
|
|
|
|
is_pro = "seedream-5-0-pro" in model_id
|
2026-05-11 12:53:00 +03:00
|
|
|
|
|
|
|
|
|
|
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
|
2026-07-09 00:04:19 +03:00
|
|
|
|
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."
|
|
|
|
|
|
)
|
2026-05-11 12:53:00 +03:00
|
|
|
|
|
|
|
|
|
|
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."
|
|
|
|
|
|
)
|
2026-07-10 11:58:22 +03:00
|
|
|
|
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."
|
|
|
|
|
|
)
|
2026-05-11 12:53:00 +03:00
|
|
|
|
|
|
|
|
|
|
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",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-07-10 11:58:22 +03:00
|
|
|
|
optimize_prompt_options = None
|
|
|
|
|
|
if n_input_images == 0:
|
|
|
|
|
|
optimize_prompt_options = Seedream5OptimizePromptOptions(thinking="enabled" if thinking else "disabled")
|
2026-05-11 12:53:00 +03:00
|
|
|
|
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,
|
2026-07-09 00:04:19 +03:00
|
|
|
|
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),
|
2026-05-11 12:53:00 +03:00
|
|
|
|
watermark=watermark,
|
2026-07-10 11:58:22 +03:00
|
|
|
|
optimize_prompt_options=optimize_prompt_options,
|
2026-05-11 12:53:00 +03:00
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
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]))
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-07 16:25:37 -04:00
|
|
|
|
class ByteDanceSeedreamLayerSeparationNode(IO.ComfyNode):
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls):
|
|
|
|
|
|
return IO.Schema(
|
|
|
|
|
|
node_id="ByteDanceSeedreamLayerSeparationNode",
|
|
|
|
|
|
display_name="ByteDance Seedream 5.0 Pro Layer Separation",
|
|
|
|
|
|
category="partner/image/ByteDance",
|
|
|
|
|
|
search_aliases=["layer separation", "split layers", "decompose", "cutout", "RGBA layers"],
|
|
|
|
|
|
description=(
|
|
|
|
|
|
"Decompose an image into a background plate plus up to 16 repositionable transparent layers, "
|
|
|
|
|
|
"each with stacking order, bounding box, name and description."
|
|
|
|
|
|
),
|
|
|
|
|
|
inputs=[
|
|
|
|
|
|
IO.Image.Input(
|
|
|
|
|
|
"image",
|
|
|
|
|
|
tooltip=(
|
|
|
|
|
|
"The image to separate. Exactly one image, at least 512x512 pixels, aspect ratio "
|
|
|
|
|
|
"between 1:16 and 16:1. Inputs larger than about 4MP are downscaled before upload."
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.String.Input(
|
|
|
|
|
|
"prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip=(
|
|
|
|
|
|
"How to separate the image. Leave empty to auto-detect and separate all major elements. "
|
|
|
|
|
|
"Describe elements in natural language to control the separation, or target exact regions "
|
|
|
|
|
|
"with <bbox>left top right bottom</bbox> tags (0-1000 per-mille coordinates)."
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"size",
|
|
|
|
|
|
options=["auto", "1K", "1.5K", "2K"],
|
|
|
|
|
|
default="auto",
|
|
|
|
|
|
tooltip="Output resolution level. 'auto' follows the input image size (clamped to the 1K-2K range).",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"seed",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
max=2147483647,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
|
|
|
|
|
control_after_generate=True,
|
|
|
|
|
|
tooltip="Seed to use for generation.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"prompt_optimization",
|
|
|
|
|
|
options=["standard", "fast"],
|
|
|
|
|
|
default="standard",
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
advanced=True,
|
|
|
|
|
|
tooltip="Prompt-optimization mode: 'standard' gives higher quality, 'fast' shorter generation time.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"watermark",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
advanced=True,
|
|
|
|
|
|
tooltip='Whether to add an "AI generated" watermark to the images.',
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"crop_layers",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
label_on="minimal size",
|
|
|
|
|
|
label_off="full canvas",
|
|
|
|
|
|
tooltip=(
|
|
|
|
|
|
"Geometry of the layers/masks batch outputs (layer_stack is unaffected and always "
|
|
|
|
|
|
"tight). Full canvas: each layer on a base-sized canvas at its bounding-box position - "
|
|
|
|
|
|
"recompose directly with ImageCompositeMasked. Minimal size: each layer cropped to its "
|
|
|
|
|
|
"bounding box (padded to the largest layer for batching) - much smaller tensors; "
|
|
|
|
|
|
"rebuild placement with Layers From Bounding Boxes using the bboxes output."
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
outputs=[
|
|
|
|
|
|
IO.Image.Output(
|
|
|
|
|
|
display_name="base_image",
|
|
|
|
|
|
tooltip="The base image (background plate) the layers stack onto.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Mask.Output(
|
|
|
|
|
|
display_name="base_mask",
|
|
|
|
|
|
tooltip=(
|
|
|
|
|
|
"Transparency of the base image (1 = transparent, LoadImage convention); currently "
|
|
|
|
|
|
"always fully opaque."
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Image.Output(
|
|
|
|
|
|
display_name="layers",
|
|
|
|
|
|
tooltip=(
|
|
|
|
|
|
"Transparent layers ordered bottom to top. Full canvas mode: placed on a black "
|
|
|
|
|
|
"base-sized canvas at their bounding-box position. Minimal size mode: cropped to "
|
|
|
|
|
|
"their bounding box, anchored top-left, padded to the largest layer."
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Mask.Output(
|
|
|
|
|
|
display_name="masks",
|
|
|
|
|
|
tooltip=(
|
|
|
|
|
|
"Per-layer transparency, index-aligned with the layers batch (1 = transparent, "
|
|
|
|
|
|
"LoadImage convention). For ImageCompositeMasked-style compositing, add InvertMask first."
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.BoundingBox.Output(
|
|
|
|
|
|
display_name="bboxes",
|
|
|
|
|
|
tooltip=(
|
|
|
|
|
|
"One placement box per layer, index-aligned with the layers batch (feed both, plus "
|
|
|
|
|
|
"masks, into Layers From Bounding Boxes to rebuild per-layer placement): {x, y, width, "
|
|
|
|
|
|
"height, metadata: {name, desc, z_index, native_size, content_rect, flags}}. "
|
|
|
|
|
|
"content_rect = [left, top, width, height] is the layer's content region within its "
|
|
|
|
|
|
"own frame; it lands on the canvas at the box position plus that offset."
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Layers.Output(
|
|
|
|
|
|
display_name="layer_stack",
|
|
|
|
|
|
tooltip=(
|
|
|
|
|
|
"Ready-to-edit layer document for Create Layered Image: the base plate plus each "
|
|
|
|
|
|
"element as its own named, tight-cropped layer at its true position and stacking "
|
|
|
|
|
|
"order. Connect directly, or extend with Add Layer."
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
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=["size"]),
|
|
|
|
|
|
expr="""
|
|
|
|
|
|
(
|
|
|
|
|
|
widgets.size in ["1k", "1.5k"]
|
|
|
|
|
|
? {
|
|
|
|
|
|
"type": "usd",
|
|
|
|
|
|
"usd": 0.032,
|
|
|
|
|
|
"format": { "suffix": " x images/Run", "approximate": true }
|
|
|
|
|
|
}
|
|
|
|
|
|
: {
|
|
|
|
|
|
"type": "range_usd",
|
|
|
|
|
|
"min_usd": 0.032,
|
|
|
|
|
|
"max_usd": 0.064,
|
|
|
|
|
|
"format": { "suffix": " x images/Run", "approximate": true }
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
""",
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
image: Input.Image,
|
|
|
|
|
|
prompt: str = "",
|
|
|
|
|
|
size: str = "auto",
|
|
|
|
|
|
seed: int = 0,
|
|
|
|
|
|
prompt_optimization: str = "standard",
|
|
|
|
|
|
watermark: bool = False,
|
|
|
|
|
|
crop_layers: bool = False,
|
|
|
|
|
|
) -> IO.NodeOutput:
|
|
|
|
|
|
if get_number_of_images(image) != 1:
|
|
|
|
|
|
raise ValueError("Only a single input image is supported.")
|
|
|
|
|
|
validate_image_aspect_ratio(image, (1, 16), (16, 1), strict=False)
|
|
|
|
|
|
validate_image_dimensions(image, min_width=512, min_height=512)
|
|
|
|
|
|
|
|
|
|
|
|
request = Seedream5LayerSeparationRequest(
|
|
|
|
|
|
model=SEEDREAM_LAYER_SEPARATION_MODEL,
|
|
|
|
|
|
prompt=prompt.strip() or None,
|
|
|
|
|
|
image=await upload_image_to_comfyapi(cls, image),
|
|
|
|
|
|
size=size,
|
|
|
|
|
|
seed=seed,
|
|
|
|
|
|
watermark=watermark,
|
|
|
|
|
|
optimize_prompt_options=Seedream5LayerOptimizePromptOptions(mode=prompt_optimization),
|
|
|
|
|
|
)
|
|
|
|
|
|
response = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path=BYTEPLUS_IMAGE_ENDPOINT, method="POST"),
|
|
|
|
|
|
response_model=ImageTaskCreationResponse,
|
|
|
|
|
|
data=request,
|
|
|
|
|
|
wait_label="Separating layers",
|
|
|
|
|
|
)
|
|
|
|
|
|
if response.error:
|
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
|
f"ByteDance request failed. Code: {response.error['code']}, message: {response.error['message']}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def z_index_of(d: dict) -> int:
|
|
|
|
|
|
v = d.get("z_index")
|
|
|
|
|
|
if isinstance(v, bool):
|
|
|
|
|
|
return 1_000_000
|
|
|
|
|
|
if isinstance(v, (int, float)):
|
|
|
|
|
|
return int(v)
|
|
|
|
|
|
if isinstance(v, str):
|
|
|
|
|
|
try:
|
|
|
|
|
|
return int(v.strip())
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
return 1_000_000
|
|
|
|
|
|
return 1_000_000
|
|
|
|
|
|
|
|
|
|
|
|
data = [d for d in (response.data or []) if isinstance(d, dict)]
|
|
|
|
|
|
if not data or "url" not in data[0]:
|
|
|
|
|
|
raise RuntimeError("Unexpected response: no base image returned.")
|
|
|
|
|
|
base_item = data[0]
|
|
|
|
|
|
if base_item.get("bounding_box") is not None:
|
|
|
|
|
|
logging.warning(
|
|
|
|
|
|
"ByteDance layer separation: base item unexpectedly carries a bounding_box; ignoring it."
|
|
|
|
|
|
)
|
|
|
|
|
|
if z_index_of(base_item) not in (0, 1_000_000):
|
|
|
|
|
|
raise RuntimeError("Unexpected response: the first item is not the base image.")
|
|
|
|
|
|
layer_items = [d for d in data[1:] if "url" in d]
|
|
|
|
|
|
dropped = len(data) - 1 - len(layer_items)
|
|
|
|
|
|
if dropped > 0:
|
|
|
|
|
|
logging.warning(
|
|
|
|
|
|
"ByteDance layer separation: %d of %d returned elements had no 'url' and were dropped.",
|
|
|
|
|
|
dropped,
|
|
|
|
|
|
len(data) - 1,
|
|
|
|
|
|
)
|
|
|
|
|
|
if not layer_items:
|
|
|
|
|
|
raise RuntimeError("The model returned no layers. Try a different prompt or input image.")
|
|
|
|
|
|
layer_items.sort(key=z_index_of)
|
|
|
|
|
|
|
|
|
|
|
|
base_image = (await download_url_to_image_tensor(str(base_item["url"])))[..., :3].contiguous()
|
|
|
|
|
|
height, width = base_image.shape[1], base_image.shape[2]
|
|
|
|
|
|
|
|
|
|
|
|
specs = []
|
|
|
|
|
|
for item in layer_items:
|
|
|
|
|
|
flags = []
|
|
|
|
|
|
bbox = item.get("bounding_box")
|
|
|
|
|
|
absolute = bbox.get("absolute") if isinstance(bbox, dict) else None
|
|
|
|
|
|
if (
|
|
|
|
|
|
isinstance(absolute, (list, tuple))
|
|
|
|
|
|
and len(absolute) == 4
|
|
|
|
|
|
and all(isinstance(v, (int, float)) and not isinstance(v, bool) for v in absolute)
|
|
|
|
|
|
):
|
|
|
|
|
|
left, top, right, bottom = (int(round(v)) for v in absolute)
|
|
|
|
|
|
rect_w, rect_h = right - left, bottom - top # exclusive right/bottom
|
|
|
|
|
|
if rect_w > width or rect_h > height:
|
|
|
|
|
|
rect_w, rect_h = min(rect_w, width), min(rect_h, height)
|
|
|
|
|
|
flags.append("bbox_clamped")
|
|
|
|
|
|
if rect_w <= 0 or rect_h <= 0:
|
|
|
|
|
|
flags.append("bbox_degenerate")
|
|
|
|
|
|
else:
|
|
|
|
|
|
flags.append("bbox_missing")
|
|
|
|
|
|
left, top, rect_w, rect_h = 0, 0, width, height
|
|
|
|
|
|
specs.append({"item": item, "flags": flags, "left": left, "top": top,
|
|
|
|
|
|
"rect_w": rect_w, "rect_h": rect_h, "native_size": "", "stack_item": None})
|
|
|
|
|
|
|
|
|
|
|
|
if crop_layers:
|
|
|
|
|
|
canvas_w = max((s["rect_w"] for s in specs if "bbox_degenerate" not in s["flags"]), default=1)
|
|
|
|
|
|
canvas_h = max((s["rect_h"] for s in specs if "bbox_degenerate" not in s["flags"]), default=1)
|
|
|
|
|
|
else:
|
|
|
|
|
|
canvas_w, canvas_h = width, height
|
|
|
|
|
|
base_mask = torch.zeros((1, height, width))
|
|
|
|
|
|
layers = torch.zeros((len(specs), canvas_h, canvas_w, 3))
|
|
|
|
|
|
# Create Layered Image / LoadImage mask convention: 1 = transparent
|
|
|
|
|
|
masks = torch.ones((len(specs), canvas_h, canvas_w))
|
|
|
|
|
|
|
|
|
|
|
|
semaphore = asyncio.Semaphore(4)
|
|
|
|
|
|
|
|
|
|
|
|
async def fetch_and_place(i: int, spec: dict) -> None:
|
|
|
|
|
|
item, flags = spec["item"], spec["flags"]
|
|
|
|
|
|
left, top, rect_w, rect_h = spec["left"], spec["top"], spec["rect_w"], spec["rect_h"]
|
|
|
|
|
|
async with semaphore:
|
|
|
|
|
|
try:
|
|
|
|
|
|
rgba = (await download_url_to_image_tensor(str(item["url"])))[0]
|
|
|
|
|
|
except ProcessingInterrupted:
|
|
|
|
|
|
raise
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
|
f"Failed to download layer {i + 1} of {len(specs)} (name={item.get('name')!r}): {exc} "
|
|
|
|
|
|
"The generation completed and was billed; the response with all layer URLs "
|
|
|
|
|
|
"is in ComfyUI/temp/api_logs/."
|
|
|
|
|
|
) from exc
|
|
|
|
|
|
spec["native_size"] = f"{rgba.shape[1]}x{rgba.shape[0]}"
|
|
|
|
|
|
if "bbox_degenerate" in flags:
|
|
|
|
|
|
return
|
|
|
|
|
|
if (rgba.shape[1], rgba.shape[0]) != (rect_w, rect_h):
|
|
|
|
|
|
# premultiply before resizing: interpolating straight alpha bleeds the undefined
|
|
|
|
|
|
# colors of transparent pixels into the anti-aliased edges
|
|
|
|
|
|
rgba = rgba.clone()
|
|
|
|
|
|
rgba[..., :3] *= rgba[..., 3:4]
|
|
|
|
|
|
rgba = (
|
|
|
|
|
|
torch.nn.functional.interpolate(
|
|
|
|
|
|
rgba.permute(2, 0, 1).unsqueeze(0),
|
|
|
|
|
|
size=(rect_h, rect_w),
|
|
|
|
|
|
mode="bilinear",
|
|
|
|
|
|
antialias=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
.squeeze(0)
|
|
|
|
|
|
.permute(1, 2, 0)
|
|
|
|
|
|
)
|
|
|
|
|
|
alpha = rgba[..., 3:4]
|
|
|
|
|
|
rgba = torch.cat([rgba[..., :3] / alpha.clamp(min=1e-6), alpha], dim=-1).clamp(0, 1)
|
|
|
|
|
|
flags.append("resized_to_bbox")
|
|
|
|
|
|
# straight (unpremultiplied) RGB: downstream compositing applies the mask itself
|
|
|
|
|
|
if crop_layers:
|
|
|
|
|
|
layers[i, :rect_h, :rect_w] = rgba[..., :3]
|
|
|
|
|
|
masks[i, :rect_h, :rect_w] = 1.0 - rgba[..., 3]
|
|
|
|
|
|
else:
|
|
|
|
|
|
x0, y0 = max(left, 0), max(top, 0)
|
|
|
|
|
|
x1, y1 = min(left + rect_w, width), min(top + rect_h, height)
|
|
|
|
|
|
if x0 < x1 and y0 < y1:
|
|
|
|
|
|
patch = rgba[y0 - top : y1 - top, x0 - left : x1 - left]
|
|
|
|
|
|
layers[i, y0:y1, x0:x1] = patch[..., :3]
|
|
|
|
|
|
masks[i, y0:y1, x0:x1] = 1.0 - patch[..., 3]
|
|
|
|
|
|
else:
|
|
|
|
|
|
flags.append("bbox_out_of_canvas")
|
|
|
|
|
|
zi = z_index_of(item)
|
|
|
|
|
|
stack_item = {
|
|
|
|
|
|
"image": rgba[..., :3].unsqueeze(0).contiguous(),
|
|
|
|
|
|
"type": "raster",
|
|
|
|
|
|
"x": left,
|
|
|
|
|
|
"y": top,
|
|
|
|
|
|
"z_index": zi if zi != 1_000_000 else i + 1,
|
|
|
|
|
|
"mask": (1.0 - rgba[..., 3]).unsqueeze(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
if isinstance(item.get("name"), str):
|
|
|
|
|
|
stack_item["name"] = item["name"]
|
|
|
|
|
|
spec["stack_item"] = stack_item
|
|
|
|
|
|
|
|
|
|
|
|
await asyncio.gather(*(fetch_and_place(i, s) for i, s in enumerate(specs)))
|
|
|
|
|
|
|
|
|
|
|
|
stack_items = [{"image": base_image, "type": "raster", "x": 0, "y": 0, "z_index": 0, "name": "background"}]
|
|
|
|
|
|
boxes = []
|
|
|
|
|
|
for i, s in enumerate(specs):
|
|
|
|
|
|
abnormal = [f for f in s["flags"] if f != "resized_to_bbox"]
|
|
|
|
|
|
if abnormal:
|
|
|
|
|
|
logging.warning(
|
|
|
|
|
|
"ByteDance layer separation: layer %d (%r) flagged %s.",
|
|
|
|
|
|
i + 1,
|
|
|
|
|
|
s["item"].get("name"),
|
|
|
|
|
|
", ".join(abnormal),
|
|
|
|
|
|
)
|
|
|
|
|
|
if s["stack_item"] is not None:
|
|
|
|
|
|
stack_items.append(s["stack_item"])
|
|
|
|
|
|
zi = z_index_of(s["item"])
|
|
|
|
|
|
# placement box sized to this layer's tensor so Create Layered Image renders it 1:1;
|
|
|
|
|
|
# the true content rect travels in metadata, frame-relative
|
|
|
|
|
|
rect_x, rect_y = (0, 0) if crop_layers else (s["left"], s["top"])
|
|
|
|
|
|
boxes.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"x": s["left"] if crop_layers else 0,
|
|
|
|
|
|
"y": s["top"] if crop_layers else 0,
|
|
|
|
|
|
"width": canvas_w,
|
|
|
|
|
|
"height": canvas_h,
|
|
|
|
|
|
"metadata": {
|
|
|
|
|
|
"name": s["item"].get("name"),
|
|
|
|
|
|
"desc": s["item"].get("description"),
|
|
|
|
|
|
"z_index": zi if zi != 1_000_000 else None,
|
|
|
|
|
|
"native_size": s["native_size"],
|
|
|
|
|
|
"content_rect": [rect_x, rect_y, max(s["rect_w"], 0), max(s["rect_h"], 0)],
|
|
|
|
|
|
"flags": s["flags"],
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
# a single frame holding every box: the per-frame BOUNDING_BOX shape for boxes that
|
|
|
|
|
|
# annotate one image, as emitted and consumed by CreateBoundingBoxes
|
|
|
|
|
|
bboxes = [boxes]
|
|
|
|
|
|
layer_stack = {"version": 1, "canvas": (width, height), "layers": stack_items}
|
|
|
|
|
|
return IO.NodeOutput(base_image, base_mask, layers, masks, bboxes, layer_stack)
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-10-14 09:55:56 +03:00
|
|
|
|
class ByteDanceTextToVideoNode(IO.ComfyNode):
|
2025-09-09 21:40:29 +03:00
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls):
|
2025-10-14 09:55:56 +03:00
|
|
|
|
return IO.Schema(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
node_id="ByteDanceTextToVideoNode",
|
|
|
|
|
|
display_name="ByteDance Text to Video",
|
2026-06-01 21:24:46 -07:00
|
|
|
|
category="partner/video/ByteDance",
|
2025-09-09 21:40:29 +03:00
|
|
|
|
description="Generate video using ByteDance models via api based on prompt",
|
|
|
|
|
|
inputs=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Combo.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"model",
|
2026-01-16 08:09:07 +02:00
|
|
|
|
options=[
|
|
|
|
|
|
"seedance-1-5-pro-251215",
|
|
|
|
|
|
"seedance-1-0-pro-250528",
|
|
|
|
|
|
"seedance-1-0-lite-t2v-250428",
|
|
|
|
|
|
"seedance-1-0-pro-fast-251015",
|
|
|
|
|
|
],
|
2025-12-08 11:33:46 +02:00
|
|
|
|
default="seedance-1-0-pro-fast-251015",
|
2025-09-09 21:40:29 +03:00
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.String.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
tooltip="The text prompt used to generate the video.",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Combo.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"resolution",
|
|
|
|
|
|
options=["480p", "720p", "1080p"],
|
|
|
|
|
|
tooltip="The resolution of the output video.",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Combo.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"aspect_ratio",
|
|
|
|
|
|
options=["16:9", "4:3", "1:1", "3:4", "9:16", "21:9"],
|
|
|
|
|
|
tooltip="The aspect ratio of the output video.",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Int.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"duration",
|
|
|
|
|
|
default=5,
|
|
|
|
|
|
min=3,
|
|
|
|
|
|
max=12,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
tooltip="The duration of the output video in seconds.",
|
2025-10-14 09:55:56 +03:00
|
|
|
|
display_mode=IO.NumberDisplay.slider,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Int.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"seed",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
max=2147483647,
|
|
|
|
|
|
step=1,
|
2025-10-14 09:55:56 +03:00
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
control_after_generate=True,
|
|
|
|
|
|
tooltip="Seed to use for generation.",
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Boolean.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"camera_fixed",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
tooltip="Specifies whether to fix the camera. The platform appends an instruction "
|
2025-10-24 08:37:16 +03:00
|
|
|
|
"to fix the camera to your prompt, but does not guarantee the actual effect.",
|
2025-09-09 21:40:29 +03:00
|
|
|
|
optional=True,
|
feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI
Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.
Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification
Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype
Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity
Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: remove advanced=True from DynamicCombo.Input (unsupported)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: address review - un-mark model merge, video, image, and training node widgets as advanced
Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)
Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352
* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)
Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1
---------
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
|
|
|
|
advanced=True,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Boolean.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"watermark",
|
2025-12-20 08:24:37 +02:00
|
|
|
|
default=False,
|
2025-10-24 08:37:16 +03:00
|
|
|
|
tooltip='Whether to add an "AI generated" watermark to the video.',
|
2025-09-09 21:40:29 +03:00
|
|
|
|
optional=True,
|
feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI
Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.
Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification
Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype
Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity
Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: remove advanced=True from DynamicCombo.Input (unsupported)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: address review - un-mark model merge, video, image, and training node widgets as advanced
Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)
Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352
* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)
Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1
---------
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
|
|
|
|
advanced=True,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
),
|
2026-01-16 08:09:07 +02:00
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"generate_audio",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
tooltip="This parameter is ignored for any model except seedance-1-5-pro.",
|
|
|
|
|
|
optional=True,
|
feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI
Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.
Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification
Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype
Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity
Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: remove advanced=True from DynamicCombo.Input (unsupported)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: address review - un-mark model merge, video, image, and training node widgets as advanced
Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)
Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352
* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)
Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1
---------
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
|
|
|
|
advanced=True,
|
2026-01-16 08:09:07 +02:00
|
|
|
|
),
|
2025-09-09 21:40:29 +03:00
|
|
|
|
],
|
|
|
|
|
|
outputs=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Video.Output(),
|
2025-09-09 21:40:29 +03:00
|
|
|
|
],
|
|
|
|
|
|
hidden=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Hidden.auth_token_comfy_org,
|
|
|
|
|
|
IO.Hidden.api_key_comfy_org,
|
|
|
|
|
|
IO.Hidden.unique_id,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
],
|
|
|
|
|
|
is_api_node=True,
|
2026-01-14 02:18:28 +02:00
|
|
|
|
price_badge=PRICE_BADGE_VIDEO,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
model: str,
|
|
|
|
|
|
prompt: str,
|
|
|
|
|
|
resolution: str,
|
|
|
|
|
|
aspect_ratio: str,
|
|
|
|
|
|
duration: int,
|
|
|
|
|
|
seed: int,
|
|
|
|
|
|
camera_fixed: bool,
|
|
|
|
|
|
watermark: bool,
|
2026-01-16 08:09:07 +02:00
|
|
|
|
generate_audio: bool = False,
|
2025-10-14 09:55:56 +03:00
|
|
|
|
) -> IO.NodeOutput:
|
2026-01-16 08:09:07 +02:00
|
|
|
|
if model == "seedance-1-5-pro-251215" and duration < 4:
|
|
|
|
|
|
raise ValueError("Minimum supported duration for Seedance 1.5 Pro is 4 seconds.")
|
2025-09-09 21:40:29 +03:00
|
|
|
|
validate_string(prompt, strip_whitespace=True, min_length=1)
|
|
|
|
|
|
raise_if_text_params(prompt, ["resolution", "ratio", "duration", "seed", "camerafixed", "watermark"])
|
|
|
|
|
|
|
|
|
|
|
|
prompt = (
|
|
|
|
|
|
f"{prompt} "
|
|
|
|
|
|
f"--resolution {resolution} "
|
|
|
|
|
|
f"--ratio {aspect_ratio} "
|
|
|
|
|
|
f"--duration {duration} "
|
|
|
|
|
|
f"--seed {seed} "
|
|
|
|
|
|
f"--camerafixed {str(camera_fixed).lower()} "
|
|
|
|
|
|
f"--watermark {str(watermark).lower()}"
|
|
|
|
|
|
)
|
|
|
|
|
|
return await process_video_task(
|
2025-10-24 08:37:16 +03:00
|
|
|
|
cls,
|
2026-01-16 08:09:07 +02:00
|
|
|
|
payload=Text2VideoTaskCreationRequest(
|
|
|
|
|
|
model=model,
|
|
|
|
|
|
content=[TaskTextContent(text=prompt)],
|
|
|
|
|
|
generate_audio=generate_audio if model == "seedance-1-5-pro-251215" else None,
|
|
|
|
|
|
),
|
2025-09-09 21:40:29 +03:00
|
|
|
|
estimated_duration=max(1, math.ceil(VIDEO_TASKS_EXECUTION_TIME[model][resolution] * (duration / 10.0))),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-10-14 09:55:56 +03:00
|
|
|
|
class ByteDanceImageToVideoNode(IO.ComfyNode):
|
2025-09-09 21:40:29 +03:00
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls):
|
2025-10-14 09:55:56 +03:00
|
|
|
|
return IO.Schema(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
node_id="ByteDanceImageToVideoNode",
|
|
|
|
|
|
display_name="ByteDance Image to Video",
|
2026-06-01 21:24:46 -07:00
|
|
|
|
category="partner/video/ByteDance",
|
2025-09-09 21:40:29 +03:00
|
|
|
|
description="Generate video using ByteDance models via api based on image and prompt",
|
|
|
|
|
|
inputs=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Combo.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"model",
|
2026-01-16 08:09:07 +02:00
|
|
|
|
options=[
|
|
|
|
|
|
"seedance-1-5-pro-251215",
|
|
|
|
|
|
"seedance-1-0-pro-250528",
|
|
|
|
|
|
"seedance-1-0-lite-i2v-250428",
|
|
|
|
|
|
"seedance-1-0-pro-fast-251015",
|
|
|
|
|
|
],
|
2025-12-08 11:33:46 +02:00
|
|
|
|
default="seedance-1-0-pro-fast-251015",
|
2025-09-09 21:40:29 +03:00
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.String.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
tooltip="The text prompt used to generate the video.",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Image.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"image",
|
|
|
|
|
|
tooltip="First frame to be used for the video.",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Combo.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"resolution",
|
|
|
|
|
|
options=["480p", "720p", "1080p"],
|
|
|
|
|
|
tooltip="The resolution of the output video.",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Combo.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"aspect_ratio",
|
|
|
|
|
|
options=["adaptive", "16:9", "4:3", "1:1", "3:4", "9:16", "21:9"],
|
|
|
|
|
|
tooltip="The aspect ratio of the output video.",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Int.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"duration",
|
|
|
|
|
|
default=5,
|
|
|
|
|
|
min=3,
|
|
|
|
|
|
max=12,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
tooltip="The duration of the output video in seconds.",
|
2025-10-14 09:55:56 +03:00
|
|
|
|
display_mode=IO.NumberDisplay.slider,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Int.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"seed",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
max=2147483647,
|
|
|
|
|
|
step=1,
|
2025-10-14 09:55:56 +03:00
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
control_after_generate=True,
|
|
|
|
|
|
tooltip="Seed to use for generation.",
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Boolean.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"camera_fixed",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
tooltip="Specifies whether to fix the camera. The platform appends an instruction "
|
2025-10-24 08:37:16 +03:00
|
|
|
|
"to fix the camera to your prompt, but does not guarantee the actual effect.",
|
2025-09-09 21:40:29 +03:00
|
|
|
|
optional=True,
|
feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI
Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.
Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification
Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype
Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity
Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: remove advanced=True from DynamicCombo.Input (unsupported)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: address review - un-mark model merge, video, image, and training node widgets as advanced
Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)
Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352
* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)
Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1
---------
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
|
|
|
|
advanced=True,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Boolean.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"watermark",
|
2025-12-20 08:24:37 +02:00
|
|
|
|
default=False,
|
2025-10-24 08:37:16 +03:00
|
|
|
|
tooltip='Whether to add an "AI generated" watermark to the video.',
|
2025-09-09 21:40:29 +03:00
|
|
|
|
optional=True,
|
feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI
Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.
Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification
Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype
Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity
Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: remove advanced=True from DynamicCombo.Input (unsupported)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: address review - un-mark model merge, video, image, and training node widgets as advanced
Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)
Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352
* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)
Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1
---------
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
|
|
|
|
advanced=True,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
),
|
2026-01-16 08:09:07 +02:00
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"generate_audio",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
tooltip="This parameter is ignored for any model except seedance-1-5-pro.",
|
|
|
|
|
|
optional=True,
|
feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI
Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.
Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification
Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype
Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity
Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: remove advanced=True from DynamicCombo.Input (unsupported)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: address review - un-mark model merge, video, image, and training node widgets as advanced
Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)
Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352
* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)
Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1
---------
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
|
|
|
|
advanced=True,
|
2026-01-16 08:09:07 +02:00
|
|
|
|
),
|
2025-09-09 21:40:29 +03:00
|
|
|
|
],
|
|
|
|
|
|
outputs=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Video.Output(),
|
2025-09-09 21:40:29 +03:00
|
|
|
|
],
|
|
|
|
|
|
hidden=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Hidden.auth_token_comfy_org,
|
|
|
|
|
|
IO.Hidden.api_key_comfy_org,
|
|
|
|
|
|
IO.Hidden.unique_id,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
],
|
|
|
|
|
|
is_api_node=True,
|
2026-01-14 02:18:28 +02:00
|
|
|
|
price_badge=PRICE_BADGE_VIDEO,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
model: str,
|
|
|
|
|
|
prompt: str,
|
2025-12-08 11:33:46 +02:00
|
|
|
|
image: Input.Image,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
resolution: str,
|
|
|
|
|
|
aspect_ratio: str,
|
|
|
|
|
|
duration: int,
|
|
|
|
|
|
seed: int,
|
|
|
|
|
|
camera_fixed: bool,
|
|
|
|
|
|
watermark: bool,
|
2026-01-16 08:09:07 +02:00
|
|
|
|
generate_audio: bool = False,
|
2025-10-14 09:55:56 +03:00
|
|
|
|
) -> IO.NodeOutput:
|
2026-01-16 08:09:07 +02:00
|
|
|
|
if model == "seedance-1-5-pro-251215" and duration < 4:
|
|
|
|
|
|
raise ValueError("Minimum supported duration for Seedance 1.5 Pro is 4 seconds.")
|
2025-09-09 21:40:29 +03:00
|
|
|
|
validate_string(prompt, strip_whitespace=True, min_length=1)
|
|
|
|
|
|
raise_if_text_params(prompt, ["resolution", "ratio", "duration", "seed", "camerafixed", "watermark"])
|
|
|
|
|
|
validate_image_dimensions(image, min_width=300, min_height=300, max_width=6000, max_height=6000)
|
2025-10-30 08:49:03 +02:00
|
|
|
|
validate_image_aspect_ratio(image, (2, 5), (5, 2), strict=False) # 0.4 to 2.5
|
2025-09-09 21:40:29 +03:00
|
|
|
|
|
2025-10-24 08:37:16 +03:00
|
|
|
|
image_url = (await upload_images_to_comfyapi(cls, image, max_images=1))[0]
|
2025-09-09 21:40:29 +03:00
|
|
|
|
prompt = (
|
|
|
|
|
|
f"{prompt} "
|
|
|
|
|
|
f"--resolution {resolution} "
|
|
|
|
|
|
f"--ratio {aspect_ratio} "
|
|
|
|
|
|
f"--duration {duration} "
|
|
|
|
|
|
f"--seed {seed} "
|
|
|
|
|
|
f"--camerafixed {str(camera_fixed).lower()} "
|
|
|
|
|
|
f"--watermark {str(watermark).lower()}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return await process_video_task(
|
2025-10-24 08:37:16 +03:00
|
|
|
|
cls,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
payload=Image2VideoTaskCreationRequest(
|
|
|
|
|
|
model=model,
|
|
|
|
|
|
content=[TaskTextContent(text=prompt), TaskImageContent(image_url=TaskImageContentUrl(url=image_url))],
|
2026-01-16 08:09:07 +02:00
|
|
|
|
generate_audio=generate_audio if model == "seedance-1-5-pro-251215" else None,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
),
|
|
|
|
|
|
estimated_duration=max(1, math.ceil(VIDEO_TASKS_EXECUTION_TIME[model][resolution] * (duration / 10.0))),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-10-14 09:55:56 +03:00
|
|
|
|
class ByteDanceFirstLastFrameNode(IO.ComfyNode):
|
2025-09-09 21:40:29 +03:00
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls):
|
2025-10-14 09:55:56 +03:00
|
|
|
|
return IO.Schema(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
node_id="ByteDanceFirstLastFrameNode",
|
|
|
|
|
|
display_name="ByteDance First-Last-Frame to Video",
|
2026-06-01 21:24:46 -07:00
|
|
|
|
category="partner/video/ByteDance",
|
2025-09-09 21:40:29 +03:00
|
|
|
|
description="Generate video using prompt and first and last frames.",
|
|
|
|
|
|
inputs=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Combo.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"model",
|
2026-01-16 08:09:07 +02:00
|
|
|
|
options=["seedance-1-5-pro-251215", "seedance-1-0-pro-250528", "seedance-1-0-lite-i2v-250428"],
|
2025-12-08 11:33:46 +02:00
|
|
|
|
default="seedance-1-0-lite-i2v-250428",
|
2025-09-09 21:40:29 +03:00
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.String.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
tooltip="The text prompt used to generate the video.",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Image.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"first_frame",
|
|
|
|
|
|
tooltip="First frame to be used for the video.",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Image.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"last_frame",
|
|
|
|
|
|
tooltip="Last frame to be used for the video.",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Combo.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"resolution",
|
|
|
|
|
|
options=["480p", "720p", "1080p"],
|
|
|
|
|
|
tooltip="The resolution of the output video.",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Combo.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"aspect_ratio",
|
|
|
|
|
|
options=["adaptive", "16:9", "4:3", "1:1", "3:4", "9:16", "21:9"],
|
|
|
|
|
|
tooltip="The aspect ratio of the output video.",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Int.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"duration",
|
|
|
|
|
|
default=5,
|
|
|
|
|
|
min=3,
|
|
|
|
|
|
max=12,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
tooltip="The duration of the output video in seconds.",
|
2025-10-14 09:55:56 +03:00
|
|
|
|
display_mode=IO.NumberDisplay.slider,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Int.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"seed",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
max=2147483647,
|
|
|
|
|
|
step=1,
|
2025-10-14 09:55:56 +03:00
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
control_after_generate=True,
|
|
|
|
|
|
tooltip="Seed to use for generation.",
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Boolean.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"camera_fixed",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
tooltip="Specifies whether to fix the camera. The platform appends an instruction "
|
2025-10-24 08:37:16 +03:00
|
|
|
|
"to fix the camera to your prompt, but does not guarantee the actual effect.",
|
2025-09-09 21:40:29 +03:00
|
|
|
|
optional=True,
|
feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI
Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.
Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification
Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype
Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity
Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: remove advanced=True from DynamicCombo.Input (unsupported)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: address review - un-mark model merge, video, image, and training node widgets as advanced
Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)
Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352
* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)
Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1
---------
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
|
|
|
|
advanced=True,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Boolean.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"watermark",
|
2025-12-20 08:24:37 +02:00
|
|
|
|
default=False,
|
2025-10-24 08:37:16 +03:00
|
|
|
|
tooltip='Whether to add an "AI generated" watermark to the video.',
|
2025-09-09 21:40:29 +03:00
|
|
|
|
optional=True,
|
feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI
Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.
Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification
Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype
Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity
Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: remove advanced=True from DynamicCombo.Input (unsupported)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: address review - un-mark model merge, video, image, and training node widgets as advanced
Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)
Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352
* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)
Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1
---------
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
|
|
|
|
advanced=True,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
),
|
2026-01-16 08:09:07 +02:00
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"generate_audio",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
tooltip="This parameter is ignored for any model except seedance-1-5-pro.",
|
|
|
|
|
|
optional=True,
|
feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI
Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.
Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification
Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype
Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity
Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: remove advanced=True from DynamicCombo.Input (unsupported)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: address review - un-mark model merge, video, image, and training node widgets as advanced
Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)
Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352
* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)
Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1
---------
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
|
|
|
|
advanced=True,
|
2026-01-16 08:09:07 +02:00
|
|
|
|
),
|
2025-09-09 21:40:29 +03:00
|
|
|
|
],
|
|
|
|
|
|
outputs=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Video.Output(),
|
2025-09-09 21:40:29 +03:00
|
|
|
|
],
|
|
|
|
|
|
hidden=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Hidden.auth_token_comfy_org,
|
|
|
|
|
|
IO.Hidden.api_key_comfy_org,
|
|
|
|
|
|
IO.Hidden.unique_id,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
],
|
|
|
|
|
|
is_api_node=True,
|
2026-01-14 02:18:28 +02:00
|
|
|
|
price_badge=PRICE_BADGE_VIDEO,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
model: str,
|
|
|
|
|
|
prompt: str,
|
2025-12-08 11:33:46 +02:00
|
|
|
|
first_frame: Input.Image,
|
|
|
|
|
|
last_frame: Input.Image,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
resolution: str,
|
|
|
|
|
|
aspect_ratio: str,
|
|
|
|
|
|
duration: int,
|
|
|
|
|
|
seed: int,
|
|
|
|
|
|
camera_fixed: bool,
|
|
|
|
|
|
watermark: bool,
|
2026-01-16 08:09:07 +02:00
|
|
|
|
generate_audio: bool = False,
|
2025-10-14 09:55:56 +03:00
|
|
|
|
) -> IO.NodeOutput:
|
2026-01-16 08:09:07 +02:00
|
|
|
|
if model == "seedance-1-5-pro-251215" and duration < 4:
|
|
|
|
|
|
raise ValueError("Minimum supported duration for Seedance 1.5 Pro is 4 seconds.")
|
2025-09-09 21:40:29 +03:00
|
|
|
|
validate_string(prompt, strip_whitespace=True, min_length=1)
|
|
|
|
|
|
raise_if_text_params(prompt, ["resolution", "ratio", "duration", "seed", "camerafixed", "watermark"])
|
|
|
|
|
|
for i in (first_frame, last_frame):
|
|
|
|
|
|
validate_image_dimensions(i, min_width=300, min_height=300, max_width=6000, max_height=6000)
|
2025-10-30 08:49:03 +02:00
|
|
|
|
validate_image_aspect_ratio(i, (2, 5), (5, 2), strict=False) # 0.4 to 2.5
|
2025-09-09 21:40:29 +03:00
|
|
|
|
|
|
|
|
|
|
download_urls = await upload_images_to_comfyapi(
|
2025-10-24 08:37:16 +03:00
|
|
|
|
cls,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
image_tensor_pair_to_batch(first_frame, last_frame),
|
|
|
|
|
|
max_images=2,
|
|
|
|
|
|
mime_type="image/png",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
prompt = (
|
|
|
|
|
|
f"{prompt} "
|
|
|
|
|
|
f"--resolution {resolution} "
|
|
|
|
|
|
f"--ratio {aspect_ratio} "
|
|
|
|
|
|
f"--duration {duration} "
|
|
|
|
|
|
f"--seed {seed} "
|
|
|
|
|
|
f"--camerafixed {str(camera_fixed).lower()} "
|
|
|
|
|
|
f"--watermark {str(watermark).lower()}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return await process_video_task(
|
2025-10-24 08:37:16 +03:00
|
|
|
|
cls,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
payload=Image2VideoTaskCreationRequest(
|
|
|
|
|
|
model=model,
|
|
|
|
|
|
content=[
|
|
|
|
|
|
TaskTextContent(text=prompt),
|
|
|
|
|
|
TaskImageContent(image_url=TaskImageContentUrl(url=str(download_urls[0])), role="first_frame"),
|
|
|
|
|
|
TaskImageContent(image_url=TaskImageContentUrl(url=str(download_urls[1])), role="last_frame"),
|
|
|
|
|
|
],
|
2026-01-16 08:09:07 +02:00
|
|
|
|
generate_audio=generate_audio if model == "seedance-1-5-pro-251215" else None,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
),
|
|
|
|
|
|
estimated_duration=max(1, math.ceil(VIDEO_TASKS_EXECUTION_TIME[model][resolution] * (duration / 10.0))),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-10-14 09:55:56 +03:00
|
|
|
|
class ByteDanceImageReferenceNode(IO.ComfyNode):
|
2025-09-09 21:40:29 +03:00
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls):
|
2025-10-14 09:55:56 +03:00
|
|
|
|
return IO.Schema(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
node_id="ByteDanceImageReferenceNode",
|
|
|
|
|
|
display_name="ByteDance Reference Images to Video",
|
2026-06-01 21:24:46 -07:00
|
|
|
|
category="partner/video/ByteDance",
|
2025-09-09 21:40:29 +03:00
|
|
|
|
description="Generate video using prompt and reference images.",
|
|
|
|
|
|
inputs=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Combo.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"model",
|
2025-12-08 11:33:46 +02:00
|
|
|
|
options=["seedance-1-0-pro-250528", "seedance-1-0-lite-i2v-250428"],
|
|
|
|
|
|
default="seedance-1-0-lite-i2v-250428",
|
2025-09-09 21:40:29 +03:00
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.String.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
tooltip="The text prompt used to generate the video.",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Image.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"images",
|
|
|
|
|
|
tooltip="One to four images.",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Combo.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"resolution",
|
|
|
|
|
|
options=["480p", "720p"],
|
|
|
|
|
|
tooltip="The resolution of the output video.",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Combo.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"aspect_ratio",
|
|
|
|
|
|
options=["adaptive", "16:9", "4:3", "1:1", "3:4", "9:16", "21:9"],
|
|
|
|
|
|
tooltip="The aspect ratio of the output video.",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Int.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"duration",
|
|
|
|
|
|
default=5,
|
|
|
|
|
|
min=3,
|
|
|
|
|
|
max=12,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
tooltip="The duration of the output video in seconds.",
|
2025-10-14 09:55:56 +03:00
|
|
|
|
display_mode=IO.NumberDisplay.slider,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Int.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"seed",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
max=2147483647,
|
|
|
|
|
|
step=1,
|
2025-10-14 09:55:56 +03:00
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
control_after_generate=True,
|
|
|
|
|
|
tooltip="Seed to use for generation.",
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Boolean.Input(
|
2025-09-09 21:40:29 +03:00
|
|
|
|
"watermark",
|
2025-12-20 08:24:37 +02:00
|
|
|
|
default=False,
|
2025-10-24 08:37:16 +03:00
|
|
|
|
tooltip='Whether to add an "AI generated" watermark to the video.',
|
2025-09-09 21:40:29 +03:00
|
|
|
|
optional=True,
|
feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI
Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.
Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification
Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype
Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity
Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: remove advanced=True from DynamicCombo.Input (unsupported)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: address review - un-mark model merge, video, image, and training node widgets as advanced
Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)
Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352
* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)
Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1
---------
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
|
|
|
|
advanced=True,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
outputs=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Video.Output(),
|
2025-09-09 21:40:29 +03:00
|
|
|
|
],
|
|
|
|
|
|
hidden=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Hidden.auth_token_comfy_org,
|
|
|
|
|
|
IO.Hidden.api_key_comfy_org,
|
|
|
|
|
|
IO.Hidden.unique_id,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
],
|
|
|
|
|
|
is_api_node=True,
|
2026-01-16 08:09:07 +02:00
|
|
|
|
price_badge=IO.PriceBadge(
|
|
|
|
|
|
depends_on=IO.PriceBadgeDepends(widgets=["model", "duration", "resolution"]),
|
|
|
|
|
|
expr="""
|
|
|
|
|
|
(
|
|
|
|
|
|
$priceByModel := {
|
|
|
|
|
|
"seedance-1-0-pro": {
|
|
|
|
|
|
"480p":[0.23,0.24],
|
|
|
|
|
|
"720p":[0.51,0.56]
|
|
|
|
|
|
},
|
|
|
|
|
|
"seedance-1-0-lite": {
|
|
|
|
|
|
"480p":[0.17,0.18],
|
|
|
|
|
|
"720p":[0.37,0.41]
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
$model := widgets.model;
|
|
|
|
|
|
$modelKey :=
|
|
|
|
|
|
$contains($model, "seedance-1-0-pro") ? "seedance-1-0-pro" :
|
|
|
|
|
|
"seedance-1-0-lite";
|
|
|
|
|
|
$resolution := widgets.resolution;
|
|
|
|
|
|
$resKey :=
|
|
|
|
|
|
$contains($resolution, "720") ? "720p" :
|
|
|
|
|
|
"480p";
|
|
|
|
|
|
$modelPrices := $lookup($priceByModel, $modelKey);
|
|
|
|
|
|
$baseRange := $lookup($modelPrices, $resKey);
|
|
|
|
|
|
$min10s := $baseRange[0];
|
|
|
|
|
|
$max10s := $baseRange[1];
|
|
|
|
|
|
$scale := widgets.duration / 10;
|
|
|
|
|
|
$minCost := $min10s * $scale;
|
|
|
|
|
|
$maxCost := $max10s * $scale;
|
|
|
|
|
|
($minCost = $maxCost)
|
|
|
|
|
|
? {"type":"usd","usd": $minCost}
|
|
|
|
|
|
: {"type":"range_usd","min_usd": $minCost, "max_usd": $maxCost}
|
|
|
|
|
|
)
|
|
|
|
|
|
""",
|
|
|
|
|
|
),
|
2025-09-09 21:40:29 +03:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
model: str,
|
|
|
|
|
|
prompt: str,
|
2025-12-08 11:33:46 +02:00
|
|
|
|
images: Input.Image,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
resolution: str,
|
|
|
|
|
|
aspect_ratio: str,
|
|
|
|
|
|
duration: int,
|
|
|
|
|
|
seed: int,
|
|
|
|
|
|
watermark: bool,
|
2025-10-14 09:55:56 +03:00
|
|
|
|
) -> IO.NodeOutput:
|
2025-09-09 21:40:29 +03:00
|
|
|
|
validate_string(prompt, strip_whitespace=True, min_length=1)
|
|
|
|
|
|
raise_if_text_params(prompt, ["resolution", "ratio", "duration", "seed", "watermark"])
|
|
|
|
|
|
for image in images:
|
|
|
|
|
|
validate_image_dimensions(image, min_width=300, min_height=300, max_width=6000, max_height=6000)
|
2025-10-30 08:49:03 +02:00
|
|
|
|
validate_image_aspect_ratio(image, (2, 5), (5, 2), strict=False) # 0.4 to 2.5
|
2025-09-09 21:40:29 +03:00
|
|
|
|
|
2025-10-24 08:37:16 +03:00
|
|
|
|
image_urls = await upload_images_to_comfyapi(cls, images, max_images=4, mime_type="image/png")
|
2025-09-09 21:40:29 +03:00
|
|
|
|
prompt = (
|
|
|
|
|
|
f"{prompt} "
|
|
|
|
|
|
f"--resolution {resolution} "
|
|
|
|
|
|
f"--ratio {aspect_ratio} "
|
|
|
|
|
|
f"--duration {duration} "
|
|
|
|
|
|
f"--seed {seed} "
|
|
|
|
|
|
f"--watermark {str(watermark).lower()}"
|
|
|
|
|
|
)
|
|
|
|
|
|
x = [
|
|
|
|
|
|
TaskTextContent(text=prompt),
|
2025-10-24 08:37:16 +03:00
|
|
|
|
*[TaskImageContent(image_url=TaskImageContentUrl(url=str(i)), role="reference_image") for i in image_urls],
|
2025-09-09 21:40:29 +03:00
|
|
|
|
]
|
|
|
|
|
|
return await process_video_task(
|
2025-10-24 08:37:16 +03:00
|
|
|
|
cls,
|
2026-03-20 05:05:01 +02:00
|
|
|
|
payload=Image2VideoTaskCreationRequest(model=model, content=x, generate_audio=None),
|
2025-09-09 21:40:29 +03:00
|
|
|
|
estimated_duration=max(1, math.ceil(VIDEO_TASKS_EXECUTION_TIME[model][resolution] * (duration / 10.0))),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def raise_if_text_params(prompt: str, text_params: list[str]) -> None:
|
|
|
|
|
|
for i in text_params:
|
|
|
|
|
|
if f"--{i} " in prompt:
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
f"--{i} is not allowed in the prompt, use the appropriated widget input to change this value."
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-14 02:18:28 +02:00
|
|
|
|
PRICE_BADGE_VIDEO = IO.PriceBadge(
|
2026-01-16 08:09:07 +02:00
|
|
|
|
depends_on=IO.PriceBadgeDepends(widgets=["model", "duration", "resolution", "generate_audio"]),
|
2026-01-14 02:18:28 +02:00
|
|
|
|
expr="""
|
|
|
|
|
|
(
|
|
|
|
|
|
$priceByModel := {
|
2026-01-16 08:09:07 +02:00
|
|
|
|
"seedance-1-5-pro": {
|
|
|
|
|
|
"480p":[0.12,0.12],
|
|
|
|
|
|
"720p":[0.26,0.26],
|
|
|
|
|
|
"1080p":[0.58,0.59]
|
|
|
|
|
|
},
|
2026-01-14 02:18:28 +02:00
|
|
|
|
"seedance-1-0-pro": {
|
|
|
|
|
|
"480p":[0.23,0.24],
|
|
|
|
|
|
"720p":[0.51,0.56],
|
|
|
|
|
|
"1080p":[1.18,1.22]
|
|
|
|
|
|
},
|
|
|
|
|
|
"seedance-1-0-pro-fast": {
|
|
|
|
|
|
"480p":[0.09,0.1],
|
|
|
|
|
|
"720p":[0.21,0.23],
|
|
|
|
|
|
"1080p":[0.47,0.49]
|
|
|
|
|
|
},
|
|
|
|
|
|
"seedance-1-0-lite": {
|
|
|
|
|
|
"480p":[0.17,0.18],
|
|
|
|
|
|
"720p":[0.37,0.41],
|
|
|
|
|
|
"1080p":[0.85,0.88]
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
$model := widgets.model;
|
|
|
|
|
|
$modelKey :=
|
2026-01-16 08:09:07 +02:00
|
|
|
|
$contains($model, "seedance-1-5-pro") ? "seedance-1-5-pro" :
|
2026-01-14 02:18:28 +02:00
|
|
|
|
$contains($model, "seedance-1-0-pro-fast") ? "seedance-1-0-pro-fast" :
|
|
|
|
|
|
$contains($model, "seedance-1-0-pro") ? "seedance-1-0-pro" :
|
|
|
|
|
|
"seedance-1-0-lite";
|
|
|
|
|
|
$resolution := widgets.resolution;
|
|
|
|
|
|
$resKey :=
|
|
|
|
|
|
$contains($resolution, "1080") ? "1080p" :
|
|
|
|
|
|
$contains($resolution, "720") ? "720p" :
|
|
|
|
|
|
"480p";
|
|
|
|
|
|
$modelPrices := $lookup($priceByModel, $modelKey);
|
|
|
|
|
|
$baseRange := $lookup($modelPrices, $resKey);
|
|
|
|
|
|
$min10s := $baseRange[0];
|
|
|
|
|
|
$max10s := $baseRange[1];
|
|
|
|
|
|
$scale := widgets.duration / 10;
|
2026-01-16 08:09:07 +02:00
|
|
|
|
$audioMultiplier := ($modelKey = "seedance-1-5-pro" and widgets.generate_audio) ? 2 : 1;
|
|
|
|
|
|
$minCost := $min10s * $scale * $audioMultiplier;
|
|
|
|
|
|
$maxCost := $max10s * $scale * $audioMultiplier;
|
2026-01-14 02:18:28 +02:00
|
|
|
|
($minCost = $maxCost)
|
2026-01-16 08:09:07 +02:00
|
|
|
|
? {"type":"usd","usd": $minCost, "format": { "approximate": true }}
|
|
|
|
|
|
: {"type":"range_usd","min_usd": $minCost, "max_usd": $maxCost, "format": { "approximate": true }}
|
2026-01-14 02:18:28 +02:00
|
|
|
|
)
|
|
|
|
|
|
""",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-08 09:39:13 +03:00
|
|
|
|
def _seedance2_text_inputs(resolutions: list[str], default_ratio: str = "16:9"):
|
2026-04-13 08:26:19 +03:00
|
|
|
|
return [
|
|
|
|
|
|
IO.String.Input(
|
|
|
|
|
|
"prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip="Text prompt for video generation.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"resolution",
|
2026-04-17 04:32:04 +03:00
|
|
|
|
options=resolutions,
|
2026-04-13 08:26:19 +03:00
|
|
|
|
tooltip="Resolution of the output video.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"ratio",
|
|
|
|
|
|
options=["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"],
|
2026-05-08 09:39:13 +03:00
|
|
|
|
default=default_ratio,
|
2026-04-13 08:26:19 +03:00
|
|
|
|
tooltip="Aspect ratio of the output video.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"duration",
|
|
|
|
|
|
default=7,
|
|
|
|
|
|
min=4,
|
|
|
|
|
|
max=15,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
tooltip="Duration of the output video in seconds (4-15).",
|
|
|
|
|
|
display_mode=IO.NumberDisplay.slider,
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"generate_audio",
|
|
|
|
|
|
default=True,
|
|
|
|
|
|
tooltip="Enable audio generation for the output video.",
|
|
|
|
|
|
),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-07 21:28:09 +03:00
|
|
|
|
def _seedance25_text_inputs(with_ratio: bool = True, with_video_editing: bool = False):
|
|
|
|
|
|
return [
|
|
|
|
|
|
IO.String.Input(
|
|
|
|
|
|
"prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip="Text prompt for video generation. Put spoken lines in double quotes to steer "
|
|
|
|
|
|
"the generated dialogue.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"resolution",
|
|
|
|
|
|
options=["480p", "720p"],
|
|
|
|
|
|
default="720p",
|
|
|
|
|
|
tooltip="Resolution of the output video.",
|
|
|
|
|
|
),
|
|
|
|
|
|
*(
|
|
|
|
|
|
[
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"ratio",
|
|
|
|
|
|
options=["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"],
|
|
|
|
|
|
default="16:9",
|
|
|
|
|
|
tooltip="Aspect ratio of the output video.",
|
|
|
|
|
|
)
|
|
|
|
|
|
]
|
|
|
|
|
|
if with_ratio
|
|
|
|
|
|
else []
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"duration",
|
|
|
|
|
|
default=5,
|
|
|
|
|
|
min=4,
|
|
|
|
|
|
max=30,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
tooltip="Duration of the output video in seconds (4-30).",
|
|
|
|
|
|
display_mode=IO.NumberDisplay.slider,
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"generate_audio",
|
|
|
|
|
|
default=True,
|
|
|
|
|
|
tooltip="Enable audio generation for the output video.",
|
|
|
|
|
|
),
|
|
|
|
|
|
*(
|
|
|
|
|
|
[
|
|
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"video_editing",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
tooltip="Enable when the prompt edits a connected reference video, for example "
|
|
|
|
|
|
"replacing an object in it. The output then keeps the source clip's own length "
|
|
|
|
|
|
"and aspect ratio, and the duration and ratio widgets are ignored. Leave "
|
|
|
|
|
|
"disabled to generate a new video, or to extend one to the duration you set.",
|
|
|
|
|
|
)
|
|
|
|
|
|
]
|
|
|
|
|
|
if with_video_editing
|
|
|
|
|
|
else []
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"output_format",
|
|
|
|
|
|
options=["mp4"],
|
|
|
|
|
|
default="mp4",
|
|
|
|
|
|
tooltip="Container format of the output video.",
|
|
|
|
|
|
),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _seedance25_reference_inputs():
|
|
|
|
|
|
return [
|
|
|
|
|
|
*_seedance25_text_inputs(with_video_editing=True),
|
|
|
|
|
|
IO.Autogrow.Input(
|
|
|
|
|
|
"reference_images",
|
|
|
|
|
|
template=IO.Autogrow.TemplateNames(
|
|
|
|
|
|
IO.Image.Input("reference_image"),
|
|
|
|
|
|
names=[f"image_{i}" for i in range(1, 31)],
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Autogrow.Input(
|
|
|
|
|
|
"reference_videos",
|
|
|
|
|
|
template=IO.Autogrow.TemplateNames(
|
|
|
|
|
|
IO.Video.Input("reference_video"),
|
|
|
|
|
|
names=[f"video_{i}" for i in range(1, 11)],
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Autogrow.Input(
|
|
|
|
|
|
"reference_audios",
|
|
|
|
|
|
template=IO.Autogrow.TemplateNames(
|
|
|
|
|
|
IO.Audio.Input("reference_audio"),
|
|
|
|
|
|
names=[f"audio_{i}" for i in range(1, 11)],
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"auto_downscale",
|
|
|
|
|
|
default=True,
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
tooltip="Automatically downscale reference videos that exceed the model's pixel budget "
|
|
|
|
|
|
"for the selected resolution. Aspect ratio is preserved; videos already within limits are untouched.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"auto_upscale",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
advanced=True,
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
tooltip="Automatically upscale reference videos that are below the model's minimum pixel count "
|
|
|
|
|
|
"for the selected resolution. Aspect ratio is preserved; videos already meeting the minimum are "
|
|
|
|
|
|
"untouched. Note: upscaling a low-resolution source does not add real detail and may produce "
|
|
|
|
|
|
"lower-quality generations.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Autogrow.Input(
|
|
|
|
|
|
"reference_assets",
|
|
|
|
|
|
template=IO.Autogrow.TemplateNames(
|
|
|
|
|
|
IO.String.Input("reference_asset"),
|
|
|
|
|
|
names=[f"asset_{i}" for i in range(1, 31)],
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _seedance2_build_request(
|
|
|
|
|
|
model: dict,
|
|
|
|
|
|
model_id: str,
|
|
|
|
|
|
content: list,
|
|
|
|
|
|
seed: int,
|
|
|
|
|
|
watermark: bool,
|
|
|
|
|
|
ratio: str,
|
|
|
|
|
|
) -> Seedance2TaskCreationRequest:
|
|
|
|
|
|
video_editing = bool(model.get("video_editing"))
|
|
|
|
|
|
return Seedance2TaskCreationRequest(
|
|
|
|
|
|
model=model_id,
|
|
|
|
|
|
content=content,
|
|
|
|
|
|
generate_audio=model["generate_audio"],
|
|
|
|
|
|
resolution=model["resolution"],
|
|
|
|
|
|
ratio="adaptive" if video_editing else ratio,
|
|
|
|
|
|
duration=-1 if video_editing else model["duration"],
|
|
|
|
|
|
seed=seed,
|
|
|
|
|
|
watermark=watermark,
|
|
|
|
|
|
output_format=model.get("output_format"),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_SEEDANCE2_PRICE_EXPR_TEMPLATE = """
|
|
|
|
|
|
(
|
|
|
|
|
|
$m := widgets.model;
|
|
|
|
|
|
$res := $lookup(widgets, "model.resolution");
|
|
|
|
|
|
$ratio := $lookup(widgets, "model.ratio");
|
|
|
|
|
|
$dur := $lookup(widgets, "model.duration");
|
|
|
|
|
|
$auto := $lookup(widgets, "model.video_editing") = true;
|
|
|
|
|
|
$hasVideo := __HAS_VIDEO__;
|
|
|
|
|
|
$ready := $type($m) = "string" and $type($res) = "string" and ($auto or $type($dur) = "number");
|
|
|
|
|
|
$ready ? (
|
|
|
|
|
|
$contains($m, "2.5") ? (
|
|
|
|
|
|
$is480 := $res = "480p";
|
|
|
|
|
|
$perFrame := $ratio = "1:1" ? ($is480 ? 400 : 900) :
|
|
|
|
|
|
$ratio = "4:3" ? ($is480 ? 411.25 : 905.6719) :
|
|
|
|
|
|
$ratio = "3:4" ? ($is480 ? 411.25 : 905.6719) :
|
|
|
|
|
|
$ratio = "21:9" ? ($is480 ? 418.5 : 904.3945) :
|
|
|
|
|
|
($is480 ? 400.3125 : 900);
|
|
|
|
|
|
$price := $hasVideo ? 0.009152 : 0.015301;
|
|
|
|
|
|
$costFor := function($d) { $floor($perFrame * (24 * $d + 1)) / 1000 * $price };
|
|
|
|
|
|
$lo := $costFor($auto ? 4 : $dur);
|
|
|
|
|
|
$hi := $costFor(($auto ? 30 : $dur) + ($hasVideo ? 30 : 0));
|
|
|
|
|
|
$lo = $hi
|
|
|
|
|
|
? {"type": "usd", "usd": $lo, "format": {"approximate": true}}
|
|
|
|
|
|
: {"type": "range_usd", "min_usd": $lo, "max_usd": $hi, "format": {"approximate": true}}
|
|
|
|
|
|
) : (
|
|
|
|
|
|
$rate := $res = "4k" ? 195200 :
|
|
|
|
|
|
$res = "1080p" ? 48800 :
|
|
|
|
|
|
$res = "720p" ? 21600 : 10044;
|
|
|
|
|
|
$noVideoPrice := $res = "4k" ? 0.00572 :
|
|
|
|
|
|
$res = "1080p" ? 0.011011 :
|
|
|
|
|
|
$contains($m, "mini") ? 0.005005 :
|
|
|
|
|
|
$contains($m, "fast") ? 0.008008 : 0.01001;
|
|
|
|
|
|
$videoPrice := $res = "4k" ? 0.003432 :
|
|
|
|
|
|
$res = "1080p" ? 0.006721 :
|
|
|
|
|
|
$contains($m, "mini") ? 0.003003 :
|
|
|
|
|
|
$contains($m, "fast") ? 0.004719 : 0.006149;
|
|
|
|
|
|
$hasVideo
|
|
|
|
|
|
? {"type": "range_usd",
|
|
|
|
|
|
"min_usd": $ceil($dur * 5 / 3) * $rate * $videoPrice / 1000,
|
|
|
|
|
|
"max_usd": (15 + $dur) * $rate * $videoPrice / 1000,
|
|
|
|
|
|
"format": {"approximate": true}}
|
|
|
|
|
|
: {"type": "usd", "usd": $dur * $rate * $noVideoPrice / 1000,
|
|
|
|
|
|
"format": {"approximate": true}}
|
|
|
|
|
|
)
|
|
|
|
|
|
) : undefined
|
|
|
|
|
|
)
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_SEEDANCE_AUDIO_POLICY_CODE = "OutputAudioSensitiveContentDetected.PolicyViolation"
|
|
|
|
|
|
_SEEDANCE_TASK_TYPE_CONSTRAINT_CODE = "InvalidParameter.TaskTypeConstraint"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _seedance2_poll_video_task(
|
|
|
|
|
|
cls: type[IO.ComfyNode],
|
|
|
|
|
|
task_id: str,
|
|
|
|
|
|
model_id: str,
|
|
|
|
|
|
resolution: str,
|
|
|
|
|
|
has_video_input: bool,
|
|
|
|
|
|
) -> TaskStatusResponse:
|
|
|
|
|
|
try:
|
|
|
|
|
|
return await poll_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path=f"{BYTEPLUS_SEEDANCE2_TASK_STATUS_ENDPOINT}/{task_id}"),
|
|
|
|
|
|
response_model=TaskStatusResponse,
|
|
|
|
|
|
status_extractor=lambda r: r.status,
|
|
|
|
|
|
price_extractor=_seedance2_price_extractor(
|
|
|
|
|
|
model_id, has_video_input=has_video_input, resolution=resolution
|
|
|
|
|
|
),
|
|
|
|
|
|
poll_interval=9,
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
if _SEEDANCE_AUDIO_POLICY_CODE in str(exc):
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
"The provider rejected the audio track this model generated for the video "
|
|
|
|
|
|
"(possible copyright match). The video itself was fine. Turn off generate_audio "
|
|
|
|
|
|
"to get a silent video, or adjust the prompt and try again."
|
|
|
|
|
|
) from exc
|
|
|
|
|
|
if _SEEDANCE_TASK_TYPE_CONSTRAINT_CODE in str(exc):
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
"Seedance read this prompt as editing the reference video, and an edit always "
|
|
|
|
|
|
"takes its duration and aspect ratio from that video. Enable video_editing on "
|
|
|
|
|
|
"this node and run again, or reword the prompt so it describes a new video "
|
|
|
|
|
|
"rather than a change to the reference one."
|
|
|
|
|
|
) from exc
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _seedance2_price_badge(with_reference_videos: bool) -> IO.PriceBadge:
|
|
|
|
|
|
widgets = ["model", "model.resolution", "model.ratio", "model.duration"]
|
|
|
|
|
|
if with_reference_videos:
|
|
|
|
|
|
widgets.append("model.video_editing")
|
|
|
|
|
|
has_video = (
|
|
|
|
|
|
'$exists(inputGroups) and $lookup(inputGroups, "model.reference_videos") > 0'
|
|
|
|
|
|
if with_reference_videos
|
|
|
|
|
|
else "false"
|
|
|
|
|
|
)
|
|
|
|
|
|
return IO.PriceBadge(
|
|
|
|
|
|
depends_on=IO.PriceBadgeDepends(
|
|
|
|
|
|
widgets=widgets,
|
|
|
|
|
|
input_groups=["model.reference_videos"] if with_reference_videos else [],
|
|
|
|
|
|
),
|
|
|
|
|
|
expr=_SEEDANCE2_PRICE_EXPR_TEMPLATE.replace("__HAS_VIDEO__", has_video),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-13 08:26:19 +03:00
|
|
|
|
class ByteDance2TextToVideoNode(IO.ComfyNode):
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls):
|
|
|
|
|
|
return IO.Schema(
|
|
|
|
|
|
node_id="ByteDance2TextToVideoNode",
|
2026-08-07 21:28:09 +03:00
|
|
|
|
display_name="ByteDance Seedance 2.5 Text to Video",
|
2026-06-01 21:24:46 -07:00
|
|
|
|
category="partner/video/ByteDance",
|
2026-08-07 21:28:09 +03:00
|
|
|
|
description="Generate video using Seedance 2.5 or 2.0 models based on a text prompt.",
|
2026-04-13 08:26:19 +03:00
|
|
|
|
inputs=[
|
|
|
|
|
|
IO.DynamicCombo.Input(
|
|
|
|
|
|
"model",
|
|
|
|
|
|
options=[
|
2026-08-07 21:28:09 +03:00
|
|
|
|
IO.DynamicCombo.Option("Seedance 2.5", _seedance25_text_inputs()),
|
2026-06-24 17:57:46 +03:00
|
|
|
|
IO.DynamicCombo.Option("Seedance 2.0", _seedance2_text_inputs(["480p", "720p", "1080p", "4k"])),
|
2026-04-17 04:32:04 +03:00
|
|
|
|
IO.DynamicCombo.Option("Seedance 2.0 Fast", _seedance2_text_inputs(["480p", "720p"])),
|
2026-06-25 17:57:04 +03:00
|
|
|
|
IO.DynamicCombo.Option("Seedance 2.0 Mini", _seedance2_text_inputs(["480p", "720p"])),
|
2026-04-13 08:26:19 +03:00
|
|
|
|
],
|
2026-08-07 21:28:09 +03:00
|
|
|
|
tooltip=SEEDANCE_MODEL_TOOLTIP,
|
2026-04-13 08:26:19 +03:00
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"seed",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
max=2147483647,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
|
|
|
|
|
control_after_generate=True,
|
|
|
|
|
|
tooltip="Seed controls whether the node should re-run; "
|
|
|
|
|
|
"results are non-deterministic regardless of seed.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"watermark",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
tooltip="Whether to add a watermark to the video.",
|
|
|
|
|
|
advanced=True,
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
outputs=[
|
|
|
|
|
|
IO.Video.Output(),
|
|
|
|
|
|
],
|
|
|
|
|
|
hidden=[
|
|
|
|
|
|
IO.Hidden.auth_token_comfy_org,
|
|
|
|
|
|
IO.Hidden.api_key_comfy_org,
|
|
|
|
|
|
IO.Hidden.unique_id,
|
|
|
|
|
|
],
|
|
|
|
|
|
is_api_node=True,
|
2026-08-07 21:28:09 +03:00
|
|
|
|
price_badge=_seedance2_price_badge(with_reference_videos=False),
|
2026-04-13 08:26:19 +03:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
model: dict,
|
|
|
|
|
|
seed: int,
|
|
|
|
|
|
watermark: bool,
|
|
|
|
|
|
) -> IO.NodeOutput:
|
|
|
|
|
|
validate_string(model["prompt"], strip_whitespace=True, min_length=1)
|
|
|
|
|
|
model_id = SEEDANCE_MODELS[model["model"]]
|
|
|
|
|
|
initial_response = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path=BYTEPLUS_TASK_ENDPOINT, method="POST"),
|
2026-08-07 21:28:09 +03:00
|
|
|
|
data=_seedance2_build_request(
|
|
|
|
|
|
model,
|
|
|
|
|
|
model_id,
|
|
|
|
|
|
[TaskTextContent(text=model["prompt"])],
|
|
|
|
|
|
seed,
|
|
|
|
|
|
watermark,
|
2026-04-13 08:26:19 +03:00
|
|
|
|
ratio=model["ratio"],
|
|
|
|
|
|
),
|
|
|
|
|
|
response_model=TaskCreationResponse,
|
|
|
|
|
|
)
|
2026-08-07 21:28:09 +03:00
|
|
|
|
response = await _seedance2_poll_video_task(
|
|
|
|
|
|
cls, initial_response.id, model_id, model["resolution"], has_video_input=False
|
2026-04-13 08:26:19 +03:00
|
|
|
|
)
|
|
|
|
|
|
return IO.NodeOutput(await download_url_to_video_output(response.content.video_url))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ByteDance2FirstLastFrameNode(IO.ComfyNode):
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls):
|
|
|
|
|
|
return IO.Schema(
|
|
|
|
|
|
node_id="ByteDance2FirstLastFrameNode",
|
2026-08-07 21:28:09 +03:00
|
|
|
|
display_name="ByteDance Seedance 2.5 First-Last-Frame to Video",
|
2026-06-01 21:24:46 -07:00
|
|
|
|
category="partner/video/ByteDance",
|
2026-08-07 21:28:09 +03:00
|
|
|
|
description="Generate video using Seedance 2.5 or 2.0 from a first frame image "
|
|
|
|
|
|
"and optional last frame image.",
|
2026-04-13 08:26:19 +03:00
|
|
|
|
inputs=[
|
|
|
|
|
|
IO.DynamicCombo.Input(
|
|
|
|
|
|
"model",
|
|
|
|
|
|
options=[
|
2026-08-07 21:28:09 +03:00
|
|
|
|
IO.DynamicCombo.Option("Seedance 2.5", _seedance25_text_inputs(with_ratio=False)),
|
2026-05-08 09:39:13 +03:00
|
|
|
|
IO.DynamicCombo.Option(
|
|
|
|
|
|
"Seedance 2.0",
|
2026-06-24 17:57:46 +03:00
|
|
|
|
_seedance2_text_inputs(["480p", "720p", "1080p", "4k"], default_ratio="adaptive"),
|
2026-05-08 09:39:13 +03:00
|
|
|
|
),
|
|
|
|
|
|
IO.DynamicCombo.Option(
|
|
|
|
|
|
"Seedance 2.0 Fast",
|
|
|
|
|
|
_seedance2_text_inputs(["480p", "720p"], default_ratio="adaptive"),
|
|
|
|
|
|
),
|
2026-06-25 17:57:04 +03:00
|
|
|
|
IO.DynamicCombo.Option(
|
|
|
|
|
|
"Seedance 2.0 Mini",
|
|
|
|
|
|
_seedance2_text_inputs(["480p", "720p"], default_ratio="adaptive"),
|
|
|
|
|
|
),
|
2026-04-13 08:26:19 +03:00
|
|
|
|
],
|
2026-08-07 21:28:09 +03:00
|
|
|
|
tooltip=SEEDANCE_MODEL_TOOLTIP,
|
2026-04-13 08:26:19 +03:00
|
|
|
|
),
|
|
|
|
|
|
IO.Image.Input(
|
|
|
|
|
|
"first_frame",
|
|
|
|
|
|
tooltip="First frame image for the video.",
|
2026-04-23 03:59:55 +03:00
|
|
|
|
optional=True,
|
2026-04-13 08:26:19 +03:00
|
|
|
|
),
|
|
|
|
|
|
IO.Image.Input(
|
|
|
|
|
|
"last_frame",
|
|
|
|
|
|
tooltip="Last frame image for the video.",
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2026-04-23 03:59:55 +03:00
|
|
|
|
IO.String.Input(
|
|
|
|
|
|
"first_frame_asset_id",
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip="Seedance asset_id to use as the first frame. "
|
2026-05-21 21:58:03 +03:00
|
|
|
|
"Mutually exclusive with the first_frame image input.",
|
2026-04-23 03:59:55 +03:00
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.String.Input(
|
|
|
|
|
|
"last_frame_asset_id",
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip="Seedance asset_id to use as the last frame. "
|
2026-05-21 21:58:03 +03:00
|
|
|
|
"Mutually exclusive with the last_frame image input.",
|
2026-04-23 03:59:55 +03:00
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2026-04-13 08:26:19 +03:00
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"seed",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
max=2147483647,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
|
|
|
|
|
control_after_generate=True,
|
|
|
|
|
|
tooltip="Seed controls whether the node should re-run; "
|
|
|
|
|
|
"results are non-deterministic regardless of seed.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"watermark",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
tooltip="Whether to add a watermark to the video.",
|
|
|
|
|
|
advanced=True,
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
outputs=[
|
|
|
|
|
|
IO.Video.Output(),
|
|
|
|
|
|
],
|
|
|
|
|
|
hidden=[
|
|
|
|
|
|
IO.Hidden.auth_token_comfy_org,
|
|
|
|
|
|
IO.Hidden.api_key_comfy_org,
|
|
|
|
|
|
IO.Hidden.unique_id,
|
|
|
|
|
|
],
|
|
|
|
|
|
is_api_node=True,
|
2026-08-07 21:28:09 +03:00
|
|
|
|
price_badge=_seedance2_price_badge(with_reference_videos=False),
|
2026-04-13 08:26:19 +03:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
model: dict,
|
|
|
|
|
|
seed: int,
|
|
|
|
|
|
watermark: bool,
|
2026-04-23 03:59:55 +03:00
|
|
|
|
first_frame: Input.Image | None = None,
|
2026-04-13 08:26:19 +03:00
|
|
|
|
last_frame: Input.Image | None = None,
|
2026-04-23 03:59:55 +03:00
|
|
|
|
first_frame_asset_id: str = "",
|
|
|
|
|
|
last_frame_asset_id: str = "",
|
2026-04-13 08:26:19 +03:00
|
|
|
|
) -> IO.NodeOutput:
|
|
|
|
|
|
validate_string(model["prompt"], strip_whitespace=True, min_length=1)
|
|
|
|
|
|
model_id = SEEDANCE_MODELS[model["model"]]
|
|
|
|
|
|
|
2026-04-23 03:59:55 +03:00
|
|
|
|
first_frame_asset_id = first_frame_asset_id.strip()
|
|
|
|
|
|
last_frame_asset_id = last_frame_asset_id.strip()
|
|
|
|
|
|
|
|
|
|
|
|
if first_frame is not None and first_frame_asset_id:
|
|
|
|
|
|
raise ValueError("Provide only one of first_frame or first_frame_asset_id, not both.")
|
|
|
|
|
|
if first_frame is None and not first_frame_asset_id:
|
|
|
|
|
|
raise ValueError("Either first_frame or first_frame_asset_id is required.")
|
|
|
|
|
|
if last_frame is not None and last_frame_asset_id:
|
|
|
|
|
|
raise ValueError("Provide only one of last_frame or last_frame_asset_id, not both.")
|
|
|
|
|
|
|
2026-08-07 21:28:09 +03:00
|
|
|
|
if model_id == "dreamina-seedance-2-5-260628":
|
|
|
|
|
|
# 2.5 accepts ratio="adaptive" only here and keeps the first frame's own aspect
|
|
|
|
|
|
# (a 1920x1088 frame yields 850x482, not a grid ratio), so pre-sizing the frames to a
|
|
|
|
|
|
# supported pixel pair would crop framing the model would otherwise have preserved.
|
|
|
|
|
|
request_ratio = "adaptive"
|
|
|
|
|
|
if first_frame is not None:
|
|
|
|
|
|
first_frame = _prepare_seedance_image(first_frame)
|
|
|
|
|
|
if last_frame is not None:
|
|
|
|
|
|
last_frame = _prepare_seedance_image(last_frame)
|
|
|
|
|
|
elif first_frame_asset_id or last_frame_asset_id:
|
|
|
|
|
|
request_ratio = model["ratio"]
|
2026-06-04 11:23:52 +03:00
|
|
|
|
if first_frame is not None:
|
|
|
|
|
|
first_frame = _prepare_seedance_image(first_frame)
|
|
|
|
|
|
if last_frame is not None:
|
|
|
|
|
|
last_frame = _prepare_seedance_image(last_frame)
|
|
|
|
|
|
else:
|
|
|
|
|
|
# The 1080p FLF stretch fix (pre-size frames to a supported pixel pair + submit ratio="adaptive")
|
|
|
|
|
|
# only applies to local image inputs we can resize.
|
|
|
|
|
|
request_ratio = "adaptive"
|
|
|
|
|
|
target_dims: tuple[int, int] | None = None
|
|
|
|
|
|
if first_frame is not None:
|
|
|
|
|
|
validate_image_aspect_ratio(first_frame, (2, 5), (5, 2), strict=False) # 0.4 to 2.5
|
|
|
|
|
|
validate_image_dimensions(first_frame, min_width=300, min_height=300)
|
|
|
|
|
|
target_dims = _seedance2_target_dims(model["resolution"], model["ratio"], first_frame)
|
|
|
|
|
|
first_frame = _resize_to_exact(first_frame, *target_dims)
|
|
|
|
|
|
if last_frame is not None:
|
|
|
|
|
|
validate_image_aspect_ratio(last_frame, (2, 5), (5, 2), strict=False) # 0.4 to 2.5
|
|
|
|
|
|
validate_image_dimensions(last_frame, min_width=300, min_height=300)
|
|
|
|
|
|
if target_dims is None:
|
|
|
|
|
|
target_dims = _seedance2_target_dims(model["resolution"], model["ratio"], last_frame)
|
|
|
|
|
|
last_frame = _resize_to_exact(last_frame, *target_dims)
|
2026-05-28 11:03:28 +03:00
|
|
|
|
|
2026-04-23 03:59:55 +03:00
|
|
|
|
asset_ids_to_resolve = [a for a in (first_frame_asset_id, last_frame_asset_id) if a]
|
|
|
|
|
|
image_assets: dict[str, str] = {}
|
|
|
|
|
|
if asset_ids_to_resolve:
|
|
|
|
|
|
image_assets, _, _ = await _resolve_reference_assets(cls, asset_ids_to_resolve)
|
|
|
|
|
|
for aid in asset_ids_to_resolve:
|
|
|
|
|
|
if aid not in image_assets:
|
|
|
|
|
|
raise ValueError(f"Asset {aid} is not an Image asset.")
|
|
|
|
|
|
|
|
|
|
|
|
if first_frame_asset_id:
|
|
|
|
|
|
first_frame_url = image_assets[first_frame_asset_id]
|
|
|
|
|
|
else:
|
2026-04-30 21:49:08 +03:00
|
|
|
|
first_frame_url = await _seedance_virtual_library_upload_image_asset(
|
|
|
|
|
|
cls, first_frame, wait_label="Uploading first frame."
|
|
|
|
|
|
)
|
2026-04-23 03:59:55 +03:00
|
|
|
|
|
2026-04-13 08:26:19 +03:00
|
|
|
|
content: list[TaskTextContent | TaskImageContent] = [
|
|
|
|
|
|
TaskTextContent(text=model["prompt"]),
|
|
|
|
|
|
TaskImageContent(
|
2026-04-23 03:59:55 +03:00
|
|
|
|
image_url=TaskImageContentUrl(url=first_frame_url),
|
2026-04-13 08:26:19 +03:00
|
|
|
|
role="first_frame",
|
|
|
|
|
|
),
|
|
|
|
|
|
]
|
2026-04-23 03:59:55 +03:00
|
|
|
|
if last_frame_asset_id:
|
|
|
|
|
|
content.append(
|
|
|
|
|
|
TaskImageContent(
|
|
|
|
|
|
image_url=TaskImageContentUrl(url=image_assets[last_frame_asset_id]),
|
|
|
|
|
|
role="last_frame",
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
elif last_frame is not None:
|
2026-04-13 08:26:19 +03:00
|
|
|
|
content.append(
|
|
|
|
|
|
TaskImageContent(
|
|
|
|
|
|
image_url=TaskImageContentUrl(
|
2026-04-30 21:49:08 +03:00
|
|
|
|
url=await _seedance_virtual_library_upload_image_asset(
|
|
|
|
|
|
cls, last_frame, wait_label="Uploading last frame."
|
|
|
|
|
|
)
|
2026-04-13 08:26:19 +03:00
|
|
|
|
),
|
|
|
|
|
|
role="last_frame",
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
initial_response = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path=BYTEPLUS_TASK_ENDPOINT, method="POST"),
|
2026-08-07 21:28:09 +03:00
|
|
|
|
data=_seedance2_build_request(model, model_id, content, seed, watermark, ratio=request_ratio),
|
2026-04-13 08:26:19 +03:00
|
|
|
|
response_model=TaskCreationResponse,
|
|
|
|
|
|
)
|
2026-08-07 21:28:09 +03:00
|
|
|
|
response = await _seedance2_poll_video_task(
|
|
|
|
|
|
cls, initial_response.id, model_id, model["resolution"], has_video_input=False
|
2026-04-13 08:26:19 +03:00
|
|
|
|
)
|
|
|
|
|
|
return IO.NodeOutput(await download_url_to_video_output(response.content.video_url))
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-08 09:39:13 +03:00
|
|
|
|
def _seedance2_reference_inputs(resolutions: list[str], default_ratio: str = "16:9"):
|
2026-04-13 08:26:19 +03:00
|
|
|
|
return [
|
2026-05-08 09:39:13 +03:00
|
|
|
|
*_seedance2_text_inputs(resolutions, default_ratio=default_ratio),
|
2026-04-13 08:26:19 +03:00
|
|
|
|
IO.Autogrow.Input(
|
|
|
|
|
|
"reference_images",
|
|
|
|
|
|
template=IO.Autogrow.TemplateNames(
|
|
|
|
|
|
IO.Image.Input("reference_image"),
|
|
|
|
|
|
names=[
|
|
|
|
|
|
"image_1",
|
|
|
|
|
|
"image_2",
|
|
|
|
|
|
"image_3",
|
|
|
|
|
|
"image_4",
|
|
|
|
|
|
"image_5",
|
|
|
|
|
|
"image_6",
|
|
|
|
|
|
"image_7",
|
|
|
|
|
|
"image_8",
|
|
|
|
|
|
"image_9",
|
|
|
|
|
|
],
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Autogrow.Input(
|
|
|
|
|
|
"reference_videos",
|
|
|
|
|
|
template=IO.Autogrow.TemplateNames(
|
|
|
|
|
|
IO.Video.Input("reference_video"),
|
|
|
|
|
|
names=["video_1", "video_2", "video_3"],
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Autogrow.Input(
|
|
|
|
|
|
"reference_audios",
|
|
|
|
|
|
template=IO.Autogrow.TemplateNames(
|
|
|
|
|
|
IO.Audio.Input("reference_audio"),
|
|
|
|
|
|
names=["audio_1", "audio_2", "audio_3"],
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-04-21 20:45:10 +03:00
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"auto_downscale",
|
2026-05-28 11:03:28 +03:00
|
|
|
|
default=True,
|
2026-04-21 20:45:10 +03:00
|
|
|
|
optional=True,
|
|
|
|
|
|
tooltip="Automatically downscale reference videos that exceed the model's pixel budget "
|
|
|
|
|
|
"for the selected resolution. Aspect ratio is preserved; videos already within limits are untouched.",
|
|
|
|
|
|
),
|
2026-05-21 21:58:03 +03:00
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"auto_upscale",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
advanced=True,
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
tooltip="Automatically upscale reference videos that are below the model's minimum pixel count "
|
|
|
|
|
|
"for the selected resolution. Aspect ratio is preserved; videos already meeting the minimum are "
|
|
|
|
|
|
"untouched. Note: upscaling a low-resolution source does not add real detail and may produce "
|
|
|
|
|
|
"lower-quality generations.",
|
|
|
|
|
|
),
|
2026-04-23 03:59:55 +03:00
|
|
|
|
IO.Autogrow.Input(
|
|
|
|
|
|
"reference_assets",
|
|
|
|
|
|
template=IO.Autogrow.TemplateNames(
|
|
|
|
|
|
IO.String.Input("reference_asset"),
|
|
|
|
|
|
names=[
|
|
|
|
|
|
"asset_1",
|
|
|
|
|
|
"asset_2",
|
|
|
|
|
|
"asset_3",
|
|
|
|
|
|
"asset_4",
|
|
|
|
|
|
"asset_5",
|
|
|
|
|
|
"asset_6",
|
|
|
|
|
|
"asset_7",
|
|
|
|
|
|
"asset_8",
|
|
|
|
|
|
"asset_9",
|
|
|
|
|
|
],
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-04-13 08:26:19 +03:00
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ByteDance2ReferenceNode(IO.ComfyNode):
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls):
|
|
|
|
|
|
return IO.Schema(
|
|
|
|
|
|
node_id="ByteDance2ReferenceNode",
|
2026-08-07 21:28:09 +03:00
|
|
|
|
display_name="ByteDance Seedance 2.5 Reference to Video",
|
2026-06-01 21:24:46 -07:00
|
|
|
|
category="partner/video/ByteDance",
|
2026-08-07 21:28:09 +03:00
|
|
|
|
description="Generate, edit, or extend video using Seedance 2.5 or 2.0 with reference "
|
|
|
|
|
|
"images, videos, and audio. Supports multimodal reference, video editing, and video extension.",
|
2026-04-13 08:26:19 +03:00
|
|
|
|
inputs=[
|
|
|
|
|
|
IO.DynamicCombo.Input(
|
|
|
|
|
|
"model",
|
|
|
|
|
|
options=[
|
2026-08-07 21:28:09 +03:00
|
|
|
|
IO.DynamicCombo.Option("Seedance 2.5", _seedance25_reference_inputs()),
|
2026-05-08 09:39:13 +03:00
|
|
|
|
IO.DynamicCombo.Option(
|
|
|
|
|
|
"Seedance 2.0",
|
2026-06-24 17:57:46 +03:00
|
|
|
|
_seedance2_reference_inputs(["480p", "720p", "1080p", "4k"], default_ratio="adaptive"),
|
2026-05-08 09:39:13 +03:00
|
|
|
|
),
|
|
|
|
|
|
IO.DynamicCombo.Option(
|
|
|
|
|
|
"Seedance 2.0 Fast",
|
|
|
|
|
|
_seedance2_reference_inputs(["480p", "720p"], default_ratio="adaptive"),
|
|
|
|
|
|
),
|
2026-06-25 17:57:04 +03:00
|
|
|
|
IO.DynamicCombo.Option(
|
|
|
|
|
|
"Seedance 2.0 Mini",
|
|
|
|
|
|
_seedance2_reference_inputs(["480p", "720p"], default_ratio="adaptive"),
|
|
|
|
|
|
),
|
2026-04-13 08:26:19 +03:00
|
|
|
|
],
|
2026-08-07 21:28:09 +03:00
|
|
|
|
tooltip=SEEDANCE_MODEL_TOOLTIP,
|
2026-04-13 08:26:19 +03:00
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"seed",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
max=2147483647,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
|
|
|
|
|
control_after_generate=True,
|
|
|
|
|
|
tooltip="Seed controls whether the node should re-run; "
|
|
|
|
|
|
"results are non-deterministic regardless of seed.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"watermark",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
tooltip="Whether to add a watermark to the video.",
|
|
|
|
|
|
advanced=True,
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
outputs=[
|
|
|
|
|
|
IO.Video.Output(),
|
|
|
|
|
|
],
|
|
|
|
|
|
hidden=[
|
|
|
|
|
|
IO.Hidden.auth_token_comfy_org,
|
|
|
|
|
|
IO.Hidden.api_key_comfy_org,
|
|
|
|
|
|
IO.Hidden.unique_id,
|
|
|
|
|
|
],
|
|
|
|
|
|
is_api_node=True,
|
2026-08-07 21:28:09 +03:00
|
|
|
|
price_badge=_seedance2_price_badge(with_reference_videos=True),
|
2026-04-13 08:26:19 +03:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
model: dict,
|
|
|
|
|
|
seed: int,
|
|
|
|
|
|
watermark: bool,
|
|
|
|
|
|
) -> IO.NodeOutput:
|
|
|
|
|
|
validate_string(model["prompt"], strip_whitespace=True, min_length=1)
|
|
|
|
|
|
|
|
|
|
|
|
reference_images = model.get("reference_images", {})
|
|
|
|
|
|
reference_videos = model.get("reference_videos", {})
|
|
|
|
|
|
reference_audios = model.get("reference_audios", {})
|
2026-04-23 03:59:55 +03:00
|
|
|
|
reference_assets = model.get("reference_assets", {})
|
2026-04-13 08:26:19 +03:00
|
|
|
|
|
2026-04-23 03:59:55 +03:00
|
|
|
|
reference_image_assets, reference_video_assets, reference_audio_assets = await _resolve_reference_assets(
|
|
|
|
|
|
cls, list(reference_assets.values())
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-08-07 21:28:09 +03:00
|
|
|
|
model_id = SEEDANCE_MODELS[model["model"]]
|
|
|
|
|
|
limits = seedance2_reference_limits(model_id)
|
|
|
|
|
|
|
2026-04-23 03:59:55 +03:00
|
|
|
|
if not reference_images and not reference_videos and not reference_image_assets and not reference_video_assets:
|
2026-08-07 21:28:09 +03:00
|
|
|
|
if model_id != "dreamina-seedance-2-5-260628" or not (reference_audios or reference_audio_assets):
|
|
|
|
|
|
raise ValueError("At least one reference image or video or asset is required.")
|
2026-04-23 03:59:55 +03:00
|
|
|
|
|
|
|
|
|
|
total_images = len(reference_images) + len(reference_image_assets)
|
2026-08-07 21:28:09 +03:00
|
|
|
|
if total_images > limits["max_images"]:
|
2026-04-23 03:59:55 +03:00
|
|
|
|
raise ValueError(
|
|
|
|
|
|
f"Too many reference images: {total_images} "
|
2026-08-07 21:28:09 +03:00
|
|
|
|
f"(images={len(reference_images)}, image assets={len(reference_image_assets)}). "
|
|
|
|
|
|
f"Maximum is {limits['max_images']}."
|
2026-04-23 03:59:55 +03:00
|
|
|
|
)
|
|
|
|
|
|
total_videos = len(reference_videos) + len(reference_video_assets)
|
2026-08-07 21:28:09 +03:00
|
|
|
|
if total_videos > limits["max_videos"]:
|
2026-04-23 03:59:55 +03:00
|
|
|
|
raise ValueError(
|
|
|
|
|
|
f"Too many reference videos: {total_videos} "
|
2026-08-07 21:28:09 +03:00
|
|
|
|
f"(videos={len(reference_videos)}, video assets={len(reference_video_assets)}). "
|
|
|
|
|
|
f"Maximum is {limits['max_videos']}."
|
2026-04-23 03:59:55 +03:00
|
|
|
|
)
|
|
|
|
|
|
total_audios = len(reference_audios) + len(reference_audio_assets)
|
2026-08-07 21:28:09 +03:00
|
|
|
|
if total_audios > limits["max_audios"]:
|
2026-04-23 03:59:55 +03:00
|
|
|
|
raise ValueError(
|
|
|
|
|
|
f"Too many reference audios: {total_audios} "
|
2026-08-07 21:28:09 +03:00
|
|
|
|
f"(audios={len(reference_audios)}, audio assets={len(reference_audio_assets)}). "
|
|
|
|
|
|
f"Maximum is {limits['max_audios']}."
|
2026-04-23 03:59:55 +03:00
|
|
|
|
)
|
2026-04-13 08:26:19 +03:00
|
|
|
|
|
2026-05-28 11:03:28 +03:00
|
|
|
|
for key in reference_images:
|
|
|
|
|
|
reference_images[key] = _prepare_seedance_image(reference_images[key])
|
|
|
|
|
|
|
2026-04-23 03:59:55 +03:00
|
|
|
|
has_video_input = total_videos > 0
|
2026-04-21 20:45:10 +03:00
|
|
|
|
|
|
|
|
|
|
if model.get("auto_downscale") and reference_videos:
|
2026-04-23 03:59:55 +03:00
|
|
|
|
max_px = SEEDANCE2_REF_VIDEO_PIXEL_LIMITS.get(model_id, {}).get(model["resolution"], {}).get("max")
|
2026-04-21 20:45:10 +03:00
|
|
|
|
if max_px:
|
|
|
|
|
|
for key in reference_videos:
|
2026-05-21 21:58:03 +03:00
|
|
|
|
reference_videos[key] = downscale_video_to_max_pixels(reference_videos[key], max_px)
|
|
|
|
|
|
|
|
|
|
|
|
if model.get("auto_upscale") and reference_videos:
|
|
|
|
|
|
min_px = SEEDANCE2_REF_VIDEO_PIXEL_LIMITS.get(model_id, {}).get(model["resolution"], {}).get("min")
|
|
|
|
|
|
if min_px:
|
|
|
|
|
|
for key in reference_videos:
|
|
|
|
|
|
reference_videos[key] = upscale_video_to_min_pixels(reference_videos[key], min_px)
|
2026-04-21 20:45:10 +03:00
|
|
|
|
|
2026-04-13 08:26:19 +03:00
|
|
|
|
total_video_duration = 0.0
|
|
|
|
|
|
for i, key in enumerate(reference_videos, 1):
|
|
|
|
|
|
video = reference_videos[key]
|
2026-04-21 20:45:10 +03:00
|
|
|
|
_validate_ref_video_pixels(video, model_id, model["resolution"], i)
|
2026-04-13 08:26:19 +03:00
|
|
|
|
try:
|
|
|
|
|
|
dur = video.get_duration()
|
|
|
|
|
|
if dur < 1.8:
|
|
|
|
|
|
raise ValueError(f"Reference video {i} is too short: {dur:.1f}s. Minimum duration is 1.8 seconds.")
|
|
|
|
|
|
total_video_duration += dur
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
raise
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
2026-08-07 21:28:09 +03:00
|
|
|
|
if total_video_duration > limits["max_total_seconds"]:
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
f"Total reference video duration is {total_video_duration:.1f}s. "
|
|
|
|
|
|
f"Maximum is {limits['max_total_seconds']} seconds."
|
|
|
|
|
|
)
|
2026-04-13 08:26:19 +03:00
|
|
|
|
|
|
|
|
|
|
total_audio_duration = 0.0
|
|
|
|
|
|
for i, key in enumerate(reference_audios, 1):
|
|
|
|
|
|
audio = reference_audios[key]
|
|
|
|
|
|
dur = int(audio["waveform"].shape[-1]) / int(audio["sample_rate"])
|
|
|
|
|
|
if dur < 1.8:
|
|
|
|
|
|
raise ValueError(f"Reference audio {i} is too short: {dur:.1f}s. Minimum duration is 1.8 seconds.")
|
|
|
|
|
|
total_audio_duration += dur
|
2026-08-07 21:28:09 +03:00
|
|
|
|
if total_audio_duration > limits["max_total_seconds"]:
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
f"Total reference audio duration is {total_audio_duration:.1f}s. "
|
|
|
|
|
|
f"Maximum is {limits['max_total_seconds']} seconds."
|
|
|
|
|
|
)
|
2026-04-13 08:26:19 +03:00
|
|
|
|
|
2026-04-23 03:59:55 +03:00
|
|
|
|
asset_labels = _build_asset_labels(
|
|
|
|
|
|
reference_assets,
|
|
|
|
|
|
reference_image_assets,
|
|
|
|
|
|
reference_video_assets,
|
|
|
|
|
|
reference_audio_assets,
|
|
|
|
|
|
len(reference_images),
|
|
|
|
|
|
len(reference_videos),
|
|
|
|
|
|
len(reference_audios),
|
|
|
|
|
|
)
|
|
|
|
|
|
prompt_text = _rewrite_asset_refs(model["prompt"], asset_labels)
|
|
|
|
|
|
|
2026-04-13 08:26:19 +03:00
|
|
|
|
content: list[TaskTextContent | TaskImageContent | TaskVideoContent | TaskAudioContent] = [
|
2026-04-23 03:59:55 +03:00
|
|
|
|
TaskTextContent(text=prompt_text),
|
2026-04-13 08:26:19 +03:00
|
|
|
|
]
|
|
|
|
|
|
for i, key in enumerate(reference_images, 1):
|
|
|
|
|
|
content.append(
|
|
|
|
|
|
TaskImageContent(
|
|
|
|
|
|
image_url=TaskImageContentUrl(
|
2026-04-30 21:49:08 +03:00
|
|
|
|
url=await _seedance_virtual_library_upload_image_asset(
|
2026-04-13 08:26:19 +03:00
|
|
|
|
cls,
|
2026-04-30 21:49:08 +03:00
|
|
|
|
reference_images[key],
|
2026-04-13 08:26:19 +03:00
|
|
|
|
wait_label=f"Uploading image {i}",
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
role="reference_image",
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
for i, key in enumerate(reference_videos, 1):
|
|
|
|
|
|
content.append(
|
|
|
|
|
|
TaskVideoContent(
|
|
|
|
|
|
video_url=TaskVideoContentUrl(
|
2026-05-27 09:44:27 +03:00
|
|
|
|
url=await _seedance_virtual_library_upload_video_asset(
|
2026-04-13 08:26:19 +03:00
|
|
|
|
cls,
|
|
|
|
|
|
reference_videos[key],
|
|
|
|
|
|
wait_label=f"Uploading video {i}",
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
for key in reference_audios:
|
|
|
|
|
|
content.append(
|
|
|
|
|
|
TaskAudioContent(
|
|
|
|
|
|
audio_url=TaskAudioContentUrl(
|
|
|
|
|
|
url=await upload_audio_to_comfyapi(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
reference_audios[key],
|
|
|
|
|
|
container_format="mp3",
|
|
|
|
|
|
codec_name="libmp3lame",
|
|
|
|
|
|
mime_type="audio/mpeg",
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
2026-04-23 03:59:55 +03:00
|
|
|
|
for url in reference_image_assets.values():
|
|
|
|
|
|
content.append(
|
|
|
|
|
|
TaskImageContent(
|
|
|
|
|
|
image_url=TaskImageContentUrl(url=url),
|
|
|
|
|
|
role="reference_image",
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
for url in reference_video_assets.values():
|
|
|
|
|
|
content.append(
|
|
|
|
|
|
TaskVideoContent(video_url=TaskVideoContentUrl(url=url)),
|
|
|
|
|
|
)
|
|
|
|
|
|
for url in reference_audio_assets.values():
|
|
|
|
|
|
content.append(
|
|
|
|
|
|
TaskAudioContent(audio_url=TaskAudioContentUrl(url=url)),
|
|
|
|
|
|
)
|
2026-04-13 08:26:19 +03:00
|
|
|
|
initial_response = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path=BYTEPLUS_TASK_ENDPOINT, method="POST"),
|
2026-08-07 21:28:09 +03:00
|
|
|
|
data=_seedance2_build_request(model, model_id, content, seed, watermark, ratio=model["ratio"]),
|
2026-04-13 08:26:19 +03:00
|
|
|
|
response_model=TaskCreationResponse,
|
|
|
|
|
|
)
|
2026-08-07 21:28:09 +03:00
|
|
|
|
response = await _seedance2_poll_video_task(
|
|
|
|
|
|
cls, initial_response.id, model_id, model["resolution"], has_video_input=has_video_input
|
2026-04-13 08:26:19 +03:00
|
|
|
|
)
|
|
|
|
|
|
return IO.NodeOutput(await download_url_to_video_output(response.content.video_url))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def process_video_task(
|
|
|
|
|
|
cls: type[IO.ComfyNode],
|
|
|
|
|
|
payload: Text2VideoTaskCreationRequest | Image2VideoTaskCreationRequest,
|
|
|
|
|
|
estimated_duration: int | None,
|
|
|
|
|
|
) -> IO.NodeOutput:
|
|
|
|
|
|
if payload.model in DEPRECATED_MODELS:
|
|
|
|
|
|
logger.warning(
|
|
|
|
|
|
"Model '%s' is deprecated and will be deactivated on May 13, 2026. "
|
|
|
|
|
|
"Please switch to a newer model. Recommended: seedance-1-0-pro-fast-251015.",
|
|
|
|
|
|
payload.model,
|
|
|
|
|
|
)
|
|
|
|
|
|
initial_response = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path=BYTEPLUS_TASK_ENDPOINT, method="POST"),
|
|
|
|
|
|
data=payload,
|
|
|
|
|
|
response_model=TaskCreationResponse,
|
|
|
|
|
|
)
|
|
|
|
|
|
response = await poll_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path=f"{BYTEPLUS_TASK_STATUS_ENDPOINT}/{initial_response.id}"),
|
|
|
|
|
|
status_extractor=lambda r: r.status,
|
|
|
|
|
|
estimated_duration=estimated_duration,
|
|
|
|
|
|
response_model=TaskStatusResponse,
|
|
|
|
|
|
)
|
|
|
|
|
|
return IO.NodeOutput(await download_url_to_video_output(response.content.video_url))
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-23 03:59:55 +03:00
|
|
|
|
class ByteDanceCreateImageAsset(IO.ComfyNode):
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls) -> IO.Schema:
|
|
|
|
|
|
return IO.Schema(
|
|
|
|
|
|
node_id="ByteDanceCreateImageAsset",
|
|
|
|
|
|
display_name="ByteDance Create Image Asset",
|
2026-06-01 21:24:46 -07:00
|
|
|
|
category="partner/image/ByteDance",
|
2026-04-23 03:59:55 +03:00
|
|
|
|
description=(
|
|
|
|
|
|
"Create a Seedance 2.0 personal image asset. Uploads the input image and "
|
|
|
|
|
|
"registers it in the given asset group. If group_id is empty, runs a real-person "
|
|
|
|
|
|
"H5 authentication flow to create a new group before adding the asset."
|
|
|
|
|
|
),
|
|
|
|
|
|
inputs=[
|
|
|
|
|
|
IO.Image.Input("image", tooltip="Image to register as a personal asset."),
|
|
|
|
|
|
IO.String.Input(
|
|
|
|
|
|
"group_id",
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip="Reuse an existing Seedance asset group ID to skip repeated human verification for the "
|
|
|
|
|
|
"same person. Leave empty to run real-person authentication in the browser and create a new group.",
|
|
|
|
|
|
),
|
|
|
|
|
|
# IO.String.Input(
|
|
|
|
|
|
# "name",
|
|
|
|
|
|
# default="",
|
|
|
|
|
|
# tooltip="Asset name (up to 64 characters).",
|
|
|
|
|
|
# ),
|
|
|
|
|
|
],
|
|
|
|
|
|
outputs=[
|
|
|
|
|
|
IO.String.Output(display_name="asset_id"),
|
|
|
|
|
|
IO.String.Output(display_name="group_id"),
|
|
|
|
|
|
],
|
|
|
|
|
|
hidden=[
|
|
|
|
|
|
IO.Hidden.auth_token_comfy_org,
|
|
|
|
|
|
IO.Hidden.api_key_comfy_org,
|
|
|
|
|
|
IO.Hidden.unique_id,
|
|
|
|
|
|
],
|
|
|
|
|
|
# is_api_node=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
image: Input.Image,
|
|
|
|
|
|
group_id: str = "",
|
|
|
|
|
|
# name: str = "",
|
|
|
|
|
|
) -> IO.NodeOutput:
|
|
|
|
|
|
# if len(name) > 64:
|
|
|
|
|
|
# raise ValueError("Name of asset can not be greater then 64 symbols")
|
|
|
|
|
|
validate_image_dimensions(image, min_width=300, max_width=6000, min_height=300, max_height=6000)
|
|
|
|
|
|
validate_image_aspect_ratio(image, min_ratio=(0.4, 1), max_ratio=(2.5, 1))
|
|
|
|
|
|
resolved_group = await _resolve_group_id(cls, group_id)
|
|
|
|
|
|
asset_id = await _create_seedance_asset(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
group_id=resolved_group,
|
|
|
|
|
|
url=await upload_image_to_comfyapi(cls, image),
|
|
|
|
|
|
name="",
|
|
|
|
|
|
asset_type="Image",
|
|
|
|
|
|
)
|
|
|
|
|
|
await _wait_for_asset_active(cls, asset_id, resolved_group)
|
|
|
|
|
|
PromptServer.instance.send_progress_text(
|
|
|
|
|
|
f"Please save the asset_id and group_id for reuse.\n\nasset_id: {asset_id}\n\n"
|
|
|
|
|
|
f"group_id: {resolved_group}",
|
|
|
|
|
|
cls.hidden.unique_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
return IO.NodeOutput(asset_id, resolved_group)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ByteDanceCreateVideoAsset(IO.ComfyNode):
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls) -> IO.Schema:
|
|
|
|
|
|
return IO.Schema(
|
|
|
|
|
|
node_id="ByteDanceCreateVideoAsset",
|
|
|
|
|
|
display_name="ByteDance Create Video Asset",
|
2026-06-01 21:24:46 -07:00
|
|
|
|
category="partner/video/ByteDance",
|
2026-04-23 03:59:55 +03:00
|
|
|
|
description=(
|
|
|
|
|
|
"Create a Seedance 2.0 personal video asset. Uploads the input video and "
|
|
|
|
|
|
"registers it in the given asset group. If group_id is empty, runs a real-person "
|
|
|
|
|
|
"H5 authentication flow to create a new group before adding the asset."
|
|
|
|
|
|
),
|
|
|
|
|
|
inputs=[
|
|
|
|
|
|
IO.Video.Input("video", tooltip="Video to register as a personal asset."),
|
|
|
|
|
|
IO.String.Input(
|
|
|
|
|
|
"group_id",
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip="Reuse an existing Seedance asset group ID to skip repeated human verification for the "
|
|
|
|
|
|
"same person. Leave empty to run real-person authentication in the browser and create a new group.",
|
|
|
|
|
|
),
|
|
|
|
|
|
# IO.String.Input(
|
|
|
|
|
|
# "name",
|
|
|
|
|
|
# default="",
|
|
|
|
|
|
# tooltip="Asset name (up to 64 characters).",
|
|
|
|
|
|
# ),
|
|
|
|
|
|
],
|
|
|
|
|
|
outputs=[
|
|
|
|
|
|
IO.String.Output(display_name="asset_id"),
|
|
|
|
|
|
IO.String.Output(display_name="group_id"),
|
|
|
|
|
|
],
|
|
|
|
|
|
hidden=[
|
|
|
|
|
|
IO.Hidden.auth_token_comfy_org,
|
|
|
|
|
|
IO.Hidden.api_key_comfy_org,
|
|
|
|
|
|
IO.Hidden.unique_id,
|
|
|
|
|
|
],
|
|
|
|
|
|
# is_api_node=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
video: Input.Video,
|
|
|
|
|
|
group_id: str = "",
|
|
|
|
|
|
# name: str = "",
|
|
|
|
|
|
) -> IO.NodeOutput:
|
|
|
|
|
|
# if len(name) > 64:
|
|
|
|
|
|
# raise ValueError("Name of asset can not be greater then 64 symbols")
|
|
|
|
|
|
validate_video_duration(video, min_duration=2, max_duration=15)
|
|
|
|
|
|
validate_video_dimensions(video, min_width=300, max_width=6000, min_height=300, max_height=6000)
|
|
|
|
|
|
|
|
|
|
|
|
w, h = video.get_dimensions()
|
|
|
|
|
|
if h > 0:
|
|
|
|
|
|
ratio = w / h
|
|
|
|
|
|
if not (0.4 <= ratio <= 2.5):
|
|
|
|
|
|
raise ValueError(f"Asset video aspect ratio (W/H) must be in [0.4, 2.5], got {ratio:.3f} ({w}x{h}).")
|
|
|
|
|
|
pixels = w * h
|
|
|
|
|
|
if not (409_600 <= pixels <= 927_408):
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
f"Asset video total pixels (W×H) must be in [409600, 927408], " f"got {pixels:,} ({w}x{h})."
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
fps = float(video.get_frame_rate())
|
|
|
|
|
|
if not (24 <= fps <= 60):
|
|
|
|
|
|
raise ValueError(f"Asset video FPS must be in [24, 60], got {fps:.2f}.")
|
|
|
|
|
|
|
|
|
|
|
|
resolved_group = await _resolve_group_id(cls, group_id)
|
|
|
|
|
|
asset_id = await _create_seedance_asset(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
group_id=resolved_group,
|
|
|
|
|
|
url=await upload_video_to_comfyapi(cls, video),
|
|
|
|
|
|
name="",
|
|
|
|
|
|
asset_type="Video",
|
|
|
|
|
|
)
|
|
|
|
|
|
await _wait_for_asset_active(cls, asset_id, resolved_group)
|
|
|
|
|
|
PromptServer.instance.send_progress_text(
|
|
|
|
|
|
f"Please save the asset_id and group_id for reuse.\n\nasset_id: {asset_id}\n\n"
|
|
|
|
|
|
f"group_id: {resolved_group}",
|
|
|
|
|
|
cls.hidden.unique_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
return IO.NodeOutput(asset_id, resolved_group)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-03 14:00:10 +03:00
|
|
|
|
MODE_TEXT = "text only"
|
|
|
|
|
|
MODE_AUDIO = "audio reference"
|
|
|
|
|
|
MODE_IMAGE = "image reference"
|
|
|
|
|
|
MODE_SPEAKER = "preset voice"
|
|
|
|
|
|
|
|
|
|
|
|
# (speaker_id, display_label) for built-in TTS 2.0 voices; resolvable ids are account-scoped.
|
|
|
|
|
|
SEED_AUDIO_PRESET_VOICES: list[tuple[str, str]] = [
|
|
|
|
|
|
("zh_female_vv_uranus_bigtts", "Vivi (Female, multilingual)"),
|
|
|
|
|
|
("zh_female_xiaohe_uranus_bigtts", "Mindy (Female, multilingual)"),
|
|
|
|
|
|
("en_female_stokie_uranus_bigtts", "Stokie (Female, English)"),
|
|
|
|
|
|
("en_female_dacey_uranus_bigtts", "Dacey (Female, English)"),
|
|
|
|
|
|
("en_male_tim_uranus_bigtts", "Tim (Male, English)"),
|
|
|
|
|
|
("zh_male_m191_uranus_bigtts", "Kian (Male, multilingual)"),
|
|
|
|
|
|
("zh_male_taocheng_uranus_bigtts", "Cedric (Male, multilingual)"),
|
|
|
|
|
|
("zh_male_sophie_uranus_bigtts", "Sophie (Female, multilingual)"),
|
|
|
|
|
|
("zh_female_yingyujiaoxue_uranus_bigtts", "Jean (Female, multilingual)"),
|
|
|
|
|
|
("zh_male_dayi_uranus_bigtts", "Magnus (Male, multilingual)"),
|
|
|
|
|
|
("zh_female_mizai_uranus_bigtts", "Mabel (Female, multilingual)"),
|
|
|
|
|
|
("zh_female_jitangnv_uranus_bigtts", "Nadia (Female, multilingual)"),
|
|
|
|
|
|
("zh_female_meilinvyou_uranus_bigtts", "Opal (Female, multilingual)"),
|
|
|
|
|
|
("zh_female_liuchangnv_uranus_bigtts", "Pearl (Female, multilingual)"),
|
|
|
|
|
|
("zh_male_ruyayichen_uranus_bigtts", "Quentin (Male, multilingual)"),
|
|
|
|
|
|
("zh_female_vivo_uranus_bigtts", "Vienna (Female, multilingual)"),
|
|
|
|
|
|
("zh_female_xiaoai_uranus_bigtts", "Alina (Female, multilingual)"),
|
|
|
|
|
|
("zh_female_cancan_uranus_bigtts", "Corinne (Female, multilingual)"),
|
|
|
|
|
|
("zh_female_tianmeixiaoyuan_uranus_bigtts", "Esther (Female, multilingual)"),
|
|
|
|
|
|
("zh_female_tianmeitaozi_uranus_bigtts", "Freya (Female, multilingual)"),
|
|
|
|
|
|
("zh_female_shuangkuaisisi_uranus_bigtts", "Gigi (Female, multilingual)"),
|
|
|
|
|
|
("zh_female_peiqi_uranus_bigtts", "Holly (Female, multilingual)"),
|
|
|
|
|
|
("zh_female_xiaoxue_uranus_bigtts", "Lyla (Female, multilingual)"),
|
|
|
|
|
|
("zh_female_yuanqi_uranus_bigtts", "Daisy (Female, multilingual)"),
|
|
|
|
|
|
("zh_female_kefunvsheng_uranus_bigtts", "Tracy (Female, multilingual)"),
|
|
|
|
|
|
("zh_male_shaonianzixin_uranus_bigtts", "Jess (Male, multilingual)"),
|
|
|
|
|
|
("zh_female_linjianvhai_uranus_bigtts", "Pinky (Female, multilingual)"),
|
|
|
|
|
|
("zh_female_kiwi_uranus_bigtts", "Sweety (Female, multilingual)"),
|
|
|
|
|
|
("zh_female_sajiaoxuemei_uranus_bigtts", "Sandy (Female, multilingual)"),
|
|
|
|
|
|
("de_male_seven_uranus_bigtts", "Sven (Male, German)"),
|
|
|
|
|
|
("jp_female_minimi_uranus_bigtts", "Minimi (Female, Japanese)"),
|
|
|
|
|
|
("fr_male_usseau_uranus_bigtts", "Usseau (Male, French)"),
|
|
|
|
|
|
("es_male_felipe_uranus_bigtts", "Felipe (Male, Spanish)"),
|
|
|
|
|
|
("id_male_han_uranus_bigtts", "Han (Male, Indonesian)"),
|
|
|
|
|
|
("pt_male_martins_uranus_bigtts", "Martins (Male, Portuguese)"),
|
|
|
|
|
|
("it_male_enzo_uranus_bigtts", "Enzo (Male, Italian)"),
|
|
|
|
|
|
("kr_male_shane_uranus_bigtts", "Shane (Male, Korean)"),
|
|
|
|
|
|
("zh_male_liufei_uranus_bigtts", "Felix (Male, Chinese)"),
|
|
|
|
|
|
("zh_female_qingxinnvsheng_uranus_bigtts", "Celeste (Female, Chinese)"),
|
|
|
|
|
|
("zh_male_sunwukong_uranus_bigtts", "Monkey King (Male, Chinese)"),
|
|
|
|
|
|
]
|
|
|
|
|
|
SEED_AUDIO_VOICE_OPTIONS = [label for _, label in SEED_AUDIO_PRESET_VOICES]
|
|
|
|
|
|
SEED_AUDIO_VOICE_MAP = {label: speaker_id for speaker_id, label in SEED_AUDIO_PRESET_VOICES}
|
|
|
|
|
|
|
|
|
|
|
|
_AUDIO_TAG_RE = re.compile(r"@Audio(\d+)", re.IGNORECASE)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def max_audio_tag(prompt: str) -> int:
|
|
|
|
|
|
"""Highest N referenced as @AudioN in the prompt (0 if none)."""
|
|
|
|
|
|
nums = [int(m) for m in _AUDIO_TAG_RE.findall(prompt or "")]
|
|
|
|
|
|
return max(nums) if nums else 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def connected_audio_indices(reference_mode: dict) -> list[int]:
|
|
|
|
|
|
"""Indices (1-based) of connected reference_audio sockets, in order."""
|
|
|
|
|
|
return [
|
|
|
|
|
|
i
|
|
|
|
|
|
for i in range(1, 3 + 1)
|
|
|
|
|
|
if reference_mode.get(f"reference_audio_{i}") is not None
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def validate_seed_audio_inputs(
|
|
|
|
|
|
text_prompt: str,
|
|
|
|
|
|
mode: str,
|
|
|
|
|
|
audio_indices: list[int],
|
|
|
|
|
|
has_image: bool,
|
|
|
|
|
|
preset_voice: str | None = None,
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
validate_string(text_prompt, field_name="text_prompt", min_length=1, max_length=3000)
|
|
|
|
|
|
max_tag = max_audio_tag(text_prompt)
|
|
|
|
|
|
|
|
|
|
|
|
if mode == MODE_TEXT:
|
|
|
|
|
|
if max_tag:
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
f"The prompt references @Audio{max_tag}, but reference mode is '{MODE_TEXT}'. "
|
|
|
|
|
|
f"Switch to '{MODE_AUDIO}' and connect the reference clip(s)."
|
|
|
|
|
|
)
|
|
|
|
|
|
elif mode == MODE_AUDIO:
|
|
|
|
|
|
if not audio_indices:
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
f"Reference mode '{MODE_AUDIO}' requires at least one reference_audio input "
|
|
|
|
|
|
f"(or switch to '{MODE_TEXT}')."
|
|
|
|
|
|
)
|
|
|
|
|
|
if audio_indices != list(range(1, len(audio_indices) + 1)):
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
"Connect reference_audio inputs in order without gaps: reference_audio_1, then _2, then _3."
|
|
|
|
|
|
)
|
|
|
|
|
|
if max_tag > len(audio_indices):
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
f"The prompt references @Audio{max_tag}, but only {len(audio_indices)} "
|
|
|
|
|
|
f"reference audio(s) are connected."
|
|
|
|
|
|
)
|
|
|
|
|
|
elif mode == MODE_IMAGE:
|
|
|
|
|
|
if not has_image:
|
|
|
|
|
|
raise ValueError(f"Reference mode '{MODE_IMAGE}' requires a reference_image input.")
|
|
|
|
|
|
if max_tag:
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
f"@AudioN tags are not used in '{MODE_IMAGE}' mode; the prompt should contain "
|
|
|
|
|
|
f"only the text to synthesize."
|
|
|
|
|
|
)
|
|
|
|
|
|
elif mode == MODE_SPEAKER:
|
|
|
|
|
|
if not preset_voice or preset_voice not in SEED_AUDIO_VOICE_MAP:
|
|
|
|
|
|
raise ValueError(f"Reference mode '{MODE_SPEAKER}' requires selecting a preset voice.")
|
|
|
|
|
|
if max_tag > 1:
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
f"'{MODE_SPEAKER}' mode uses a single voice, so @Audio{max_tag} is out of range. "
|
|
|
|
|
|
f"Remove the @AudioN tags — the whole prompt is read in the selected voice."
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
raise ValueError(f"Unknown reference mode: {mode!r}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ByteDanceSeedAudioNode(IO.ComfyNode):
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls) -> IO.Schema:
|
|
|
|
|
|
return IO.Schema(
|
|
|
|
|
|
node_id="ByteDanceSeedAudio",
|
|
|
|
|
|
display_name="ByteDance Seed Audio 1.0",
|
2026-07-04 00:01:05 +03:00
|
|
|
|
category="partner/audio/ByteDance",
|
2026-07-03 14:00:10 +03:00
|
|
|
|
description=(
|
|
|
|
|
|
"Generate speech, music, sound effects and multi-speaker dialogue from a single prompt "
|
|
|
|
|
|
"with ByteDance Seed Audio 1.0. Describe the voice(s), emotion, ambience, background music "
|
|
|
|
|
|
"and sound effects in the prompt, and include the lines to speak. Optionally pick a built-in "
|
|
|
|
|
|
"preset voice, clone voices from up to 3 reference clips (tagged @Audio1-3 in the prompt), "
|
2026-07-23 18:32:46 +03:00
|
|
|
|
"or derive a voice from a character image. Up to 2 minutes of audio per run. "
|
|
|
|
|
|
"The multilingual model supports 20 languages and timestamp-based timing control."
|
2026-07-03 14:00:10 +03:00
|
|
|
|
),
|
|
|
|
|
|
inputs=[
|
|
|
|
|
|
IO.String.Input(
|
|
|
|
|
|
"text_prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip=(
|
|
|
|
|
|
"Describe the voice(s), emotion, pacing, ambience, background music and sound "
|
|
|
|
|
|
"effects, and include the lines to speak (name characters inline for dialogue). "
|
|
|
|
|
|
"In 'audio reference' mode, refer to connected clips by order as @Audio1, @Audio2, "
|
2026-07-23 18:32:46 +03:00
|
|
|
|
"@Audio3. With the multilingual model, a quoted line can start with a timestamp "
|
|
|
|
|
|
'range that controls when and how long it is spoken, e.g. "[5.5s:8.0s] Wait for me!". '
|
|
|
|
|
|
"Write the prompt in the same language as the lines to speak. Maximum 3000 characters."
|
2026-07-03 14:00:10 +03:00
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.DynamicCombo.Input(
|
|
|
|
|
|
"reference_mode",
|
|
|
|
|
|
options=[
|
|
|
|
|
|
IO.DynamicCombo.Option(MODE_TEXT, []),
|
|
|
|
|
|
IO.DynamicCombo.Option(
|
|
|
|
|
|
MODE_AUDIO,
|
|
|
|
|
|
[
|
|
|
|
|
|
IO.Audio.Input(
|
|
|
|
|
|
"reference_audio_1",
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
tooltip="Reference clip for voice cloning, tagged @Audio1 in the prompt. "
|
|
|
|
|
|
"Up to 30s.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Audio.Input(
|
|
|
|
|
|
"reference_audio_2",
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
tooltip="Reference clip tagged @Audio2 in the prompt. Up to 30s.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Audio.Input(
|
|
|
|
|
|
"reference_audio_3",
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
tooltip="Reference clip tagged @Audio3 in the prompt. Up to 30s.",
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.DynamicCombo.Option(
|
|
|
|
|
|
MODE_IMAGE,
|
|
|
|
|
|
[
|
|
|
|
|
|
IO.Image.Input(
|
|
|
|
|
|
"reference_image",
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
tooltip="A single character image; the model derives a voice from it. "
|
|
|
|
|
|
"Cannot be combined with reference audio.",
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.DynamicCombo.Option(
|
|
|
|
|
|
MODE_SPEAKER,
|
|
|
|
|
|
[
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"preset_voice",
|
|
|
|
|
|
options=SEED_AUDIO_VOICE_OPTIONS,
|
|
|
|
|
|
default=SEED_AUDIO_VOICE_OPTIONS[0],
|
|
|
|
|
|
tooltip="A built-in TTS 2.0 voice that reads the prompt. No reference "
|
|
|
|
|
|
"clip needed, and @AudioN tags are not used in this mode.",
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
tooltip=(
|
|
|
|
|
|
"How to condition the voice: 'text only' (describe everything in the prompt), "
|
|
|
|
|
|
"'audio reference' (clone up to 3 voices, tagged @Audio1-3), 'image reference' "
|
|
|
|
|
|
"(derive a voice from one character image), or 'preset voice' (pick a built-in "
|
|
|
|
|
|
"named voice that reads the prompt)."
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"sample_rate",
|
|
|
|
|
|
options=["8000", "16000", "24000", "32000", "44100", "48000"],
|
|
|
|
|
|
default="24000",
|
|
|
|
|
|
tooltip="Output sample rate in Hz.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"speech_rate",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=-50,
|
|
|
|
|
|
max=100,
|
|
|
|
|
|
tooltip="Speaking speed. 0 = normal, 100 = 2.0x, -50 = 0.5x.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"loudness_rate",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=-50,
|
|
|
|
|
|
max=100,
|
|
|
|
|
|
tooltip="Loudness. 0 = normal, 100 = 2.0x, -50 = 0.5x.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"pitch_rate",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=-12,
|
|
|
|
|
|
max=12,
|
|
|
|
|
|
tooltip="Pitch shift in semitones (-12 to 12).",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"seed",
|
|
|
|
|
|
default=42,
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
max=2147483647,
|
|
|
|
|
|
control_after_generate=True,
|
|
|
|
|
|
tooltip="Seed controls whether the node should re-run; "
|
|
|
|
|
|
"results are non-deterministic regardless of seed.",
|
|
|
|
|
|
),
|
2026-07-23 18:32:46 +03:00
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"model",
|
|
|
|
|
|
options=["seed-audio-1.0-multilingual", "seed-audio-1.0"],
|
|
|
|
|
|
default="seed-audio-1.0-multilingual",
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
tooltip=(
|
|
|
|
|
|
"seed-audio-1.0-multilingual: 20 languages (English, Chinese, Japanese, Korean, "
|
|
|
|
|
|
"Mexican & Castilian Spanish, Indonesian, German, Brazilian Portuguese, French, "
|
|
|
|
|
|
"Thai, Vietnamese, Malay, Filipino, Italian, Russian, Dutch, Polish, Turkish, "
|
|
|
|
|
|
'Swedish) plus per-sentence timing control via "[5.5s:8.0s] ..." timestamps. '
|
|
|
|
|
|
"seed-audio-1.0: English and Chinese only, no timing control."
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-07-03 14:00:10 +03:00
|
|
|
|
],
|
|
|
|
|
|
outputs=[IO.Audio.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(
|
|
|
|
|
|
expr="""{"type":"usd","usd": 0.2145, "format":{"suffix":"/minute","approximate":true}}""",
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
text_prompt: str,
|
|
|
|
|
|
reference_mode: dict,
|
|
|
|
|
|
sample_rate: str,
|
|
|
|
|
|
speech_rate: int,
|
|
|
|
|
|
loudness_rate: int,
|
|
|
|
|
|
pitch_rate: int,
|
|
|
|
|
|
seed: int,
|
2026-07-23 18:32:46 +03:00
|
|
|
|
model: str = "seed-audio-1.0-multilingual",
|
2026-07-03 14:00:10 +03:00
|
|
|
|
) -> IO.NodeOutput:
|
|
|
|
|
|
mode = reference_mode["reference_mode"]
|
|
|
|
|
|
audio_indices = connected_audio_indices(reference_mode)
|
|
|
|
|
|
image = reference_mode.get("reference_image")
|
|
|
|
|
|
preset_voice = reference_mode.get("preset_voice")
|
|
|
|
|
|
validate_seed_audio_inputs(text_prompt, mode, audio_indices, image is not None, preset_voice)
|
|
|
|
|
|
|
|
|
|
|
|
references: list[SeedAudioReference] | None = None
|
|
|
|
|
|
if mode == MODE_AUDIO:
|
|
|
|
|
|
references = []
|
|
|
|
|
|
for i in audio_indices:
|
|
|
|
|
|
clip = reference_mode[f"reference_audio_{i}"]
|
|
|
|
|
|
validate_audio_duration(clip, max_duration=30.0)
|
|
|
|
|
|
mp3_bytes = audio_input_to_mp3(clip).getvalue()
|
|
|
|
|
|
references.append(SeedAudioReference(audio_data=base64.b64encode(mp3_bytes).decode("utf-8")))
|
|
|
|
|
|
elif mode == MODE_IMAGE:
|
|
|
|
|
|
image = upscale_image_tensor_to_min_pixels(image, 160_000)
|
|
|
|
|
|
references = [SeedAudioReference(image_data=tensor_to_base64_string(image, mime_type="image/png"))]
|
|
|
|
|
|
elif mode == MODE_SPEAKER:
|
|
|
|
|
|
references = [SeedAudioReference(speaker=SEED_AUDIO_VOICE_MAP[preset_voice])]
|
|
|
|
|
|
|
|
|
|
|
|
response = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path="/proxy/byteplus/api/v3/tts/create", method="POST"),
|
|
|
|
|
|
response_model=SeedAudioResponse,
|
|
|
|
|
|
data=SeedAudioRequest(
|
2026-07-23 18:32:46 +03:00
|
|
|
|
model=model,
|
2026-07-03 14:00:10 +03:00
|
|
|
|
text_prompt=text_prompt,
|
|
|
|
|
|
references=references,
|
|
|
|
|
|
audio_config=SeedAudioConfig(
|
|
|
|
|
|
sample_rate=int(sample_rate),
|
|
|
|
|
|
speech_rate=speech_rate,
|
|
|
|
|
|
loudness_rate=loudness_rate,
|
|
|
|
|
|
pitch_rate=pitch_rate,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
if not response.audio:
|
|
|
|
|
|
raise Exception(
|
|
|
|
|
|
f"Seed Audio returned no audio (code={response.code}): {response.message}"
|
|
|
|
|
|
)
|
|
|
|
|
|
return IO.NodeOutput(audio_bytes_to_audio_input(base64.b64decode(response.audio)))
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-09-03 23:17:07 +03:00
|
|
|
|
class ByteDanceExtension(ComfyExtension):
|
|
|
|
|
|
@override
|
2025-10-14 09:55:56 +03:00
|
|
|
|
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
2025-09-03 23:17:07 +03:00
|
|
|
|
return [
|
|
|
|
|
|
ByteDanceImageNode,
|
2025-09-11 00:13:18 +03:00
|
|
|
|
ByteDanceSeedreamNode,
|
2026-05-11 12:53:00 +03:00
|
|
|
|
ByteDanceSeedreamNodeV2,
|
2026-08-07 16:25:37 -04:00
|
|
|
|
ByteDanceSeedreamLayerSeparationNode,
|
2025-09-09 21:40:29 +03:00
|
|
|
|
ByteDanceTextToVideoNode,
|
|
|
|
|
|
ByteDanceImageToVideoNode,
|
|
|
|
|
|
ByteDanceFirstLastFrameNode,
|
|
|
|
|
|
ByteDanceImageReferenceNode,
|
2026-04-13 08:26:19 +03:00
|
|
|
|
ByteDance2TextToVideoNode,
|
|
|
|
|
|
ByteDance2FirstLastFrameNode,
|
|
|
|
|
|
ByteDance2ReferenceNode,
|
2026-04-23 03:59:55 +03:00
|
|
|
|
ByteDanceCreateImageAsset,
|
|
|
|
|
|
ByteDanceCreateVideoAsset,
|
2026-07-03 14:00:10 +03:00
|
|
|
|
ByteDanceSeedAudioNode,
|
2025-09-03 23:17:07 +03:00
|
|
|
|
]
|
|
|
|
|
|
|
2025-10-24 08:37:16 +03:00
|
|
|
|
|
2025-09-03 23:17:07 +03:00
|
|
|
|
async def comfy_entrypoint() -> ByteDanceExtension:
|
|
|
|
|
|
return ByteDanceExtension()
|