[Partner Nodes] feat(BFL): add Flux Video Upscale node (#15753)

Signed-off-by: Alexander Piskun <bigcat88@icloud.com>
(cherry picked from commit d3363657c2)
This commit is contained in:
Alexander Piskun
2026-08-20 20:22:49 +04:00
committed by Purz
parent db387a6f4e
commit 6aa1def245
2 changed files with 157 additions and 11 deletions

View File

@@ -166,3 +166,13 @@ class Flux3VideoContinuationRequest(Flux3VideoRequest):
start_video: str = Field(
..., description="MP4 (URL or base64); the new clip carries on from its final frames."
)
class BFLFluxVideoUpscaleRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
input_video: str = Field(..., description="MP4 (URL or base64), 1 to 20 seconds.")
upscale_factor: float = Field(2.0, ge=1.5, le=3.0)
creativity: int = Field(1, description="0 preserves the source precisely, 1 enhances detail.")
prompt: str | None = Field(None)
safety_tolerance: int = Field(2, ge=0, le=4)

View File

@@ -1,6 +1,7 @@
import math
import torch
from pydantic import BaseModel
from typing_extensions import override
from comfy_api.latest import IO, ComfyExtension, Input
@@ -12,18 +13,19 @@ from comfy_api_nodes.apis.bfl import (
BFLFluxProGenerateResponse,
BFLFluxProUltraGenerateRequest,
BFLFluxStatusResponse,
BFLFluxVideoUpscaleRequest,
BFLFluxVTORequest,
BFLStatus,
Flux2ProGenerateRequest,
Flux3ImageToVideoRequest,
Flux3TextToVideoRequest,
Flux3VideoContinuationRequest,
Flux3VideoRequest,
)
from comfy_api_nodes.util import (
ApiEndpoint,
convert_mask_to_image,
download_url_to_image_tensor,
downscale_video_to_max_pixels,
download_url_to_video_output,
get_number_of_images,
poll_op,
@@ -35,6 +37,8 @@ from comfy_api_nodes.util import (
validate_aspect_ratio_string,
validate_image_dimensions,
validate_string,
validate_video_dimensions,
validate_video_duration,
)
@@ -1147,16 +1151,23 @@ class Flux3VideoNodeBase(IO.ComfyNode):
)
async def _flux3_execute(cls: type[IO.ComfyNode], request: Flux3VideoRequest) -> IO.NodeOutput:
initial_response = await sync_op(
cls,
ApiEndpoint(path="/proxy/bfl/v1/flux-3-video", method="POST"),
response_model=BFLFluxProGenerateResponse,
data=request,
_FLUX3_VIDEO_ENDPOINT = ApiEndpoint(path="/proxy/bfl/v1/flux-3-video", method="POST")
_FLUX_VIDEO_UPSCALE_ENDPOINT = ApiEndpoint(path="/proxy/bfl/v1/flux-tools/video-upscale-v1", method="POST")
_BFL_POLL_PROXY_PATH = "/proxy/bfl/v1/get_result"
async def _bfl_video_execute(
cls: type[IO.ComfyNode], endpoint: ApiEndpoint, request: BaseModel, poll_via_proxy: bool = False
) -> IO.NodeOutput:
initial_response = await sync_op(cls, endpoint, response_model=BFLFluxProGenerateResponse, data=request)
poll_endpoint = (
ApiEndpoint(path=_BFL_POLL_PROXY_PATH, query_params={"polling_url": initial_response.polling_url})
if poll_via_proxy
else ApiEndpoint(initial_response.polling_url)
)
response = await poll_op(
cls,
ApiEndpoint(initial_response.polling_url),
poll_endpoint,
response_model=BFLFluxStatusResponse,
status_extractor=lambda r: r.status,
progress_extractor=lambda r: r.progress,
@@ -1221,7 +1232,7 @@ class Flux3TextToVideoNode(Flux3VideoNodeBase):
request = Flux3TextToVideoRequest(
**cls.common_fields(prompt, aspect_ratio, duration, resolution, generate_audio, safety_tolerance)
)
return await _flux3_execute(cls, request)
return await _bfl_video_execute(cls, _FLUX3_VIDEO_ENDPOINT, request)
class Flux3ImageToVideoNode(Flux3VideoNodeBase):
@@ -1319,7 +1330,7 @@ class Flux3ImageToVideoNode(Flux3VideoNodeBase):
keyframes=list(zip(times, urls)) if times is not None else urls,
**fields,
)
return await _flux3_execute(cls, request)
return await _bfl_video_execute(cls, _FLUX3_VIDEO_ENDPOINT, request)
class Flux3VideoContinuationNode(Flux3VideoNodeBase):
@@ -1370,7 +1381,131 @@ class Flux3VideoContinuationNode(Flux3VideoNodeBase):
fields = cls.common_fields(prompt, aspect_ratio, duration, resolution, generate_audio, safety_tolerance)
url = await upload_video_to_comfyapi(cls, video, wait_label="Uploading source video")
request = Flux3VideoContinuationRequest(start_video=url, **fields)
return await _flux3_execute(cls, request)
return await _bfl_video_execute(cls, _FLUX3_VIDEO_ENDPOINT, request)
_FLUX_VIDEO_UPSCALE_MODES = {"creative": 1, "precise": 0}
_FLUX_VIDEO_UPSCALE_MAX_INPUT_PIXELS = 3840 * 2160
_FLUX_VIDEO_UPSCALE_MAX_ASPECT_RATIO = 4.0
class FluxVideoUpscaleNode(IO.ComfyNode):
@classmethod
def define_schema(cls) -> IO.Schema:
return IO.Schema(
node_id="FluxVideoUpscaleNode",
display_name="Flux Video Upscale",
category="partner/video/BFL",
description="Upscales a video 1.5 to 3 times with FLUX super-resolution, either preserving "
"the source precisely or creatively enhancing its detail.",
inputs=[
IO.Video.Input(
"video",
tooltip="Source clip of 1 to 20 seconds with an aspect ratio between 1:4 and 4:1. "
"The output is rendered at 24 fps and capped at about 14.4 megapixels per frame.",
),
IO.Float.Input(
"upscale_factor",
default=2.0,
min=1.5,
max=3.0,
step=0.1,
tooltip="Output size relative to the source. Very large sources are upscaled by "
"less than the requested factor because of the per-frame cap.",
),
IO.Combo.Input(
"mode",
options=list(_FLUX_VIDEO_UPSCALE_MODES),
default="creative",
tooltip="'creative' restores and invents fine detail, best for generated footage, "
"textures and scenery. 'precise' sharpens the source without changing it, "
"for faces, products and real footage.",
),
IO.String.Input(
"prompt",
multiline=True,
default="",
tooltip="Optional description of the clip that steers the enhanced detail. "
"Leave empty for a neutral upscale.",
),
IO.Boolean.Input(
"auto_downscale",
default=True,
tooltip="Automatically downscale sources larger than 3840x2160 pixels in area to fit "
"the input limit. Aspect ratio is preserved; smaller videos are untouched.",
),
IO.Int.Input(
"safety_tolerance",
default=2,
min=0,
max=4,
advanced=True,
tooltip="Moderation tolerance, 0 is the strictest.",
),
IO.Int.Input(
"seed",
default=42,
min=0,
max=0xFFFFFFFF,
control_after_generate=True,
tooltip="Seed to determine if node should re-run; FLUX picks its own seed, so "
"actual results are nondeterministic regardless of this value.",
),
],
outputs=[IO.Video.Output()],
hidden=[
IO.Hidden.auth_token_comfy_org,
IO.Hidden.api_key_comfy_org,
IO.Hidden.unique_id,
],
is_api_node=True,
price_badge=IO.PriceBadge(
depends_on=IO.PriceBadgeDepends(widgets=["mode"]),
expr="""
(
$precise := widgets.mode = "precise";
{"type":"range_usd",
"min_usd": $precise ? 0.212 : 0.297,
"max_usd": $precise ? 0.848 : 1.188,
"format": {"approximate": true, "suffix": "/s", "note": "(1080p-4K output)"}}
)
""",
),
)
@classmethod
async def execute(
cls,
video: Input.Video,
upscale_factor: float,
mode: str,
prompt: str,
auto_downscale: bool,
safety_tolerance: int,
seed: int,
) -> IO.NodeOutput:
validate_video_duration(video, min_duration=1.0, max_duration=20.0)
validate_video_dimensions(video, min_width=64, min_height=64)
width, height = video.get_dimensions()
if max(width, height) > _FLUX_VIDEO_UPSCALE_MAX_ASPECT_RATIO * min(width, height):
raise ValueError(f"Video aspect ratio must be between 1:4 and 4:1, got {width}x{height}.")
if auto_downscale:
video = downscale_video_to_max_pixels(video, _FLUX_VIDEO_UPSCALE_MAX_INPUT_PIXELS)
elif width * height > _FLUX_VIDEO_UPSCALE_MAX_INPUT_PIXELS:
raise ValueError(
f"Video must be at most 3840x2160 pixels in area, got {width}x{height}. "
"Enable auto_downscale or use a smaller video."
)
url = await upload_video_to_comfyapi(cls, video, wait_label="Uploading source video")
request = BFLFluxVideoUpscaleRequest(
input_video=url,
upscale_factor=round(upscale_factor, 1),
creativity=_FLUX_VIDEO_UPSCALE_MODES[mode],
prompt=prompt.strip() or None,
safety_tolerance=safety_tolerance,
)
return await _bfl_video_execute(cls, _FLUX_VIDEO_UPSCALE_ENDPOINT, request, poll_via_proxy=True)
class BFLExtension(ComfyExtension):
@@ -1390,6 +1525,7 @@ class BFLExtension(ComfyExtension):
Flux3TextToVideoNode,
Flux3ImageToVideoNode,
Flux3VideoContinuationNode,
FluxVideoUpscaleNode,
]