Add curated Seedance 2.5 video editing node

This commit is contained in:
Jodh Singh
2026-09-23 12:51:51 -07:00
parent 4a6c7b934e
commit 28a045b72c
2 changed files with 242 additions and 0 deletions
+115
View File
@@ -2787,6 +2787,119 @@ class SeedanceProImageToVideoNode:
)
class Seedance25VideoToVideoNode:
"""Curated Seedance 2.5 video editing node."""
ENDPOINT = "bytedance/seedance-2.5/reference-to-video"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"video": (
"VIDEO",
{
"tooltip": "Source video to edit. URL-backed VIDEO inputs are passed through without re-uploading.",
},
),
"prompt": (
"STRING",
{
"default": "",
"multiline": True,
"tooltip": "Describe the edits to apply to the source video.",
},
),
},
"optional": {
"resolution": (
["480p", "720p", "1080p"],
{"default": "720p", "tooltip": "Output video resolution."},
),
"generate_audio": (
"BOOLEAN",
{
"default": True,
"tooltip": "Generate synchronized audio for the edited video.",
},
),
"bitrate_mode": (
["standard", "high"],
{
"default": "standard",
"tooltip": "Use the standard bitrate or request a larger, higher-quality encode.",
},
),
"seed": (
"INT",
{
"default": -1,
"min": -1,
"max": 2147483647,
"tooltip": "Random seed for reproducible results; -1 lets the API choose.",
},
),
"force_rerun": (
"BOOLEAN",
{
"default": False,
"tooltip": "Bypass the persistent result cache and submit a new fal request.",
},
),
},
}
RETURN_TYPES = ("VIDEO", "STRING")
RETURN_NAMES = ("video", "video_url")
FUNCTION = "edit_video"
CATEGORY = "FAL/VideoGeneration"
DESCRIPTION = (
"Edit one video with Seedance 2.5. The endpoint is always called with "
"task='editing' and the source as a single video_urls entry."
)
@classmethod
def IS_CHANGED(cls, force_rerun=False, **_kwargs):
if force_rerun:
return float("nan")
return False
def edit_video(
self,
video,
prompt,
resolution="720p",
generate_audio=True,
bitrate_mode="standard",
seed=-1,
force_rerun=False,
):
try:
uploaded_url = MediaUtils.upload_video(video)
arguments = {
"prompt": prompt,
"task": "editing",
"video_urls": [uploaded_url],
"resolution": resolution,
"generate_audio": generate_audio,
"bitrate_mode": bitrate_mode,
}
if seed != -1:
arguments["seed"] = seed
result = ApiHandler.submit_and_get_result(
self.ENDPOINT,
arguments,
skip_cache=bool(force_rerun),
)
video_url = MediaUtils.require_http_url(
result["video"]["url"], self.ENDPOINT
)
return (MediaUtils.video_from_url(video_url), video_url)
except Exception as e:
return ApiHandler.handle_video_generation_error(self.ENDPOINT, e)
class Veo3Node:
@classmethod
def INPUT_TYPES(cls):
@@ -3733,6 +3846,7 @@ NODE_CLASS_MAPPINGS = {
"DYWanFun22_fal": DYWanFun22Node,
"DYWanUpscaler_fal": DYWanUpscalerNode,
"SeedanceImageToVideo_fal": SeedanceImageToVideoNode,
"Seedance25VideoToVideo_fal": Seedance25VideoToVideoNode,
"SeedanceProImageToVideo_fal": SeedanceProImageToVideoNode,
"SeedanceTextToVideo_fal": SeedanceTextToVideoNode,
"Veo3_fal": Veo3Node,
@@ -3779,6 +3893,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"Veo2ImageToVideo_fal": "Google Veo2 Image-to-Video (fal)",
"WanPro_fal": "Wan Pro Image-to-Video (fal)",
"SeedanceImageToVideo_fal": "Seedance Image-to-Video (fal)",
"Seedance25VideoToVideo_fal": "Seedance 2.5 Video-to-Video (fal)",
"SeedanceProImageToVideo_fal": "Seedance Pro Image-to-Video (fal)",
"SeedanceTextToVideo_fal": "Seedance Text-to-Video (fal)",
"Veo3_fal": "Veo3 Video Generation (fal)",
+127
View File
@@ -0,0 +1,127 @@
"""Focused tests for the curated Seedance 2.5 video editing node."""
from __future__ import annotations
import math
import sys
class _URLVideo:
def __init__(self, url: str):
self.url = url
def get_stream_source(self):
return self.url
def _node_and_module(pack):
node_cls = pack.NODE_CLASS_MAPPINGS["Seedance25VideoToVideo_fal"]
return node_cls, sys.modules[node_cls.__module__]
def test_seedance25_video_to_video_schema_and_registration(pack):
node_cls, _module = _node_and_module(pack)
inputs = node_cls.INPUT_TYPES()
assert inputs["required"]["video"][0] == "VIDEO"
assert inputs["required"]["prompt"][0] == "STRING"
assert set(inputs["optional"]) == {
"resolution",
"generate_audio",
"bitrate_mode",
"seed",
"force_rerun",
}
assert node_cls.RETURN_TYPES == ("VIDEO", "STRING")
assert node_cls.RETURN_NAMES == ("video", "video_url")
assert pack.NODE_DISPLAY_NAME_MAPPINGS["Seedance25VideoToVideo_fal"] == (
"Seedance 2.5 Video-to-Video (fal)"
)
def test_seedance25_edit_payload_outputs_and_force_rerun(pack, monkeypatch):
node_cls, module = _node_and_module(pack)
submitted = {}
native_output = object()
monkeypatch.setattr(
module.MediaUtils,
"upload_video",
staticmethod(lambda _video: "https://fal.media/input.mp4"),
)
monkeypatch.setattr(
module.MediaUtils,
"video_from_url",
staticmethod(lambda url: native_output if url.endswith("output.mp4") else None),
)
def submit(endpoint, arguments, *, skip_cache=False):
submitted.update(
endpoint=endpoint, arguments=arguments, skip_cache=skip_cache
)
return {"video": {"url": "https://fal.media/output.mp4"}}
monkeypatch.setattr(
module.ApiHandler, "submit_and_get_result", staticmethod(submit)
)
result = node_cls().edit_video(
object(),
"Turn the daytime scene into night",
resolution="1080p",
generate_audio=False,
bitrate_mode="high",
seed=123,
force_rerun=True,
)
assert submitted == {
"endpoint": "bytedance/seedance-2.5/reference-to-video",
"arguments": {
"prompt": "Turn the daytime scene into night",
"task": "editing",
"video_urls": ["https://fal.media/input.mp4"],
"resolution": "1080p",
"generate_audio": False,
"bitrate_mode": "high",
"seed": 123,
},
"skip_cache": True,
}
assert result == (native_output, "https://fal.media/output.mp4")
assert math.isnan(node_cls.IS_CHANGED(force_rerun=True))
def test_seedance25_url_backed_video_passes_through_without_upload(
pack, monkeypatch
):
node_cls, module = _node_and_module(pack)
source_url = "https://fal.media/already-uploaded.mp4"
captured = {}
def unexpected_upload(_value):
raise AssertionError("URL-backed VIDEO input must not be re-uploaded")
monkeypatch.setattr(
module.ImageUtils, "upload_file", staticmethod(unexpected_upload)
)
monkeypatch.setattr(
module.MediaUtils,
"video_from_url",
staticmethod(lambda _url: object()),
)
def submit(_endpoint, arguments, *, skip_cache=False):
captured.update(arguments)
assert skip_cache is False
return {"video": {"url": "https://fal.media/output.mp4"}}
monkeypatch.setattr(
module.ApiHandler, "submit_and_get_result", staticmethod(submit)
)
node_cls().edit_video(_URLVideo(source_url), "Restyle as watercolor")
assert captured["video_urls"] == [source_url]
assert captured["task"] == "editing"
assert "seed" not in captured