[Partner Nodes] feat(ByteDance): add Seedance 2.5 task_type for video extension (#15579)
Signed-off-by: Alexander Piskun <bigcat88@icloud.com>
This commit is contained in:
parent
a7365071e4
commit
0f1fa67ad8
|
|
@ -116,6 +116,7 @@ class Seedance2TaskCreationRequest(BaseModel):
|
|||
seed: int | None = Field(None, ge=0, le=2147483647)
|
||||
watermark: bool | None = Field(None)
|
||||
output_format: str | None = Field(None)
|
||||
omni_reference_task_type: str | None = Field(None, description="One of: auto, reference, edit, extend.")
|
||||
|
||||
|
||||
class TaskCreationResponse(BaseModel):
|
||||
|
|
|
|||
|
|
@ -2069,7 +2069,7 @@ 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):
|
||||
def _seedance25_text_inputs(with_ratio: bool = True, with_video_editing: bool = False, with_task_type: bool = False):
|
||||
return [
|
||||
IO.String.Input(
|
||||
"prompt",
|
||||
|
|
@ -2124,6 +2124,29 @@ def _seedance25_text_inputs(with_ratio: bool = True, with_video_editing: bool =
|
|||
if with_video_editing
|
||||
else []
|
||||
),
|
||||
*(
|
||||
[
|
||||
IO.Combo.Input(
|
||||
"task_type",
|
||||
options=["auto", "reference", "edit", "extend"],
|
||||
default="auto",
|
||||
tooltip="What to do with the reference media. Every value except auto is "
|
||||
"validated when the task is submitted, so mismatched settings fail before "
|
||||
"generation starts. auto: the model infers the task from the prompt and "
|
||||
"inputs, and settings that conflict with its reading fail only after "
|
||||
"generation has started. reference: generate a new video guided by the "
|
||||
"reference images, videos, and audio. edit: change a connected reference "
|
||||
"video (add, remove, replace); the output keeps the source clip's own length "
|
||||
"and aspect ratio, and the duration and ratio widgets are ignored. extend: "
|
||||
"continue a connected reference video forward or backward; the prompt should "
|
||||
"say 'extend forward', 'extend backward', or 'continue', the aspect ratio "
|
||||
"follows the source clip, and the output contains only the newly generated "
|
||||
"segment of the duration you set, not the source clip.",
|
||||
)
|
||||
]
|
||||
if with_task_type
|
||||
else []
|
||||
),
|
||||
IO.Combo.Input(
|
||||
"output_format",
|
||||
options=["mp4"],
|
||||
|
|
@ -2133,9 +2156,9 @@ def _seedance25_text_inputs(with_ratio: bool = True, with_video_editing: bool =
|
|||
]
|
||||
|
||||
|
||||
def _seedance25_reference_inputs():
|
||||
def _seedance25_reference_inputs(with_video_editing: bool = False, with_task_type: bool = False):
|
||||
return [
|
||||
*_seedance25_text_inputs(with_video_editing=True),
|
||||
*_seedance25_text_inputs(with_video_editing=with_video_editing, with_task_type=with_task_type),
|
||||
IO.Autogrow.Input(
|
||||
"reference_images",
|
||||
template=IO.Autogrow.TemplateNames(
|
||||
|
|
@ -2196,17 +2219,23 @@ def _seedance2_build_request(
|
|||
watermark: bool,
|
||||
ratio: str,
|
||||
) -> Seedance2TaskCreationRequest:
|
||||
video_editing = bool(model.get("video_editing"))
|
||||
task_type = model.get("task_type", "auto")
|
||||
duration = model["duration"]
|
||||
if model.get("video_editing") or task_type == "edit":
|
||||
ratio, duration = "adaptive", -1
|
||||
elif task_type == "extend":
|
||||
ratio = "adaptive"
|
||||
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"],
|
||||
ratio=ratio,
|
||||
duration=duration,
|
||||
seed=seed,
|
||||
watermark=watermark,
|
||||
output_format=model.get("output_format"),
|
||||
omni_reference_task_type=None if task_type == "auto" else task_type,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -2216,7 +2245,7 @@ _SEEDANCE2_PRICE_EXPR_TEMPLATE = """
|
|||
$res := $lookup(widgets, "model.resolution");
|
||||
$ratio := $lookup(widgets, "model.ratio");
|
||||
$dur := $lookup(widgets, "model.duration");
|
||||
$auto := $lookup(widgets, "model.video_editing") = true;
|
||||
$auto := __IS_EDIT__;
|
||||
$hasVideo := __HAS_VIDEO__;
|
||||
$ready := $type($m) = "string" and $type($res) = "string" and ($auto or $type($dur) = "number");
|
||||
$ready ? (
|
||||
|
|
@ -2261,6 +2290,7 @@ _SEEDANCE2_PRICE_EXPR_TEMPLATE = """
|
|||
|
||||
_SEEDANCE_AUDIO_POLICY_CODE = "OutputAudioSensitiveContentDetected.PolicyViolation"
|
||||
_SEEDANCE_TASK_TYPE_CONSTRAINT_CODE = "InvalidParameter.TaskTypeConstraint"
|
||||
_SEEDANCE_TASK_TYPE_MISMATCH_CODE = "InvalidParameter.TaskTypeMismatch"
|
||||
|
||||
|
||||
async def _seedance2_poll_video_task(
|
||||
|
|
@ -2269,6 +2299,7 @@ async def _seedance2_poll_video_task(
|
|||
model_id: str,
|
||||
resolution: str,
|
||||
has_video_input: bool,
|
||||
task_type: str | None = None,
|
||||
) -> TaskStatusResponse:
|
||||
try:
|
||||
return await poll_op(
|
||||
|
|
@ -2289,19 +2320,48 @@ async def _seedance2_poll_video_task(
|
|||
"to get a silent video, or adjust the prompt and try again."
|
||||
) from exc
|
||||
if _SEEDANCE_TASK_TYPE_CONSTRAINT_CODE in str(exc):
|
||||
if task_type is None:
|
||||
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
|
||||
if task_type == "edit":
|
||||
raise ValueError(
|
||||
"The request does not satisfy the 'edit' constraints: the clip being edited "
|
||||
"must be 4 to 30 seconds long."
|
||||
) from exc
|
||||
if task_type == "extend":
|
||||
raise ValueError(
|
||||
"The request does not satisfy the 'extend' constraints: the clip being "
|
||||
"extended must be 1.9 to 30 seconds long."
|
||||
) from 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."
|
||||
"Seedance decided from the prompt that this task's duration or aspect ratio "
|
||||
"must come from the reference video, and the current settings conflict with "
|
||||
"that. Set task_type to the task you mean ('edit' or 'extend') and run again, "
|
||||
"or reword the prompt so it describes a new video rather than a change to the "
|
||||
"reference one."
|
||||
) from exc
|
||||
if _SEEDANCE_TASK_TYPE_MISMATCH_CODE in str(exc):
|
||||
raise ValueError(
|
||||
f"Seedance read this prompt as a different task than the selected task_type "
|
||||
f"'{task_type}'. Reword the prompt so it matches: an extend prompt should say "
|
||||
"'extend forward', 'extend backward', or 'continue'; an edit prompt should use "
|
||||
"words like add, remove, replace, or change. Or set task_type to auto."
|
||||
) from exc
|
||||
raise
|
||||
|
||||
|
||||
def _seedance2_price_badge(with_reference_videos: bool) -> IO.PriceBadge:
|
||||
def _seedance2_price_badge(with_reference_videos: bool, legacy_video_editing: bool = False) -> IO.PriceBadge:
|
||||
widgets = ["model", "model.resolution", "model.ratio", "model.duration"]
|
||||
if legacy_video_editing:
|
||||
is_edit = '$lookup(widgets, "model.video_editing") = true'
|
||||
else:
|
||||
is_edit = '$lookup(widgets, "model.task_type") = "edit"'
|
||||
if with_reference_videos:
|
||||
widgets.append("model.video_editing")
|
||||
widgets.append("model.video_editing" if legacy_video_editing else "model.task_type")
|
||||
has_video = (
|
||||
'$exists(inputGroups) and $lookup(inputGroups, "model.reference_videos") > 0'
|
||||
if with_reference_videos
|
||||
|
|
@ -2312,7 +2372,7 @@ def _seedance2_price_badge(with_reference_videos: bool) -> IO.PriceBadge:
|
|||
widgets=widgets,
|
||||
input_groups=["model.reference_videos"] if with_reference_videos else [],
|
||||
),
|
||||
expr=_SEEDANCE2_PRICE_EXPR_TEMPLATE.replace("__HAS_VIDEO__", has_video),
|
||||
expr=_SEEDANCE2_PRICE_EXPR_TEMPLATE.replace("__HAS_VIDEO__", has_video).replace("__IS_EDIT__", is_edit),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -2662,12 +2722,12 @@ def _seedance2_reference_inputs(resolutions: list[str], default_ratio: str = "16
|
|||
]
|
||||
|
||||
|
||||
class ByteDance2ReferenceNode(IO.ComfyNode):
|
||||
class ByteDance2ReferenceNodeV2(IO.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return IO.Schema(
|
||||
node_id="ByteDance2ReferenceNode",
|
||||
node_id="ByteDance2ReferenceNodeV2",
|
||||
display_name="ByteDance Seedance 2.5 Reference to Video",
|
||||
category="partner/video/ByteDance",
|
||||
description="Generate, edit, or extend video using Seedance 2.5 or 2.0 with reference "
|
||||
|
|
@ -2676,7 +2736,7 @@ class ByteDance2ReferenceNode(IO.ComfyNode):
|
|||
IO.DynamicCombo.Input(
|
||||
"model",
|
||||
options=[
|
||||
IO.DynamicCombo.Option("Seedance 2.5", _seedance25_reference_inputs()),
|
||||
IO.DynamicCombo.Option("Seedance 2.5", _seedance25_reference_inputs(with_task_type=True)),
|
||||
IO.DynamicCombo.Option(
|
||||
"Seedance 2.0",
|
||||
_seedance2_reference_inputs(["480p", "720p", "1080p", "4k"], default_ratio="adaptive"),
|
||||
|
|
@ -2761,6 +2821,13 @@ class ByteDance2ReferenceNode(IO.ComfyNode):
|
|||
f"(videos={len(reference_videos)}, video assets={len(reference_video_assets)}). "
|
||||
f"Maximum is {limits['max_videos']}."
|
||||
)
|
||||
task_type = model.get("task_type")
|
||||
if task_type in ("edit", "extend") and total_videos == 0:
|
||||
raise ValueError(
|
||||
f"A '{task_type}' task needs at least one reference video. Connect the video "
|
||||
f"you want to {'change' if task_type == 'edit' else 'continue'}, or set "
|
||||
"task_type to 'reference' to generate a new video from the references you have."
|
||||
)
|
||||
total_audios = len(reference_audios) + len(reference_audio_assets)
|
||||
if total_audios > limits["max_audios"]:
|
||||
raise ValueError(
|
||||
|
|
@ -2893,11 +2960,78 @@ class ByteDance2ReferenceNode(IO.ComfyNode):
|
|||
response_model=TaskCreationResponse,
|
||||
)
|
||||
response = await _seedance2_poll_video_task(
|
||||
cls, initial_response.id, model_id, model["resolution"], has_video_input=has_video_input
|
||||
cls,
|
||||
initial_response.id,
|
||||
model_id,
|
||||
model["resolution"],
|
||||
has_video_input=has_video_input,
|
||||
task_type=task_type,
|
||||
)
|
||||
return IO.NodeOutput(await download_url_to_video_output(response.content.video_url))
|
||||
|
||||
|
||||
class ByteDance2ReferenceNode(ByteDance2ReferenceNodeV2):
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return IO.Schema(
|
||||
node_id="ByteDance2ReferenceNode",
|
||||
display_name="ByteDance Seedance 2.5 Reference to Video (Legacy)",
|
||||
category="partner/video/ByteDance",
|
||||
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(with_video_editing=True)),
|
||||
IO.DynamicCombo.Option(
|
||||
"Seedance 2.0",
|
||||
_seedance2_reference_inputs(["480p", "720p", "1080p", "4k"], default_ratio="adaptive"),
|
||||
),
|
||||
IO.DynamicCombo.Option(
|
||||
"Seedance 2.0 Fast",
|
||||
_seedance2_reference_inputs(["480p", "720p"], default_ratio="adaptive"),
|
||||
),
|
||||
IO.DynamicCombo.Option(
|
||||
"Seedance 2.0 Mini",
|
||||
_seedance2_reference_inputs(["480p", "720p"], default_ratio="adaptive"),
|
||||
),
|
||||
],
|
||||
tooltip=SEEDANCE_MODEL_TOOLTIP,
|
||||
),
|
||||
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,
|
||||
is_deprecated=True,
|
||||
price_badge=_seedance2_price_badge(with_reference_videos=True, legacy_video_editing=True),
|
||||
)
|
||||
|
||||
|
||||
async def process_video_task(
|
||||
cls: type[IO.ComfyNode],
|
||||
payload: Text2VideoTaskCreationRequest | Image2VideoTaskCreationRequest,
|
||||
|
|
@ -3413,6 +3547,7 @@ class ByteDanceExtension(ComfyExtension):
|
|||
ByteDance2TextToVideoNode,
|
||||
ByteDance2FirstLastFrameNode,
|
||||
ByteDance2ReferenceNode,
|
||||
ByteDance2ReferenceNodeV2,
|
||||
ByteDanceCreateImageAsset,
|
||||
ByteDanceCreateVideoAsset,
|
||||
ByteDanceSeedAudioNode,
|
||||
|
|
|
|||
Loading…
Reference in New Issue