Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions comfy_api_nodes/apis/bfl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
3 changes: 2 additions & 1 deletion comfy_api_nodes/apis/bytedance.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ class Seedream4Options(BaseModel):


class Seedream5OptimizePromptOptions(BaseModel):
thinking: Literal["auto", "enabled", "disabled"] = Field(...)
thinking: Literal["auto", "enabled", "disabled"] | None = Field(None)
mode: Literal["standard", "fast"] | None = Field(None)


class Seedream4TaskCreationRequest(BaseModel):
Expand Down
158 changes: 147 additions & 11 deletions comfy_api_nodes/nodes_bfl.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -12,18 +13,19 @@
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,
Expand All @@ -35,6 +37,8 @@
validate_aspect_ratio_string,
validate_image_dimensions,
validate_string,
validate_video_dimensions,
validate_video_duration,
)


Expand Down Expand Up @@ -1147,16 +1151,23 @@ def price_badge(cls) -> IO.PriceBadge:
)


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,
Expand Down Expand Up @@ -1221,7 +1232,7 @@ async def execute(
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):
Expand Down Expand Up @@ -1319,7 +1330,7 @@ async def execute(
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):
Expand Down Expand Up @@ -1370,7 +1381,131 @@ async def execute(
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):
Expand All @@ -1390,6 +1525,7 @@ async def get_node_list(self) -> list[type[IO.ComfyNode]]:
Flux3TextToVideoNode,
Flux3ImageToVideoNode,
Flux3VideoContinuationNode,
FluxVideoUpscaleNode,
]


Expand Down
Loading
Loading