diff --git a/comfy_api_nodes/apis/bfl.py b/comfy_api_nodes/apis/bfl.py index 389706cf496..0e33f2f5ec3 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/apis/bytedance.py b/comfy_api_nodes/apis/bytedance.py index 6b854631d00..87d22034a21 100644 --- a/comfy_api_nodes/apis/bytedance.py +++ b/comfy_api_nodes/apis/bytedance.py @@ -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): diff --git a/comfy_api_nodes/nodes_bfl.py b/comfy_api_nodes/nodes_bfl.py index 30275bfbe71..dd8653cdda9 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 @@ 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 @@ validate_aspect_ratio_string, validate_image_dimensions, validate_string, + validate_video_dimensions, + validate_video_duration, ) @@ -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, @@ -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): @@ -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): @@ -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): @@ -1390,6 +1525,7 @@ async def get_node_list(self) -> list[type[IO.ComfyNode]]: Flux3TextToVideoNode, Flux3ImageToVideoNode, Flux3VideoContinuationNode, + FluxVideoUpscaleNode, ] diff --git a/comfy_api_nodes/nodes_bytedance.py b/comfy_api_nodes/nodes_bytedance.py index ddef41ff148..1f2dfd21b44 100644 --- a/comfy_api_nodes/nodes_bytedance.py +++ b/comfy_api_nodes/nodes_bytedance.py @@ -755,6 +755,8 @@ def _seedream_model_inputs( max_width: int = 6240, max_height: int = 4992, supports_batch: bool = True, + supports_fast: bool = False, + include_common: bool = False, ): inputs = [ IO.Combo.Input( @@ -815,15 +817,57 @@ def _seedream_model_inputs( advanced=True, ) ) + if supports_fast: + inputs.append( + IO.Combo.Input( + "prompt_optimization", + options=["standard", "fast"], + default="standard", + tooltip="Prompt-optimization mode when reference images are provided: " + "'standard' gives higher quality, 'fast' shorter generation time.", + advanced=True, + ) + ) + if include_common: + inputs.extend( + [ + IO.Int.Input( + "seed", + default=42, + min=0, + max=2147483647, + step=1, + display_mode=IO.NumberDisplay.number, + control_after_generate=True, + tooltip="Seed to use for generation.", + ), + IO.Boolean.Input( + "watermark", + default=False, + tooltip='Whether to add an "AI generated" watermark to the image.', + advanced=True, + ), + IO.Boolean.Input( + "thinking", + default=True, + tooltip=( + "Enable the model's prompt-optimization reasoning ('thinking') for better adherence. " + "Can substantially increase generation time — notably on Seedream 5.0 Pro. " + "Can only be disabled for text-to-image (not when reference images are provided)." + ), + advanced=True, + ), + ] + ) return inputs -class ByteDanceSeedreamNodeV2(IO.ComfyNode): +class ByteDanceSeedreamNodeV3(IO.ComfyNode): @classmethod def define_schema(cls): return IO.Schema( - node_id="ByteDanceSeedreamNodeV2", + node_id="ByteDanceSeedreamNodeV3", display_name="ByteDance Seedream 4.5 & 5.0", category="partner/image/ByteDance", description="Unified text-to-image generation and precise single-sentence editing at up to 4K resolution.", @@ -845,49 +889,36 @@ def define_schema(cls): max_width=3136, max_height=2496, supports_batch=False, + supports_fast=True, + include_common=True, ), ), IO.DynamicCombo.Option( "seedream 5.0 lite", - _seedream_model_inputs(max_ref_images=14, presets=RECOMMENDED_PRESETS_SEEDREAM_5_LITE), + _seedream_model_inputs( + max_ref_images=14, + presets=RECOMMENDED_PRESETS_SEEDREAM_5_LITE, + include_common=True, + ), ), IO.DynamicCombo.Option( "seedream-4-5-251128", - _seedream_model_inputs(max_ref_images=10, presets=RECOMMENDED_PRESETS_SEEDREAM_4_5), + _seedream_model_inputs( + max_ref_images=10, + presets=RECOMMENDED_PRESETS_SEEDREAM_4_5, + include_common=True, + ), ), IO.DynamicCombo.Option( "seedream-4-0-250828", - _seedream_model_inputs(max_ref_images=10, presets=RECOMMENDED_PRESETS_SEEDREAM_4_0), + _seedream_model_inputs( + max_ref_images=10, + presets=RECOMMENDED_PRESETS_SEEDREAM_4_0, + include_common=True, + ), ), ], ), - IO.Int.Input( - "seed", - default=0, - min=0, - max=2147483647, - step=1, - display_mode=IO.NumberDisplay.number, - control_after_generate=True, - tooltip="Seed to use for generation.", - ), - IO.Boolean.Input( - "watermark", - default=False, - tooltip='Whether to add an "AI generated" watermark to the image.', - advanced=True, - ), - IO.Boolean.Input( - "thinking", - default=True, - tooltip=( - "Enable the model's prompt-optimization reasoning ('thinking') for better adherence. " - "Can substantially increase generation time — notably on Seedream 5.0 Pro. " - "Can only be disabled for text-to-image (not when reference images are provided)." - ), - optional=True, - advanced=True, - ), ], outputs=[ IO.Image.Output(), @@ -900,27 +931,39 @@ def define_schema(cls): is_api_node=True, price_badge=IO.PriceBadge( depends_on=IO.PriceBadgeDepends( - widgets=["model", "model.size_preset", "model.width", "model.height"] + widgets=["model", "model.size_preset", "model.width", "model.height"], + input_groups=["model.images"], ), expr=""" ( - $sp := $lookup(widgets, "model.size_preset"); - $px := $lookup(widgets, "model.width") * $lookup(widgets, "model.height"); - $isPro := $contains(widgets.model, "5.0 pro"); - $price := $isPro - ? ( - $contains($sp, "custom") - ? ($px <= 2360000 ? 0.045 : 0.09) - : ($contains($sp, "1k") ? 0.045 : 0.09) - ) - : $contains(widgets.model, "5.0 lite") ? 0.035 - : $contains(widgets.model, "4-5") ? 0.04 - : 0.03; - { - "type": "usd", - "usd": $price, - "format": { "suffix": $isPro ? "/Image" : " x images/Run", "approximate": true } - } + $model := $string(widgets.model); + $sp := $string($lookup(widgets, "model.size_preset")); + $w := $lookup(widgets, "model.width"); + $h := $lookup(widgets, "model.height"); + $px := ($type($w) = "number" and $type($h) = "number") ? $w * $h : 0; + $refs := $lookup(inputGroups, "model.images"); + $extra := ($type($refs) = "number" and $refs > 1) ? ($refs - 1) * 0.003 : 0; + $isPro := $contains($model, "5.0 pro"); + $isCustom := $contains($sp, "custom"); + $sizeKnown := $isCustom ? $px > 0 : ($contains($sp, "1k") or $contains($sp, "2k")); + $proPrice := $isCustom + ? ($px < 2610000 ? 0.045 : 0.09) + : ($contains($sp, "1k") ? 0.045 : 0.09); + ($isPro and ($sizeKnown = false)) + ? { + "type": "range_usd", + "min_usd": 0.045 + $extra, + "max_usd": 0.09 + $extra, + "format": { "suffix": "/Image", "approximate": true } + } + : { + "type": "usd", + "usd": $isPro ? $proPrice + $extra + : $contains($model, "5.0 lite") ? 0.035 + : $contains($model, "4-5") ? 0.04 + : 0.03, + "format": { "suffix": $isPro ? "/Image" : " x images/Run", "approximate": true } + } ) """, ), @@ -947,6 +990,10 @@ async def execute( sequential_image_generation = "disabled" if max_images == 1 else "auto" images_dict = model.get("images") or {} fail_on_partial = model.get("fail_on_partial", False) + prompt_optimization = model.get("prompt_optimization", "standard") + seed = model.get("seed", seed) + watermark = model.get("watermark", watermark) + thinking = model.get("thinking", thinking) w = h = None for label, tw, th in presets: @@ -1013,6 +1060,8 @@ async def execute( optimize_prompt_options = None if n_input_images == 0: optimize_prompt_options = Seedream5OptimizePromptOptions(thinking="enabled" if thinking else "disabled") + elif prompt_optimization == "fast": + optimize_prompt_options = Seedream5OptimizePromptOptions(mode="fast") response = await sync_op( cls, ApiEndpoint(path=BYTEPLUS_IMAGE_ENDPOINT, method="POST"), @@ -1037,6 +1086,115 @@ async def execute( return IO.NodeOutput(torch.cat([await download_url_to_image_tensor(i) for i in urls])) +class ByteDanceSeedreamNodeV2(ByteDanceSeedreamNodeV3): + + @classmethod + def define_schema(cls): + return IO.Schema( + node_id="ByteDanceSeedreamNodeV2", + display_name="ByteDance Seedream 4.5 & 5.0 (Legacy)", + category="partner/image/ByteDance", + description="Unified text-to-image generation and precise single-sentence editing at up to 4K resolution.", + inputs=[ + IO.String.Input( + "prompt", + multiline=True, + default="", + tooltip="Text prompt for creating or editing an image.", + ), + IO.DynamicCombo.Input( + "model", + options=[ + IO.DynamicCombo.Option( + "seedream 5.0 pro", + _seedream_model_inputs( + max_ref_images=10, + presets=RECOMMENDED_PRESETS_SEEDREAM_5_PRO, + max_width=3136, + max_height=2496, + supports_batch=False, + ), + ), + IO.DynamicCombo.Option( + "seedream 5.0 lite", + _seedream_model_inputs(max_ref_images=14, presets=RECOMMENDED_PRESETS_SEEDREAM_5_LITE), + ), + IO.DynamicCombo.Option( + "seedream-4-5-251128", + _seedream_model_inputs(max_ref_images=10, presets=RECOMMENDED_PRESETS_SEEDREAM_4_5), + ), + IO.DynamicCombo.Option( + "seedream-4-0-250828", + _seedream_model_inputs(max_ref_images=10, presets=RECOMMENDED_PRESETS_SEEDREAM_4_0), + ), + ], + ), + IO.Int.Input( + "seed", + default=0, + min=0, + max=2147483647, + step=1, + display_mode=IO.NumberDisplay.number, + control_after_generate=True, + tooltip="Seed to use for generation.", + ), + IO.Boolean.Input( + "watermark", + default=False, + tooltip='Whether to add an "AI generated" watermark to the image.', + advanced=True, + ), + IO.Boolean.Input( + "thinking", + default=True, + tooltip=( + "Enable the model's prompt-optimization reasoning ('thinking') for better adherence. " + "Can substantially increase generation time — notably on Seedream 5.0 Pro. " + "Can only be disabled for text-to-image (not when reference images are provided)." + ), + optional=True, + advanced=True, + ), + ], + outputs=[ + IO.Image.Output(), + ], + hidden=[ + IO.Hidden.auth_token_comfy_org, + IO.Hidden.api_key_comfy_org, + IO.Hidden.unique_id, + ], + is_api_node=True, + is_deprecated=True, + price_badge=IO.PriceBadge( + depends_on=IO.PriceBadgeDepends( + widgets=["model", "model.size_preset", "model.width", "model.height"] + ), + expr=""" + ( + $sp := $lookup(widgets, "model.size_preset"); + $px := $lookup(widgets, "model.width") * $lookup(widgets, "model.height"); + $isPro := $contains(widgets.model, "5.0 pro"); + $price := $isPro + ? ( + $contains($sp, "custom") + ? ($px <= 2360000 ? 0.045 : 0.09) + : ($contains($sp, "1k") ? 0.045 : 0.09) + ) + : $contains(widgets.model, "5.0 lite") ? 0.035 + : $contains(widgets.model, "4-5") ? 0.04 + : 0.03; + { + "type": "usd", + "usd": $price, + "format": { "suffix": $isPro ? "/Image" : " x images/Run", "approximate": true } + } + ) + """, + ), + ) + class ByteDanceSeedreamLayerSeparationNode(IO.ComfyNode): @classmethod @@ -3516,6 +3674,7 @@ async def get_node_list(self) -> list[type[IO.ComfyNode]]: ByteDanceImageNode, ByteDanceSeedreamNode, ByteDanceSeedreamNodeV2, + ByteDanceSeedreamNodeV3, ByteDanceSeedreamLayerSeparationNode, ByteDanceTextToVideoNode, ByteDanceImageToVideoNode, diff --git a/comfy_api_nodes/nodes_ideogram.py b/comfy_api_nodes/nodes_ideogram.py index 252617b2cb1..2acf77b88c1 100644 --- a/comfy_api_nodes/nodes_ideogram.py +++ b/comfy_api_nodes/nodes_ideogram.py @@ -531,7 +531,7 @@ class IdeogramPImage(IO.ComfyNode): def define_schema(cls): return IO.Schema( node_id="IdeogramPImage", - display_name="Ideogram P-Image", + display_name="Ideogram & Pruna P-Image", category="partner/image/Ideogram", description="Generates images using P-Image, Ideogram's fast text-to-image model. " "Strong typography and photorealism; "