Add curated Seedance 2.5 video editing node
This commit is contained in:
@@ -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)",
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user