From 6aa1def245ce2f7d54f22eda9c54d808cd06e631 Mon Sep 17 00:00:00 2001 From: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Date: Thu, 20 Aug 2026 20:22:49 +0400 Subject: [PATCH] [Partner Nodes] feat(BFL): add Flux Video Upscale node (#15753) Signed-off-by: Alexander Piskun (cherry picked from commit d3363657c2d6b89980791835218da36eeaafd6d7) --- comfy_api_nodes/apis/bfl.py | 10 +++ comfy_api_nodes/nodes_bfl.py | 158 ++++++++++++++++++++++++++++++++--- 2 files changed, 157 insertions(+), 11 deletions(-) diff --git a/comfy_api_nodes/apis/bfl.py b/comfy_api_nodes/apis/bfl.py index 389706cf4..0e33f2f5e 100644 --- a/comfy_api_nodes/apis/bfl.py +++ b/comfy_api_nodes/apis/bfl.py @@ -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) diff --git a/comfy_api_nodes/nodes_bfl.py b/comfy_api_nodes/nodes_bfl.py index 30275bfbe..dd8653cdd 100644 --- a/comfy_api_nodes/nodes_bfl.py +++ b/comfy_api_nodes/nodes_bfl.py @@ -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, ]