From 0db869411564d9c1990043042416b6fce2a18f8a Mon Sep 17 00:00:00 2001 From: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Date: Fri, 7 Aug 2026 21:28:09 +0300 Subject: [PATCH] [Partner Nodes] feat(ByteDance): add SeeDance 2.5 model (#15395) * [Partner Nodes] feat(ByteDance): add SeeDance 2.5 model Signed-off-by: Alexander Piskun --- comfy_api_nodes/apis/bytedance.py | 30 +- comfy_api_nodes/nodes_bytedance.py | 493 +++++++++++++++++++---------- 2 files changed, 350 insertions(+), 173 deletions(-) diff --git a/comfy_api_nodes/apis/bytedance.py b/comfy_api_nodes/apis/bytedance.py index 515e124ca..b83bb99ce 100644 --- a/comfy_api_nodes/apis/bytedance.py +++ b/comfy_api_nodes/apis/bytedance.py @@ -95,9 +95,10 @@ class Seedance2TaskCreationRequest(BaseModel): generate_audio: bool | None = Field(None) resolution: str | None = Field(None) ratio: str | None = Field(None) - duration: int | None = Field(None, ge=4, le=15) + duration: int | None = Field(None) seed: int | None = Field(None, ge=0, le=2147483647) watermark: bool | None = Field(None) + output_format: str | None = Field(None) class TaskCreationResponse(BaseModel): @@ -186,6 +187,10 @@ SEEDANCE2_PRICE_PER_1K_TOKENS = { ("dreamina-seedance-2-0-mini", True, "480p"): 0.0021, ("dreamina-seedance-2-0-mini", False, "720p"): 0.0035, ("dreamina-seedance-2-0-mini", True, "720p"): 0.0021, + ("dreamina-seedance-2-5-260628", False, "480p"): 0.0107, + ("dreamina-seedance-2-5-260628", True, "480p"): 0.0064, + ("dreamina-seedance-2-5-260628", False, "720p"): 0.0107, + ("dreamina-seedance-2-5-260628", True, "720p"): 0.0064, } @@ -304,8 +309,31 @@ SEEDANCE2_REF_VIDEO_PIXEL_LIMITS = { "480p": {"min": 409_600, "max": 927_408}, "720p": {"min": 409_600, "max": 927_408}, }, + "dreamina-seedance-2-5-260628": { + "480p": {"min": 409_600, "max": 8_295_044}, + "720p": {"min": 409_600, "max": 8_295_044}, + }, } +SEEDANCE2_REFERENCE_LIMITS_DEFAULT = { + "max_images": 9, + "max_videos": 3, + "max_audios": 3, + "max_total_seconds": 15.1, +} +SEEDANCE2_REFERENCE_LIMITS = { + "dreamina-seedance-2-5-260628": { + "max_images": 30, + "max_videos": 10, + "max_audios": 10, + "max_total_seconds": 30.1, + }, +} + + +def seedance2_reference_limits(model_id: str) -> dict: + return SEEDANCE2_REFERENCE_LIMITS.get(model_id, SEEDANCE2_REFERENCE_LIMITS_DEFAULT) + # The time in this dictionary are given for 10 seconds duration. VIDEO_TASKS_EXECUTION_TIME = { "seedance-1-0-lite-t2v-250428": { diff --git a/comfy_api_nodes/nodes_bytedance.py b/comfy_api_nodes/nodes_bytedance.py index 561d6ae80..f321a496b 100644 --- a/comfy_api_nodes/nodes_bytedance.py +++ b/comfy_api_nodes/nodes_bytedance.py @@ -47,6 +47,7 @@ from comfy_api_nodes.apis.bytedance import ( Text2ImageTaskCreationRequest, Text2VideoTaskCreationRequest, seedance2_price_per_1k_tokens, + seedance2_reference_limits, ) from comfy_api_nodes.util import ( ApiEndpoint, @@ -101,11 +102,18 @@ BYTEPLUS_TASK_STATUS_ENDPOINT = "/proxy/byteplus/api/v3/contents/generations/tas BYTEPLUS_SEEDANCE2_TASK_STATUS_ENDPOINT = "/proxy/byteplus-seedance2/api/v3/contents/generations/tasks" # + /{task_id} SEEDANCE_MODELS = { + "Seedance 2.5": "dreamina-seedance-2-5-260628", "Seedance 2.0": "dreamina-seedance-2-0-260128", "Seedance 2.0 Fast": "dreamina-seedance-2-0-fast-260128", "Seedance 2.0 Mini": "dreamina-seedance-2-0-mini", } +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." +) + DEPRECATED_MODELS = {"seedance-1-0-lite-t2v-250428", "seedance-1-0-lite-i2v-250428"} @@ -1692,25 +1700,272 @@ def _seedance2_text_inputs(resolutions: list[str], default_ratio: str = "16:9"): ] +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), + ) + + class ByteDance2TextToVideoNode(IO.ComfyNode): @classmethod def define_schema(cls): return IO.Schema( node_id="ByteDance2TextToVideoNode", - display_name="ByteDance Seedance 2.0 Text to Video", + display_name="ByteDance Seedance 2.5 Text to Video", category="partner/video/ByteDance", - description="Generate video using Seedance 2.0 models based on a text prompt.", + description="Generate video using Seedance 2.5 or 2.0 models based on a text prompt.", inputs=[ IO.DynamicCombo.Input( "model", options=[ + IO.DynamicCombo.Option("Seedance 2.5", _seedance25_text_inputs()), IO.DynamicCombo.Option("Seedance 2.0", _seedance2_text_inputs(["480p", "720p", "1080p", "4k"])), IO.DynamicCombo.Option("Seedance 2.0 Fast", _seedance2_text_inputs(["480p", "720p"])), IO.DynamicCombo.Option("Seedance 2.0 Mini", _seedance2_text_inputs(["480p", "720p"])), ], - tooltip="Seedance 2.0 for maximum quality; Fast for speed optimization; " - "Mini for the fastest, lowest-cost generation.", + tooltip=SEEDANCE_MODEL_TOOLTIP, ), IO.Int.Input( "seed", @@ -1739,30 +1994,7 @@ class ByteDance2TextToVideoNode(IO.ComfyNode): IO.Hidden.unique_id, ], is_api_node=True, - price_badge=IO.PriceBadge( - depends_on=IO.PriceBadgeDepends(widgets=["model", "model.resolution", "model.duration"]), - expr=""" - ( - $rate480 := 10044; - $rate720 := 21600; - $rate1080 := 48800; - $rate4k := 195200; - $m := widgets.model; - $res := $lookup(widgets, "model.resolution"); - $dur := $lookup(widgets, "model.duration"); - $pricePer1K := $res = "4k" ? 0.00572 : - $res = "1080p" ? 0.011011 : - $contains($m, "mini") ? 0.005005 : - $contains($m, "fast") ? 0.008008 : 0.01001; - $rate := $res = "4k" ? $rate4k : - $res = "1080p" ? $rate1080 : - $res = "720p" ? $rate720 : - $rate480; - $cost := $dur * $rate * $pricePer1K / 1000; - {"type": "usd", "usd": $cost, "format": {"approximate": true}} - ) - """, - ), + price_badge=_seedance2_price_badge(with_reference_videos=False), ) @classmethod @@ -1777,25 +2009,18 @@ class ByteDance2TextToVideoNode(IO.ComfyNode): initial_response = await sync_op( cls, ApiEndpoint(path=BYTEPLUS_TASK_ENDPOINT, method="POST"), - data=Seedance2TaskCreationRequest( - model=model_id, - content=[TaskTextContent(text=model["prompt"])], - generate_audio=model["generate_audio"], - resolution=model["resolution"], + data=_seedance2_build_request( + model, + model_id, + [TaskTextContent(text=model["prompt"])], + seed, + watermark, ratio=model["ratio"], - duration=model["duration"], - seed=seed, - watermark=watermark, ), response_model=TaskCreationResponse, ) - response = await poll_op( - cls, - ApiEndpoint(path=f"{BYTEPLUS_SEEDANCE2_TASK_STATUS_ENDPOINT}/{initial_response.id}"), - response_model=TaskStatusResponse, - status_extractor=lambda r: r.status, - price_extractor=_seedance2_price_extractor(model_id, has_video_input=False, resolution=model["resolution"]), - poll_interval=9, + response = await _seedance2_poll_video_task( + cls, initial_response.id, model_id, model["resolution"], has_video_input=False ) return IO.NodeOutput(await download_url_to_video_output(response.content.video_url)) @@ -1806,13 +2031,15 @@ class ByteDance2FirstLastFrameNode(IO.ComfyNode): def define_schema(cls): return IO.Schema( node_id="ByteDance2FirstLastFrameNode", - display_name="ByteDance Seedance 2.0 First-Last-Frame to Video", + display_name="ByteDance Seedance 2.5 First-Last-Frame to Video", category="partner/video/ByteDance", - description="Generate video using Seedance 2.0 from a first frame image and optional last frame image.", + description="Generate video using Seedance 2.5 or 2.0 from a first frame image " + "and optional last frame image.", inputs=[ IO.DynamicCombo.Input( "model", options=[ + IO.DynamicCombo.Option("Seedance 2.5", _seedance25_text_inputs(with_ratio=False)), IO.DynamicCombo.Option( "Seedance 2.0", _seedance2_text_inputs(["480p", "720p", "1080p", "4k"], default_ratio="adaptive"), @@ -1826,8 +2053,7 @@ class ByteDance2FirstLastFrameNode(IO.ComfyNode): _seedance2_text_inputs(["480p", "720p"], default_ratio="adaptive"), ), ], - tooltip="Seedance 2.0 for maximum quality; Fast for speed optimization; " - "Mini for the fastest, lowest-cost generation.", + tooltip=SEEDANCE_MODEL_TOOLTIP, ), IO.Image.Input( "first_frame", @@ -1880,30 +2106,7 @@ class ByteDance2FirstLastFrameNode(IO.ComfyNode): IO.Hidden.unique_id, ], is_api_node=True, - price_badge=IO.PriceBadge( - depends_on=IO.PriceBadgeDepends(widgets=["model", "model.resolution", "model.duration"]), - expr=""" - ( - $rate480 := 10044; - $rate720 := 21600; - $rate1080 := 48800; - $rate4k := 195200; - $m := widgets.model; - $res := $lookup(widgets, "model.resolution"); - $dur := $lookup(widgets, "model.duration"); - $pricePer1K := $res = "4k" ? 0.00572 : - $res = "1080p" ? 0.011011 : - $contains($m, "mini") ? 0.005005 : - $contains($m, "fast") ? 0.008008 : 0.01001; - $rate := $res = "4k" ? $rate4k : - $res = "1080p" ? $rate1080 : - $res = "720p" ? $rate720 : - $rate480; - $cost := $dur * $rate * $pricePer1K / 1000; - {"type": "usd", "usd": $cost, "format": {"approximate": true}} - ) - """, - ), + price_badge=_seedance2_price_badge(with_reference_videos=False), ) @classmethod @@ -1930,8 +2133,17 @@ class ByteDance2FirstLastFrameNode(IO.ComfyNode): 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.") - request_ratio = model["ratio"] - if first_frame_asset_id or last_frame_asset_id: + 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"] if first_frame is not None: first_frame = _prepare_seedance_image(first_frame) if last_frame is not None: @@ -1997,25 +2209,11 @@ class ByteDance2FirstLastFrameNode(IO.ComfyNode): initial_response = await sync_op( cls, ApiEndpoint(path=BYTEPLUS_TASK_ENDPOINT, method="POST"), - data=Seedance2TaskCreationRequest( - model=model_id, - content=content, - generate_audio=model["generate_audio"], - resolution=model["resolution"], - ratio=request_ratio, - duration=model["duration"], - seed=seed, - watermark=watermark, - ), + data=_seedance2_build_request(model, model_id, content, seed, watermark, ratio=request_ratio), response_model=TaskCreationResponse, ) - response = await poll_op( - cls, - ApiEndpoint(path=f"{BYTEPLUS_SEEDANCE2_TASK_STATUS_ENDPOINT}/{initial_response.id}"), - response_model=TaskStatusResponse, - status_extractor=lambda r: r.status, - price_extractor=_seedance2_price_extractor(model_id, has_video_input=False, resolution=model["resolution"]), - poll_interval=9, + response = await _seedance2_poll_video_task( + cls, initial_response.id, model_id, model["resolution"], has_video_input=False ) return IO.NodeOutput(await download_url_to_video_output(response.content.video_url)) @@ -2101,14 +2299,15 @@ class ByteDance2ReferenceNode(IO.ComfyNode): def define_schema(cls): return IO.Schema( node_id="ByteDance2ReferenceNode", - display_name="ByteDance Seedance 2.0 Reference to Video", + display_name="ByteDance Seedance 2.5 Reference to Video", category="partner/video/ByteDance", - description="Generate, edit, or extend video using Seedance 2.0 with reference images, " - "videos, and audio. Supports multimodal reference, video editing, and video extension.", + 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.", inputs=[ IO.DynamicCombo.Input( "model", options=[ + IO.DynamicCombo.Option("Seedance 2.5", _seedance25_reference_inputs()), IO.DynamicCombo.Option( "Seedance 2.0", _seedance2_reference_inputs(["480p", "720p", "1080p", "4k"], default_ratio="adaptive"), @@ -2122,8 +2321,7 @@ class ByteDance2ReferenceNode(IO.ComfyNode): _seedance2_reference_inputs(["480p", "720p"], default_ratio="adaptive"), ), ], - tooltip="Seedance 2.0 for maximum quality; Fast for speed optimization; " - "Mini for the fastest, lowest-cost generation.", + tooltip=SEEDANCE_MODEL_TOOLTIP, ), IO.Int.Input( "seed", @@ -2152,52 +2350,7 @@ class ByteDance2ReferenceNode(IO.ComfyNode): IO.Hidden.unique_id, ], is_api_node=True, - price_badge=IO.PriceBadge( - depends_on=IO.PriceBadgeDepends( - widgets=["model", "model.resolution", "model.duration"], - input_groups=["model.reference_videos"], - ), - expr=""" - ( - $rate480 := 10044; - $rate720 := 21600; - $rate1080 := 48800; - $rate4k := 195200; - $m := widgets.model; - $hasVideo := $lookup(inputGroups, "model.reference_videos") > 0; - $res := $lookup(widgets, "model.resolution"); - $dur := $lookup(widgets, "model.duration"); - $noVideoPricePer1K := $res = "4k" ? 0.00572 : - $res = "1080p" ? 0.011011 : - $contains($m, "mini") ? 0.005005 : - $contains($m, "fast") ? 0.008008 : 0.01001; - $videoPricePer1K := $res = "4k" ? 0.003432 : - $res = "1080p" ? 0.006721 : - $contains($m, "mini") ? 0.003003 : - $contains($m, "fast") ? 0.004719 : 0.006149; - $rate := $res = "4k" ? $rate4k : - $res = "1080p" ? $rate1080 : - $res = "720p" ? $rate720 : - $rate480; - $noVideoCost := $dur * $rate * $noVideoPricePer1K / 1000; - $minVideoFactor := $ceil($dur * 5 / 3); - $minVideoCost := $minVideoFactor * $rate * $videoPricePer1K / 1000; - $maxVideoCost := (15 + $dur) * $rate * $videoPricePer1K / 1000; - $hasVideo - ? { - "type": "range_usd", - "min_usd": $minVideoCost, - "max_usd": $maxVideoCost, - "format": {"approximate": true} - } - : { - "type": "usd", - "usd": $noVideoCost, - "format": {"approximate": true} - } - ) - """, - ), + price_badge=_seedance2_price_badge(with_reference_videos=True), ) @classmethod @@ -2218,32 +2371,38 @@ class ByteDance2ReferenceNode(IO.ComfyNode): cls, list(reference_assets.values()) ) + model_id = SEEDANCE_MODELS[model["model"]] + limits = seedance2_reference_limits(model_id) + if not reference_images and not reference_videos and not reference_image_assets and not reference_video_assets: - raise ValueError("At least one reference image or video or asset is required.") + 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.") total_images = len(reference_images) + len(reference_image_assets) - if total_images > 9: + if total_images > limits["max_images"]: raise ValueError( f"Too many reference images: {total_images} " - f"(images={len(reference_images)}, image assets={len(reference_image_assets)}). Maximum is 9." + f"(images={len(reference_images)}, image assets={len(reference_image_assets)}). " + f"Maximum is {limits['max_images']}." ) total_videos = len(reference_videos) + len(reference_video_assets) - if total_videos > 3: + if total_videos > limits["max_videos"]: raise ValueError( f"Too many reference videos: {total_videos} " - f"(videos={len(reference_videos)}, video assets={len(reference_video_assets)}). Maximum is 3." + f"(videos={len(reference_videos)}, video assets={len(reference_video_assets)}). " + f"Maximum is {limits['max_videos']}." ) total_audios = len(reference_audios) + len(reference_audio_assets) - if total_audios > 3: + if total_audios > limits["max_audios"]: raise ValueError( f"Too many reference audios: {total_audios} " - f"(audios={len(reference_audios)}, audio assets={len(reference_audio_assets)}). Maximum is 3." + f"(audios={len(reference_audios)}, audio assets={len(reference_audio_assets)}). " + f"Maximum is {limits['max_audios']}." ) for key in reference_images: reference_images[key] = _prepare_seedance_image(reference_images[key]) - model_id = SEEDANCE_MODELS[model["model"]] has_video_input = total_videos > 0 if model.get("auto_downscale") and reference_videos: @@ -2271,8 +2430,11 @@ class ByteDance2ReferenceNode(IO.ComfyNode): raise except Exception: pass - if total_video_duration > 15.1: - raise ValueError(f"Total reference video duration is {total_video_duration:.1f}s. Maximum is 15.1 seconds.") + 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." + ) total_audio_duration = 0.0 for i, key in enumerate(reference_audios, 1): @@ -2281,8 +2443,11 @@ class ByteDance2ReferenceNode(IO.ComfyNode): 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 - if total_audio_duration > 15.1: - raise ValueError(f"Total reference audio duration is {total_audio_duration:.1f}s. Maximum is 15.1 seconds.") + 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." + ) asset_labels = _build_asset_labels( reference_assets, @@ -2355,27 +2520,11 @@ class ByteDance2ReferenceNode(IO.ComfyNode): initial_response = await sync_op( cls, ApiEndpoint(path=BYTEPLUS_TASK_ENDPOINT, method="POST"), - data=Seedance2TaskCreationRequest( - model=model_id, - content=content, - generate_audio=model["generate_audio"], - resolution=model["resolution"], - ratio=model["ratio"], - duration=model["duration"], - seed=seed, - watermark=watermark, - ), + data=_seedance2_build_request(model, model_id, content, seed, watermark, ratio=model["ratio"]), response_model=TaskCreationResponse, ) - response = await poll_op( - cls, - ApiEndpoint(path=f"{BYTEPLUS_SEEDANCE2_TASK_STATUS_ENDPOINT}/{initial_response.id}"), - response_model=TaskStatusResponse, - status_extractor=lambda r: r.status, - price_extractor=_seedance2_price_extractor( - model_id, has_video_input=has_video_input, resolution=model["resolution"] - ), - poll_interval=9, + response = await _seedance2_poll_video_task( + cls, initial_response.id, model_id, model["resolution"], has_video_input=has_video_input ) return IO.NodeOutput(await download_url_to_video_output(response.content.video_url))