[Partner Nodes] add widget for automatic upscaling for the ByteDance2Reference node (#14032)

Signed-off-by: bigcat88 <bigcat88@icloud.com>
This commit is contained in:
Alexander Piskun 2026-05-21 21:58:03 +03:00 committed by GitHub
parent 2ca1480f91
commit b293f8cefd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 66 additions and 13 deletions

View File

@ -43,15 +43,16 @@ from comfy_api_nodes.util import (
ApiEndpoint, ApiEndpoint,
download_url_to_image_tensor, download_url_to_image_tensor,
download_url_to_video_output, download_url_to_video_output,
downscale_video_to_max_pixels,
get_number_of_images, get_number_of_images,
image_tensor_pair_to_batch, image_tensor_pair_to_batch,
poll_op, poll_op,
resize_video_to_pixel_budget,
sync_op, sync_op,
upload_audio_to_comfyapi, upload_audio_to_comfyapi,
upload_image_to_comfyapi, upload_image_to_comfyapi,
upload_images_to_comfyapi, upload_images_to_comfyapi,
upload_video_to_comfyapi, upload_video_to_comfyapi,
upscale_video_to_min_pixels,
validate_image_aspect_ratio, validate_image_aspect_ratio,
validate_image_dimensions, validate_image_dimensions,
validate_string, validate_string,
@ -110,12 +111,13 @@ def _validate_ref_video_pixels(video: Input.Video, model_id: str, resolution: st
max_px = limits.get("max") max_px = limits.get("max")
if min_px and pixels < min_px: if min_px and pixels < min_px:
raise ValueError( raise ValueError(
f"Reference video {index} is too small: {w}x{h} = {pixels:,}px. " f"Minimum is {min_px:,}px for this model." f"Reference video {index} is too small: {w}x{h} = {pixels:,} total pixels. "
f"Minimum for this model is {min_px:,} total pixels."
) )
if max_px and pixels > max_px: if max_px and pixels > max_px:
raise ValueError( raise ValueError(
f"Reference video {index} is too large: {w}x{h} = {pixels:,}px. " f"Reference video {index} is too large: {w}x{h} = {pixels:,} total pixels. "
f"Maximum is {max_px:,}px for this model. Try downscaling the video." f"Maximum for this model is {max_px:,} total pixels. Try downscaling the video."
) )
@ -1676,14 +1678,14 @@ class ByteDance2FirstLastFrameNode(IO.ComfyNode):
"first_frame_asset_id", "first_frame_asset_id",
default="", default="",
tooltip="Seedance asset_id to use as the first frame. " tooltip="Seedance asset_id to use as the first frame. "
"Mutually exclusive with the first_frame image input.", "Mutually exclusive with the first_frame image input.",
optional=True, optional=True,
), ),
IO.String.Input( IO.String.Input(
"last_frame_asset_id", "last_frame_asset_id",
default="", default="",
tooltip="Seedance asset_id to use as the last frame. " tooltip="Seedance asset_id to use as the last frame. "
"Mutually exclusive with the last_frame image input.", "Mutually exclusive with the last_frame image input.",
optional=True, optional=True,
), ),
IO.Int.Input( IO.Int.Input(
@ -1865,11 +1867,20 @@ def _seedance2_reference_inputs(resolutions: list[str], default_ratio: str = "16
IO.Boolean.Input( IO.Boolean.Input(
"auto_downscale", "auto_downscale",
default=False, default=False,
advanced=True,
optional=True, optional=True,
tooltip="Automatically downscale reference videos that exceed the model's pixel budget " tooltip="Automatically downscale reference videos that exceed the model's pixel budget "
"for the selected resolution. Aspect ratio is preserved; videos already within limits are untouched.", "for the selected resolution. Aspect ratio is preserved; videos already within limits are untouched.",
), ),
IO.Boolean.Input(
"auto_upscale",
default=False,
advanced=True,
optional=True,
tooltip="Automatically upscale reference videos that are below the model's minimum pixel count "
"for the selected resolution. Aspect ratio is preserved; videos already meeting the minimum are "
"untouched. Note: upscaling a low-resolution source does not add real detail and may produce "
"lower-quality generations.",
),
IO.Autogrow.Input( IO.Autogrow.Input(
"reference_assets", "reference_assets",
template=IO.Autogrow.TemplateNames( template=IO.Autogrow.TemplateNames(
@ -2030,7 +2041,13 @@ class ByteDance2ReferenceNode(IO.ComfyNode):
max_px = SEEDANCE2_REF_VIDEO_PIXEL_LIMITS.get(model_id, {}).get(model["resolution"], {}).get("max") max_px = SEEDANCE2_REF_VIDEO_PIXEL_LIMITS.get(model_id, {}).get(model["resolution"], {}).get("max")
if max_px: if max_px:
for key in reference_videos: for key in reference_videos:
reference_videos[key] = resize_video_to_pixel_budget(reference_videos[key], max_px) reference_videos[key] = downscale_video_to_max_pixels(reference_videos[key], max_px)
if model.get("auto_upscale") and reference_videos:
min_px = SEEDANCE2_REF_VIDEO_PIXEL_LIMITS.get(model_id, {}).get(model["resolution"], {}).get("min")
if min_px:
for key in reference_videos:
reference_videos[key] = upscale_video_to_min_pixels(reference_videos[key], min_px)
total_video_duration = 0.0 total_video_duration = 0.0
for i, key in enumerate(reference_videos, 1): for i, key in enumerate(reference_videos, 1):

View File

@ -16,16 +16,17 @@ from .conversions import (
convert_mask_to_image, convert_mask_to_image,
downscale_image_tensor, downscale_image_tensor,
downscale_image_tensor_by_max_side, downscale_image_tensor_by_max_side,
downscale_video_to_max_pixels,
image_tensor_pair_to_batch, image_tensor_pair_to_batch,
pil_to_bytesio, pil_to_bytesio,
resize_mask_to_image, resize_mask_to_image,
resize_video_to_pixel_budget,
tensor_to_base64_string, tensor_to_base64_string,
tensor_to_bytesio, tensor_to_bytesio,
tensor_to_pil, tensor_to_pil,
text_filepath_to_base64_string, text_filepath_to_base64_string,
text_filepath_to_data_uri, text_filepath_to_data_uri,
trim_video, trim_video,
upscale_video_to_min_pixels,
video_to_base64_string, video_to_base64_string,
) )
from .download_helpers import ( from .download_helpers import (
@ -88,16 +89,17 @@ __all__ = [
"convert_mask_to_image", "convert_mask_to_image",
"downscale_image_tensor", "downscale_image_tensor",
"downscale_image_tensor_by_max_side", "downscale_image_tensor_by_max_side",
"downscale_video_to_max_pixels",
"image_tensor_pair_to_batch", "image_tensor_pair_to_batch",
"pil_to_bytesio", "pil_to_bytesio",
"resize_mask_to_image", "resize_mask_to_image",
"resize_video_to_pixel_budget",
"tensor_to_base64_string", "tensor_to_base64_string",
"tensor_to_bytesio", "tensor_to_bytesio",
"tensor_to_pil", "tensor_to_pil",
"text_filepath_to_base64_string", "text_filepath_to_base64_string",
"text_filepath_to_data_uri", "text_filepath_to_data_uri",
"trim_video", "trim_video",
"upscale_video_to_min_pixels",
"video_to_base64_string", "video_to_base64_string",
# Validation utilities # Validation utilities
"get_image_dimensions", "get_image_dimensions",

View File

@ -415,14 +415,48 @@ def trim_video(video: Input.Video, duration_sec: float) -> Input.Video:
raise RuntimeError(f"Failed to trim video: {str(e)}") from e raise RuntimeError(f"Failed to trim video: {str(e)}") from e
def resize_video_to_pixel_budget(video: Input.Video, total_pixels: int) -> Input.Video: def downscale_video_to_max_pixels(video: Input.Video, max_pixels: int) -> Input.Video:
"""Downscale a video to fit within ``total_pixels`` (w * h), preserving aspect ratio. """Downscale a video to fit within ``max_pixels`` (w * h), preserving aspect ratio.
Returns the original video object untouched when it already fits. Preserves frame rate, duration, and audio. Returns the original video object untouched when it already fits. Preserves frame rate, duration, and audio.
Aspect ratio is preserved up to a fraction of a percent (even-dim rounding). Aspect ratio is preserved up to a fraction of a percent (even-dim rounding).
""" """
src_w, src_h = video.get_dimensions() src_w, src_h = video.get_dimensions()
scale_dims = _compute_downscale_dims(src_w, src_h, total_pixels) scale_dims = _compute_downscale_dims(src_w, src_h, max_pixels)
if scale_dims is None:
return video
return _apply_video_scale(video, scale_dims)
def _compute_upscale_dims(src_w: int, src_h: int, total_pixels: int) -> tuple[int, int] | None:
"""Return upscaled (w, h) with even dims meeting at least ``total_pixels``, or None if already large enough.
Source aspect ratio is preserved; output may drift by a fraction of a percent because both dimensions
are rounded up to even values (many codecs require divisible-by-2). The result is guaranteed to be at
least ``total_pixels``.
"""
pixels = src_w * src_h
if pixels >= total_pixels:
return None
scale = math.sqrt(total_pixels / pixels)
new_w = math.ceil(src_w * scale)
new_h = math.ceil(src_h * scale)
if new_w % 2:
new_w += 1
if new_h % 2:
new_h += 1
return new_w, new_h
def upscale_video_to_min_pixels(video: Input.Video, min_pixels: int) -> Input.Video:
"""Upscale a video to meet at least ``min_pixels`` (w * h), preserving aspect ratio.
Returns the original video object untouched when it already meets the minimum. Preserves frame rate,
duration, and audio. Aspect ratio is preserved up to a fraction of a percent (even-dim rounding).
Note: upscaling a low-resolution source does not add real detail; downstream model quality may suffer.
"""
src_w, src_h = video.get_dimensions()
scale_dims = _compute_upscale_dims(src_w, src_h, min_pixels)
if scale_dims is None: if scale_dims is None:
return video return video
return _apply_video_scale(video, scale_dims) return _apply_video_scale(video, scale_dims)