Add Seedance 2.0 node and preflight alignment

This commit is contained in:
Dreaming Computers
2026-04-05 08:32:53 +07:00
parent 5ac4eb5515
commit 0a631bfe01
6 changed files with 746 additions and 1 deletions
+4
View File
@@ -91,6 +91,10 @@ This node pack currently includes the following nodes:
Fast image-to-video generation optimized for quick iteration.
- **Seedance 1.5 Pro Image-to-Video, Text-to-Video**
Higher-quality image-to-video generation using the Seedance 1.5 Pro model.
- **Seedance 2.0 Video**
- Supports text-to-video, first-frame I2V, first+last-frame I2V, and multimodal reference-to-video.
- Includes a preflight node for validating mutually exclusive media modes before spending credits.
- Current implementation intentionally targets verified `bytedance/seedance-2` only; `Seedance 2.0 Fast` remains pending due to upstream docs conflicts.
## LLM Nodes
- **Gemini (LLM) [Experimental]**
+436
View File
@@ -0,0 +1,436 @@
"""Seedance 2.0 video helpers."""
import json
import time
from typing import Any
import torch
from .audio import _coerce_audio_to_wav_bytes
from .auth import _load_api_key
from .credits import _log_remaining_credits
from .jobs import _create_task, _poll_task_until_complete
from .log import _log
from .results import _extract_result_urls
from .upload import _image_tensor_to_png_bytes, _truncate_url, _upload_audio, _upload_image, _upload_video
from .validation import _validate_image_tensor_batch, _validate_prompt
from .video import _coerce_video_to_mp4_bytes, _download_video, _video_bytes_to_comfy_video
MODEL_NAME = "bytedance/seedance-2"
PROMPT_MAX_LENGTH = 5000
ASPECT_RATIO_OPTIONS = ["16:9", "9:16", "1:1"]
RESOLUTION_OPTIONS = ["480p", "720p", "1080p"]
DURATION_OPTIONS = ["5", "10", "15"]
def _validation_error(message: str) -> RuntimeError:
return RuntimeError(f"Seedance 2.0 validation error: {message}")
def _validate_frame_image(images: torch.Tensor | None, label: str) -> torch.Tensor:
batch = _validate_image_tensor_batch(images)
if batch.shape[0] > 1:
raise _validation_error(f"{label} accepts a single image. Connect one image or slice the batch first.")
return batch
def _validate_options(
*,
aspect_ratio: str,
resolution: str,
duration: str,
generate_audio: bool,
return_last_frame: bool,
web_search: bool,
) -> None:
if aspect_ratio not in ASPECT_RATIO_OPTIONS:
raise _validation_error("Invalid aspect_ratio. Use the pinned enum options.")
if resolution not in RESOLUTION_OPTIONS:
raise _validation_error("Invalid resolution. Use the pinned enum options.")
if duration not in DURATION_OPTIONS:
raise _validation_error("Invalid duration. Use the pinned enum options.")
if not isinstance(generate_audio, bool):
raise _validation_error("generate_audio must be a boolean value.")
if not isinstance(return_last_frame, bool):
raise _validation_error("return_last_frame must be a boolean value.")
if not isinstance(web_search, bool):
raise _validation_error("web_search must be a boolean value.")
def _detect_scenario(
*,
first_frame: torch.Tensor | None,
last_frame: torch.Tensor | None,
reference_images: torch.Tensor | None,
reference_video: Any | None,
reference_audio: Any | None,
) -> str:
has_first = first_frame is not None
has_last = last_frame is not None
has_references = any(item is not None for item in (reference_images, reference_video, reference_audio))
if has_last and not has_first:
raise _validation_error("last_frame requires first_frame.")
if has_first and has_last and has_references:
return "first_last_frame_with_references"
if has_first and has_references:
return "first_frame_with_references"
if has_first and has_last:
return "first_last_frame"
if has_first:
return "first_frame"
if has_references:
return "multimodal_reference"
return "text_to_video"
def _upload_single_frame(api_key: str, images: torch.Tensor | None, label: str, log: bool) -> str | None:
if images is None:
return None
batch = _validate_frame_image(images, label)
_log(log, f"Uploading {label} for Seedance 2.0...")
image_url = _upload_image(api_key, _image_tensor_to_png_bytes(batch[0]))
_log(log, f"{label} upload success: {_truncate_url(image_url)}")
return image_url
def _upload_reference_images(api_key: str, images: torch.Tensor | None, log: bool) -> list[str]:
if images is None:
return []
batch = _validate_image_tensor_batch(images)
image_urls: list[str] = []
_log(log, f"Uploading {batch.shape[0]} reference image(s) for Seedance 2.0...")
for idx in range(batch.shape[0]):
image_url = _upload_image(api_key, _image_tensor_to_png_bytes(batch[idx]))
image_urls.append(image_url)
_log(log, f"Reference image {idx + 1} upload success: {_truncate_url(image_url)}")
return image_urls
def _upload_reference_video(api_key: str, reference_video: Any | None, log: bool) -> list[str]:
if reference_video is None:
return []
video_bytes, source = _coerce_video_to_mp4_bytes(reference_video)
_log(log, f"Uploading reference video for Seedance 2.0 ({source})...")
video_url = _upload_video(api_key, video_bytes, filename="seedance2_reference.mp4")
_log(log, f"Reference video upload success: {_truncate_url(video_url)}")
return [video_url]
def _upload_reference_audio(api_key: str, reference_audio: Any | None, log: bool) -> list[str]:
if reference_audio is None:
return []
audio_bytes, source = _coerce_audio_to_wav_bytes(reference_audio)
_log(log, f"Uploading reference audio for Seedance 2.0 ({source})...")
audio_url = _upload_audio(api_key, audio_bytes, filename="seedance2_reference.wav")
_log(log, f"Reference audio upload success: {_truncate_url(audio_url)}")
return [audio_url]
def _build_seedance2_payload(
*,
prompt: str,
aspect_ratio: str,
resolution: str,
duration: str,
generate_audio: bool,
return_last_frame: bool,
web_search: bool,
first_frame: torch.Tensor | None,
last_frame: torch.Tensor | None,
reference_images: torch.Tensor | None,
reference_video: Any | None,
reference_audio: Any | None,
log: bool,
) -> tuple[dict[str, Any], str]:
_validate_prompt(prompt, max_length=PROMPT_MAX_LENGTH)
_validate_options(
aspect_ratio=aspect_ratio,
resolution=resolution,
duration=duration,
generate_audio=generate_audio,
return_last_frame=return_last_frame,
web_search=web_search,
)
scenario = _detect_scenario(
first_frame=first_frame,
last_frame=last_frame,
reference_images=reference_images,
reference_video=reference_video,
reference_audio=reference_audio,
)
api_key = _load_api_key()
input_payload: dict[str, Any] = {
"prompt": prompt,
"return_last_frame": return_last_frame,
"generate_audio": generate_audio,
"resolution": resolution,
"aspect_ratio": aspect_ratio,
"duration": int(duration),
"web_search": web_search,
}
if scenario in {
"first_frame",
"first_last_frame",
"first_frame_with_references",
"first_last_frame_with_references",
}:
first_frame_url = _upload_single_frame(api_key, first_frame, "first_frame", log)
if first_frame_url:
input_payload["first_frame_url"] = first_frame_url
if scenario in {"first_last_frame", "first_last_frame_with_references"}:
last_frame_url = _upload_single_frame(api_key, last_frame, "last_frame", log)
if last_frame_url:
input_payload["last_frame_url"] = last_frame_url
if scenario in {
"multimodal_reference",
"first_frame_with_references",
"first_last_frame_with_references",
}:
reference_image_urls = _upload_reference_images(api_key, reference_images, log)
reference_video_urls = _upload_reference_video(api_key, reference_video, log)
reference_audio_urls = _upload_reference_audio(api_key, reference_audio, log)
if reference_image_urls:
input_payload["reference_image_urls"] = reference_image_urls
if reference_video_urls:
input_payload["reference_video_urls"] = reference_video_urls
if reference_audio_urls:
input_payload["reference_audio_urls"] = reference_audio_urls
return {"model": MODEL_NAME, "input": input_payload}, scenario
def summarize_seedance2_payload(payload: dict[str, Any]) -> dict[str, Any]:
normalized = _normalize_request_payload(payload)
payload_input = normalized.get("input", {})
reference_image_urls = list(payload_input.get("reference_image_urls") or [])
reference_video_urls = list(payload_input.get("reference_video_urls") or [])
reference_audio_urls = list(payload_input.get("reference_audio_urls") or [])
fields_present = ["prompt", "return_last_frame", "generate_audio", "resolution", "aspect_ratio", "duration", "web_search"]
for field_name in (
"first_frame_url",
"last_frame_url",
"reference_image_urls",
"reference_video_urls",
"reference_audio_urls",
):
if field_name in payload_input:
fields_present.append(field_name)
scenario = "text-to-video"
if payload_input.get("first_frame_url") and payload_input.get("last_frame_url") and (
reference_image_urls or reference_video_urls or reference_audio_urls
):
scenario = "first+last-frame with references"
elif payload_input.get("first_frame_url") and (
reference_image_urls or reference_video_urls or reference_audio_urls
):
scenario = "first-frame with references"
elif payload_input.get("first_frame_url") and payload_input.get("last_frame_url"):
scenario = "first+last-frame image-to-video"
elif payload_input.get("first_frame_url"):
scenario = "first-frame image-to-video"
elif reference_image_urls or reference_video_urls or reference_audio_urls:
scenario = "multimodal reference-to-video"
return {
"scenario": scenario,
"fields_present": fields_present,
"has_first_frame": bool(payload_input.get("first_frame_url")),
"has_last_frame": bool(payload_input.get("last_frame_url")),
"reference_image_count": len(reference_image_urls),
"reference_video_count": len(reference_video_urls),
"reference_audio_count": len(reference_audio_urls),
"reference_image_aliases": [f"@Image{idx}" for idx in range(1, len(reference_image_urls) + 1)],
"reference_video_aliases": [f"@Video{idx}" for idx in range(1, len(reference_video_urls) + 1)],
"reference_audio_aliases": [f"@Audio{idx}" for idx in range(1, len(reference_audio_urls) + 1)],
}
def preflight_seedance2_payload(
*,
prompt: str,
aspect_ratio: str,
resolution: str,
duration: str,
generate_audio: bool,
return_last_frame: bool,
web_search: bool,
first_frame: torch.Tensor | None,
last_frame: torch.Tensor | None,
reference_images: torch.Tensor | None,
reference_video: Any | None,
reference_audio: Any | None,
log: bool,
) -> dict[str, Any]:
payload, _scenario = _build_seedance2_payload(
prompt=prompt,
aspect_ratio=aspect_ratio,
resolution=resolution,
duration=duration,
generate_audio=generate_audio,
return_last_frame=return_last_frame,
web_search=web_search,
first_frame=first_frame,
last_frame=last_frame,
reference_images=reference_images,
reference_video=reference_video,
reference_audio=reference_audio,
log=log,
)
return payload
def run_seedance2_video_payload(
*,
prompt: str,
aspect_ratio: str,
resolution: str,
duration: str,
generate_audio: bool,
return_last_frame: bool,
web_search: bool,
first_frame: torch.Tensor | None,
last_frame: torch.Tensor | None,
reference_images: torch.Tensor | None,
reference_video: Any | None,
reference_audio: Any | None,
log: bool,
) -> tuple[dict[str, Any], str]:
return _build_seedance2_payload(
prompt=prompt,
aspect_ratio=aspect_ratio,
resolution=resolution,
duration=duration,
generate_audio=generate_audio,
return_last_frame=return_last_frame,
web_search=web_search,
first_frame=first_frame,
last_frame=last_frame,
reference_images=reference_images,
reference_video=reference_video,
reference_audio=reference_audio,
log=log,
)
def _normalize_request_payload(payload: dict[str, Any]) -> dict[str, Any]:
normalized = dict(payload)
if normalized.get("model") != MODEL_NAME:
raise _validation_error(f"request payload model must be '{MODEL_NAME}'.")
payload_input = normalized.get("input")
if not isinstance(payload_input, dict):
raise _validation_error("request payload must include an input object.")
normalized_input = dict(payload_input)
if "reference_video_urls " in normalized_input and "reference_video_urls" not in normalized_input:
normalized_input["reference_video_urls"] = normalized_input.pop("reference_video_urls ")
normalized["input"] = normalized_input
return normalized
def _select_video_url(result_urls: list[str]) -> str:
for url in result_urls:
lower = str(url).lower()
if any(lower.endswith(ext) for ext in (".mp4", ".mov", ".webm", ".m4v")):
return url
return result_urls[0]
def run_seedance2_video(
*,
prompt: str,
aspect_ratio: str,
resolution: str,
duration: str,
generate_audio: bool,
return_last_frame: bool,
web_search: bool,
first_frame: torch.Tensor | None,
last_frame: torch.Tensor | None,
reference_images: torch.Tensor | None,
reference_video: Any | None,
reference_audio: Any | None,
poll_interval_s: float,
timeout_s: int,
log: bool,
) -> Any:
payload, _scenario = run_seedance2_video_payload(
prompt=prompt,
aspect_ratio=aspect_ratio,
resolution=resolution,
duration=duration,
generate_audio=generate_audio,
return_last_frame=return_last_frame,
web_search=web_search,
first_frame=first_frame,
last_frame=last_frame,
reference_images=reference_images,
reference_video=reference_video,
reference_audio=reference_audio,
log=log,
)
return run_seedance2_video_from_request(
payload=payload,
poll_interval_s=poll_interval_s,
timeout_s=timeout_s,
log=log,
)
def run_seedance2_video_from_request(
*,
payload: dict[str, Any],
poll_interval_s: float,
timeout_s: int,
log: bool,
) -> Any:
if not isinstance(payload, dict):
raise _validation_error("request payload must be an object.")
payload = _normalize_request_payload(payload)
_log(log, "Creating Seedance 2.0 task...")
start_time = time.time()
api_key = _load_api_key()
task_id, create_response_text = _create_task(api_key, payload)
_log(log, f"createTask response (elapsed={time.time() - start_time:.1f}s): {create_response_text}")
_log(log, f"Task created with ID {task_id}. Polling for completion...")
record_data = _poll_task_until_complete(
api_key,
task_id,
poll_interval_s,
timeout_s,
log,
start_time,
)
result_urls = _extract_result_urls(record_data)
video_url = _select_video_url(result_urls)
if len(result_urls) > 1:
_log(log, f"Seedance 2.0 returned {len(result_urls)} result URLs; selecting video URL {video_url}")
else:
_log(log, f"Final video URL: {video_url}")
video_bytes = _download_video(video_url)
video_output = _video_bytes_to_comfy_video(video_bytes)
_log_remaining_credits(log, record_data, api_key, _log)
return video_output
+222 -1
View File
@@ -29,6 +29,15 @@ from .kie_api.seedream45_edit import (
QUALITY_OPTIONS as SEEDREAM_EDIT_QUALITY_OPTIONS,
run_seedream45_edit,
)
from .kie_api.seedance2_video import (
ASPECT_RATIO_OPTIONS as SEEDANCE2_ASPECT_RATIO_OPTIONS,
DURATION_OPTIONS as SEEDANCE2_DURATION_OPTIONS,
RESOLUTION_OPTIONS as SEEDANCE2_RESOLUTION_OPTIONS,
preflight_seedance2_payload,
summarize_seedance2_payload,
run_seedance2_video,
run_seedance2_video_from_request,
)
from .kie_api.seedancev1pro_fast_i2v import KIE_SeedanceV1Pro_Fast_I2V
from .kie_api.seedance15pro_i2v import KIE_Seedance15Pro_I2V
from .kie_api.kling26_i2v import (
@@ -568,6 +577,214 @@ Outputs:
time.sleep(backoff)
class KIE_Seedance2_Video:
HELP = """
KIE Seedance 2.0 (Video)
Generate a video with Seedance 2.0 using one of four supported scenarios:
- text-to-video
- first-frame image-to-video
- first+last-frame image-to-video
- multimodal reference-to-video
Inputs:
- prompt: Required prompt text
- first_frame / last_frame: Optional frame controls (mutually exclusive with reference media)
- reference_images: Optional reference image batch
- reference_video: Optional reference video
- reference_audio: Optional reference audio
- generate_audio: Ask the endpoint to create audio
- return_last_frame: Ask the endpoint to include the last frame artifact
- aspect_ratio / resolution / duration / web_search
- seedance_data: Optional validated payload from Seedance 2.0 Preflight
- log: Console logging on/off
Rules:
- last_frame requires first_frame
- first_frame/last_frame can be combined with reference media
- current implementation targets verified model `bytedance/seedance-2`
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"multiline": True, "default": ""}),
},
"optional": {
"first_frame": ("IMAGE",),
"last_frame": ("IMAGE",),
"reference_images": ("IMAGE",),
"reference_video": ("VIDEO",),
"reference_audio": ("AUDIO",),
"generate_audio": ("BOOLEAN", {"default": False}),
"return_last_frame": ("BOOLEAN", {"default": False}),
"aspect_ratio": ("COMBO", {"options": SEEDANCE2_ASPECT_RATIO_OPTIONS, "default": "16:9"}),
"resolution": ("COMBO", {"options": SEEDANCE2_RESOLUTION_OPTIONS, "default": "720p"}),
"duration": ("COMBO", {"options": SEEDANCE2_DURATION_OPTIONS, "default": "15"}),
"web_search": ("BOOLEAN", {"default": False}),
"seedance_data": ("KIE_SEEDANCE2_REQUEST",),
"log": ("BOOLEAN", {"default": True}),
},
}
RETURN_TYPES = ("VIDEO",)
RETURN_NAMES = ("video",)
FUNCTION = "generate"
CATEGORY = "kie/api"
def generate(
self,
prompt: str,
first_frame: torch.Tensor | None = None,
last_frame: torch.Tensor | None = None,
reference_images: torch.Tensor | None = None,
reference_video: object | None = None,
reference_audio: object | None = None,
generate_audio: bool = False,
return_last_frame: bool = False,
aspect_ratio: str = "16:9",
resolution: str = "720p",
duration: str = "15",
web_search: bool = False,
seedance_data: dict | None = None,
log: bool = True,
poll_interval_s: float = 10.0,
timeout_s: int = 2000,
):
if seedance_data is not None:
video_output = run_seedance2_video_from_request(
payload=seedance_data,
poll_interval_s=poll_interval_s,
timeout_s=timeout_s,
log=log,
)
return (video_output,)
video_output = run_seedance2_video(
prompt=prompt,
first_frame=first_frame,
last_frame=last_frame,
reference_images=reference_images,
reference_video=reference_video,
reference_audio=reference_audio,
generate_audio=generate_audio,
return_last_frame=return_last_frame,
aspect_ratio=aspect_ratio,
resolution=resolution,
duration=duration,
web_search=web_search,
poll_interval_s=poll_interval_s,
timeout_s=timeout_s,
log=log,
)
return (video_output,)
class KIE_Seedance2_Preflight:
HELP = """
KIE Seedance 2.0 Preflight
Validate Seedance 2.0 inputs, upload media, and build the exact createTask payload
without submitting a generation task.
Use this node before expensive Seedance 2.0 runs.
Outputs:
- KIE_SEEDANCE2_REQUEST: validated payload object for chaining into KIE Seedance 2.0 (Video)
- STRING: payload_json
- STRING: notes
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"multiline": True, "default": ""}),
},
"optional": {
"first_frame": ("IMAGE",),
"last_frame": ("IMAGE",),
"reference_images": ("IMAGE",),
"reference_video": ("VIDEO",),
"reference_audio": ("AUDIO",),
"generate_audio": ("BOOLEAN", {"default": False}),
"return_last_frame": ("BOOLEAN", {"default": False}),
"aspect_ratio": ("COMBO", {"options": SEEDANCE2_ASPECT_RATIO_OPTIONS, "default": "16:9"}),
"resolution": ("COMBO", {"options": SEEDANCE2_RESOLUTION_OPTIONS, "default": "720p"}),
"duration": ("COMBO", {"options": SEEDANCE2_DURATION_OPTIONS, "default": "15"}),
"web_search": ("BOOLEAN", {"default": False}),
"log": ("BOOLEAN", {"default": True}),
},
}
RETURN_TYPES = ("KIE_SEEDANCE2_REQUEST", "STRING", "STRING")
RETURN_NAMES = ("seedance_data", "payload_json", "notes")
FUNCTION = "preflight"
CATEGORY = "kie/helpers"
def preflight(
self,
prompt: str,
first_frame: torch.Tensor | None = None,
last_frame: torch.Tensor | None = None,
reference_images: torch.Tensor | None = None,
reference_video: object | None = None,
reference_audio: object | None = None,
generate_audio: bool = False,
return_last_frame: bool = False,
aspect_ratio: str = "16:9",
resolution: str = "720p",
duration: str = "15",
web_search: bool = False,
log: bool = True,
):
payload = preflight_seedance2_payload(
prompt=prompt,
first_frame=first_frame,
last_frame=last_frame,
reference_images=reference_images,
reference_video=reference_video,
reference_audio=reference_audio,
generate_audio=generate_audio,
return_last_frame=return_last_frame,
aspect_ratio=aspect_ratio,
resolution=resolution,
duration=duration,
web_search=web_search,
log=log,
)
payload_input = payload.get("input", {})
summary = summarize_seedance2_payload(payload)
notes_lines = [
"VALID: Seedance 2.0 preflight checks passed.",
"No createTask call was made.",
f"Scenario: {summary['scenario']}",
f"Aspect ratio: {payload_input.get('aspect_ratio')}",
f"Resolution: {payload_input.get('resolution')}",
f"Duration sent: {payload_input.get('duration')}s",
f"Generate audio: {bool(payload_input.get('generate_audio', False))}",
f"Return last frame: {bool(payload_input.get('return_last_frame', False))}",
f"Web search: {bool(payload_input.get('web_search', False))}",
f"First frame present: {summary['has_first_frame']}",
f"Last frame present: {summary['has_last_frame']}",
"Payload fields: " + ", ".join(summary["fields_present"]),
f"Reference image count: {summary['reference_image_count']}",
f"Reference video count: {summary['reference_video_count']}",
f"Reference audio count: {summary['reference_audio_count']}",
"Prompt alias note: @ImageN / @VideoN / @AudioN mapping is inferred from upload order and is not API-verified.",
]
if summary["reference_image_aliases"]:
notes_lines.append("Reference image aliases: " + ", ".join(summary["reference_image_aliases"]))
if summary["reference_video_aliases"]:
notes_lines.append("Reference video aliases: " + ", ".join(summary["reference_video_aliases"]))
if summary["reference_audio_aliases"]:
notes_lines.append("Reference audio aliases: " + ", ".join(summary["reference_audio_aliases"]))
return (payload, json.dumps(payload, indent=2, ensure_ascii=False), "\n".join(notes_lines))
class KIE_Kling25_I2V_Pro:
HELP = """
KIE Kling 2.5 I2V Pro
@@ -1404,7 +1621,7 @@ Outputs:
"prompt": ("STRING", {"multiline": True, "default": ""}),
},
"optional": {
"multi_shot_text": ("STRING", {"multiline": True, "default": cls.SHOTS_TEXT_PLACEHOLDER}),
"shots_text": ("STRING", {"multiline": True, "default": cls.SHOTS_TEXT_PLACEHOLDER}),
"first_frame": ("IMAGE",),
"last_frame": ("IMAGE",),
"sound": ("BOOLEAN", {"default": True}),
@@ -2053,6 +2270,8 @@ NODE_CLASS_MAPPINGS = {
"KIE_GrokImagine_I2I": KIE_GrokImagine_I2I,
"KIE_SeedanceV1Pro_Fast_I2V": KIE_SeedanceV1Pro_Fast_I2V,
"KIE_Seedance15Pro_I2V": KIE_Seedance15Pro_I2V,
"KIE_Seedance2_Video": KIE_Seedance2_Video,
"KIE_Seedance2_Preflight": KIE_Seedance2_Preflight,
"KIE_Kling25_I2V_Pro": KIE_Kling25_I2V_Pro,
"KIE_Kling26_I2V": KIE_Kling26_I2V,
"KIE_Kling26_T2V": KIE_Kling26_T2V,
@@ -2082,6 +2301,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"KIE_GrokImagine_I2I": "KIE Grok Imagine (I2I)",
"KIE_SeedanceV1Pro_Fast_I2V": "KIE Seedance V1 Pro Fast (I2V)",
"KIE_Seedance15Pro_I2V": "KIE Seedance 1.5 Pro (I2V/T2V)",
"KIE_Seedance2_Video": "KIE Seedance 2.0 (Video)",
"KIE_Seedance2_Preflight": "KIE Seedance 2.0 Preflight",
"KIE_Kling25_I2V_Pro": "KIE Kling 2.5 I2V Pro",
"KIE_Kling26_I2V": "KIE Kling 2.6 (I2V)",
"KIE_Kling26_T2V": "KIE Kling 2.6 (T2V)",
+31
View File
@@ -0,0 +1,31 @@
# KIE Seedance 2.0 Preflight
Validate Seedance 2.0 inputs, upload required media, and build the exact createTask payload without submitting a generation task.
> Status: Experimental support node for Seedance 2.0 development.
## Purpose
- Catch scenario wiring errors before expensive video runs.
- Preview the exact payload sent to:
- `POST https://api.kie.ai/api/v1/jobs/createTask`
## Inputs
- Same inputs as `KIE Seedance 2.0 (Video)`, except there is no `seedance_data` passthrough input.
## Outputs
- `seedance_data` (KIE_SEEDANCE2_REQUEST): validated payload object for direct chaining into `KIE Seedance 2.0 (Video)`.
- `payload_json` (STRING): exact request payload after validation and upload URL resolution.
- `notes` (STRING): validation summary with resolved scenario and media counts.
## Notes
- This node still uploads media so the payload contains real URLs.
- It does not submit `createTask`.
- Current implementation is intentionally scoped to `bytedance/seedance-2`.
- It reports exact payload fields and inferred alias order for references:
- `@Image1..N` for `reference_image_urls`
- `@Video1..N` for `reference_video_urls`
- `@Audio1..N` for `reference_audio_urls`
- Those aliases are based on upload order and are not yet a verified KIE API contract.
- The current `aspect_ratio`, `resolution`, and `duration` options are pinned to the public examples visible in KIE's docs and should be widened only after live API verification.
- KIE's public marketing copy mentions optional lens locking, but the visible request example does not expose a verified request field for it yet.
- The official `Seedance 2.0 Fast` page is not used yet because its published request example is still inconsistent.
+51
View File
@@ -0,0 +1,51 @@
# KIE Seedance 2.0 (Video)
Generate Seedance 2.0 videos with the verified `bytedance/seedance-2` model.
> Status: Experimental development pass.
> Credit Warning: Seedance 2.0 can take several minutes per job. Use `KIE Seedance 2.0 Preflight` first.
## Inputs
- `prompt` (STRING, required): main prompt text.
- `first_frame` (IMAGE, optional): single first frame image.
- `last_frame` (IMAGE, optional): single last frame image. Requires `first_frame`.
- `reference_images` (IMAGE, optional): one or more reference images for multimodal reference mode.
- `reference_video` (VIDEO, optional): one reference video for multimodal reference mode.
- `reference_audio` (AUDIO, optional): one reference audio input for multimodal reference mode.
- `generate_audio` (BOOLEAN, optional): asks KIE to generate audio.
- `return_last_frame` (BOOLEAN, optional): asks KIE to include a last-frame artifact when supported.
- `aspect_ratio` (COMBO, optional): `16:9`, `9:16`, `1:1`.
- `resolution` (COMBO, optional): `480p`, `720p`, `1080p`.
- `duration` (COMBO, optional): `5`, `10`, `15`.
- `web_search` (BOOLEAN, optional): forwards KIE's web-search toggle.
- `seedance_data` (KIE_SEEDANCE2_REQUEST, optional): validated payload from preflight. When connected, this overrides direct field inputs.
- `log` (BOOLEAN, optional): enable helper logging.
## Scenario Rules
- Text-to-video:
Connect no media inputs.
- First-frame image-to-video:
Connect `first_frame` only.
- First+last-frame image-to-video:
Connect `first_frame` and `last_frame`.
- Multimodal reference-to-video:
Connect any mix of `reference_images`, `reference_video`, and `reference_audio`.
- Hybrid control mode:
You can combine `first_frame` and `last_frame` with reference media in the same request.
- `last_frame` still requires `first_frame`.
## Notes
- The current `aspect_ratio`, `resolution`, and `duration` option lists are pinned to the public KIE examples used for this implementation pass.
Widen them only after Windows-side live API verification confirms the accepted enum set.
- Preflight now reports the exact payload fields present plus inferred alias order such as `@Image1`, `@Video1`, and `@Audio1`.
Those aliases are treated as a likely model convention based on upload order, not a verified API contract.
- The official KIE docs currently show a typo with `reference_video_urls ` including a trailing space in the example payload.
This node normalizes that field when consuming prebuilt payloads.
- KIE's public marketing copy mentions optional lens locking, but the visible request example does not expose a verified request field for it yet.
This first pass intentionally leaves that transport field out rather than guessing.
- The official `Seedance 2.0 Fast` page is currently inconsistent and still shows `model: "bytedance/seedance-2"`.
This node intentionally targets only the verified non-fast model for now.
- If KIE returns multiple artifacts, the node selects the video URL and ignores extra artifacts in the ComfyUI output contract.
## Output
- `video` (VIDEO): SaveVideo-compatible ComfyUI video output.
+2
View File
@@ -22,6 +22,8 @@ Grok Imagine status: `T2I`, `I2I`, `T2V`, and `I2V` are implemented and document
- Grok Imagine I2V: [`KIE_GrokImagine_I2V.md`](KIE_GrokImagine_I2V.md)
- Seedance V1 Pro Fast I2V: [`KIE_SeedanceV1Pro_Fast_I2V.md`](KIE_SeedanceV1Pro_Fast_I2V.md)
- Seedance 1.5 Pro I2V/T2V: [`KIE_Seedance15Pro_I2V.md`](KIE_Seedance15Pro_I2V.md)
- Seedance 2.0 Video: [`KIE_Seedance2_Video.md`](KIE_Seedance2_Video.md)
- Seedance 2.0 Preflight: [`KIE_Seedance2_Preflight.md`](KIE_Seedance2_Preflight.md)
## API Specs
- Grok Imagine T2V spec: [`KIE_GrokImagine_T2V_Spec.md`](KIE_GrokImagine_T2V_Spec.md)