Skip to content

Commit 4d360f9

Browse files
authored
[Partner Nodes] fix (Seedance 2.0): prevent 1080p first/last-frame stretch jump (Comfy-Org#14251)
Signed-off-by: bigcat88 <bigcat88@icloud.com>
1 parent 4f99ce0 commit 4d360f9

1 file changed

Lines changed: 62 additions & 5 deletions

File tree

comfy_api_nodes/nodes_bytedance.py

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import torch
88
from typing_extensions import override
99

10+
from comfy.utils import common_upscale
1011
from comfy_api.latest import IO, ComfyExtension, Input, Types
1112
from comfy_api_nodes.apis.bytedance import (
1213
RECOMMENDED_PRESETS,
@@ -131,6 +132,44 @@ def _prepare_seedance_image(image: Input.Image) -> Input.Image:
131132
return image
132133

133134

135+
# Supported output aspect ratios, used to pre-size FLF frames to matching pixel pair to avoid the 1080p stretch jump.
136+
SEEDANCE2_RATIO_WH = {
137+
"16:9": (16, 9),
138+
"4:3": (4, 3),
139+
"1:1": (1, 1),
140+
"3:4": (3, 4),
141+
"9:16": (9, 16),
142+
"21:9": (21, 9),
143+
}
144+
SEEDANCE2_RES_SHORT_SIDE = {"480p": 480, "720p": 720, "1080p": 1080}
145+
146+
147+
def _seedance2_target_dims(resolution: str, ratio: str, image: torch.Tensor) -> tuple[int, int]:
148+
"""Exact supported output (width, height) for (resolution, ratio).
149+
150+
The shorter side equals the resolution number (e.g. 1080p 16:9 -> 1920x1080). For ratio
151+
"adaptive" (or any unexpected value) the ratio is derived from the image's own aspect, snapped
152+
to the nearest supported ratio, so the output keeps the frame's orientation.
153+
"""
154+
short = SEEDANCE2_RES_SHORT_SIDE[resolution]
155+
if ratio not in SEEDANCE2_RATIO_WH:
156+
aspect = image.shape[-2] / image.shape[-3] # W / H; tensor is (B, H, W, C)
157+
ratio = min(SEEDANCE2_RATIO_WH, key=lambda k: abs(SEEDANCE2_RATIO_WH[k][0] / SEEDANCE2_RATIO_WH[k][1] - aspect))
158+
rw, rh = SEEDANCE2_RATIO_WH[ratio]
159+
if rw >= rh: # landscape or square: shorter side is the height
160+
out_w, out_h = round(short * rw / rh), short
161+
else: # portrait: shorter side is the width
162+
out_w, out_h = short, round(short * rh / rw)
163+
return out_w - out_w % 2, out_h - out_h % 2
164+
165+
166+
def _resize_to_exact(image: torch.Tensor, width: int, height: int) -> torch.Tensor:
167+
"""Center-crop to the target aspect and resize to exactly width x height (lanczos)."""
168+
samples = image.movedim(-1, 1) # (B, H, W, C) -> (B, C, H, W)
169+
resized = common_upscale(samples, width, height, "lanczos", "center")
170+
return resized.movedim(1, -1)
171+
172+
134173
async def _resolve_reference_assets(
135174
cls: type[IO.ComfyNode],
136175
asset_ids: list[str],
@@ -1790,10 +1829,28 @@ async def execute(
17901829
if last_frame is not None and last_frame_asset_id:
17911830
raise ValueError("Provide only one of last_frame or last_frame_asset_id, not both.")
17921831

1793-
if first_frame is not None:
1794-
first_frame = _prepare_seedance_image(first_frame)
1795-
if last_frame is not None:
1796-
last_frame = _prepare_seedance_image(last_frame)
1832+
request_ratio = model["ratio"]
1833+
if first_frame_asset_id or last_frame_asset_id:
1834+
if first_frame is not None:
1835+
first_frame = _prepare_seedance_image(first_frame)
1836+
if last_frame is not None:
1837+
last_frame = _prepare_seedance_image(last_frame)
1838+
else:
1839+
# The 1080p FLF stretch fix (pre-size frames to a supported pixel pair + submit ratio="adaptive")
1840+
# only applies to local image inputs we can resize.
1841+
request_ratio = "adaptive"
1842+
target_dims: tuple[int, int] | None = None
1843+
if first_frame is not None:
1844+
validate_image_aspect_ratio(first_frame, (2, 5), (5, 2), strict=False) # 0.4 to 2.5
1845+
validate_image_dimensions(first_frame, min_width=300, min_height=300)
1846+
target_dims = _seedance2_target_dims(model["resolution"], model["ratio"], first_frame)
1847+
first_frame = _resize_to_exact(first_frame, *target_dims)
1848+
if last_frame is not None:
1849+
validate_image_aspect_ratio(last_frame, (2, 5), (5, 2), strict=False) # 0.4 to 2.5
1850+
validate_image_dimensions(last_frame, min_width=300, min_height=300)
1851+
if target_dims is None:
1852+
target_dims = _seedance2_target_dims(model["resolution"], model["ratio"], last_frame)
1853+
last_frame = _resize_to_exact(last_frame, *target_dims)
17971854

17981855
asset_ids_to_resolve = [a for a in (first_frame_asset_id, last_frame_asset_id) if a]
17991856
image_assets: dict[str, str] = {}
@@ -1844,7 +1901,7 @@ async def execute(
18441901
content=content,
18451902
generate_audio=model["generate_audio"],
18461903
resolution=model["resolution"],
1847-
ratio=model["ratio"],
1904+
ratio=request_ratio,
18481905
duration=model["duration"],
18491906
seed=seed,
18501907
watermark=watermark,

0 commit comments

Comments
 (0)