Merge branch 'master' into alexis/seed

This commit is contained in:
Alexis Rolland 2026-06-25 18:04:21 +08:00 committed by GitHub
commit bda8943cdd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 104 additions and 30 deletions

View File

@ -163,15 +163,27 @@ class SeedanceVirtualLibraryCreateAssetRequest(BaseModel):
asset_type: str | None = Field(None, description="BytePlus asset type. Defaults to Image server-side when omitted.") asset_type: str | None = Field(None, description="BytePlus asset type. Defaults to Image server-side when omitted.")
# Dollars per 1K tokens, keyed by (model_id, has_video_input). # Dollars per 1K tokens, keyed by (model_id, has_video_input, resolution).
SEEDANCE2_PRICE_PER_1K_TOKENS = { SEEDANCE2_PRICE_PER_1K_TOKENS = {
("dreamina-seedance-2-0-260128", False): 0.007, ("dreamina-seedance-2-0-260128", False, "480p"): 0.007,
("dreamina-seedance-2-0-260128", True): 0.0043, ("dreamina-seedance-2-0-260128", True, "480p"): 0.0043,
("dreamina-seedance-2-0-fast-260128", False): 0.0056, ("dreamina-seedance-2-0-260128", False, "720p"): 0.007,
("dreamina-seedance-2-0-fast-260128", True): 0.0033, ("dreamina-seedance-2-0-260128", True, "720p"): 0.0043,
("dreamina-seedance-2-0-260128", False, "1080p"): 0.0077,
("dreamina-seedance-2-0-260128", True, "1080p"): 0.0047,
("dreamina-seedance-2-0-260128", False, "4k"): 0.004,
("dreamina-seedance-2-0-260128", True, "4k"): 0.0024,
("dreamina-seedance-2-0-fast-260128", False, "480p"): 0.0056,
("dreamina-seedance-2-0-fast-260128", True, "480p"): 0.0033,
("dreamina-seedance-2-0-fast-260128", False, "720p"): 0.0056,
("dreamina-seedance-2-0-fast-260128", True, "720p"): 0.0033,
} }
def seedance2_price_per_1k_tokens(model_id: str, has_video_input: bool, resolution: str) -> float | None:
return SEEDANCE2_PRICE_PER_1K_TOKENS.get((model_id, has_video_input, resolution))
RECOMMENDED_PRESETS = [ RECOMMENDED_PRESETS = [
("1024x1024 (1:1)", 1024, 1024), ("1024x1024 (1:1)", 1024, 1024),
("864x1152 (3:4)", 864, 1152), ("864x1152 (3:4)", 864, 1152),

View File

@ -15,7 +15,6 @@ from comfy_api_nodes.apis.bytedance import (
RECOMMENDED_PRESETS_SEEDREAM_4_0, RECOMMENDED_PRESETS_SEEDREAM_4_0,
RECOMMENDED_PRESETS_SEEDREAM_4_5, RECOMMENDED_PRESETS_SEEDREAM_4_5,
RECOMMENDED_PRESETS_SEEDREAM_5_LITE, RECOMMENDED_PRESETS_SEEDREAM_5_LITE,
SEEDANCE2_PRICE_PER_1K_TOKENS,
SEEDANCE2_REF_VIDEO_PIXEL_LIMITS, SEEDANCE2_REF_VIDEO_PIXEL_LIMITS,
VIDEO_TASKS_EXECUTION_TIME, VIDEO_TASKS_EXECUTION_TIME,
GetAssetResponse, GetAssetResponse,
@ -40,6 +39,7 @@ from comfy_api_nodes.apis.bytedance import (
TaskVideoContentUrl, TaskVideoContentUrl,
Text2ImageTaskCreationRequest, Text2ImageTaskCreationRequest,
Text2VideoTaskCreationRequest, Text2VideoTaskCreationRequest,
seedance2_price_per_1k_tokens,
) )
from comfy_api_nodes.util import ( from comfy_api_nodes.util import (
ApiEndpoint, ApiEndpoint,
@ -141,7 +141,7 @@ SEEDANCE2_RATIO_WH = {
"9:16": (9, 16), "9:16": (9, 16),
"21:9": (21, 9), "21:9": (21, 9),
} }
SEEDANCE2_RES_SHORT_SIDE = {"480p": 480, "720p": 720, "1080p": 1080} SEEDANCE2_RES_SHORT_SIDE = {"480p": 480, "720p": 720, "1080p": 1080, "4k": 2160}
def _seedance2_target_dims(resolution: str, ratio: str, image: torch.Tensor) -> tuple[int, int]: def _seedance2_target_dims(resolution: str, ratio: str, image: torch.Tensor) -> tuple[int, int]:
@ -377,9 +377,9 @@ async def _seedance_virtual_library_upload_video_asset(
return f"asset://{create_resp.asset_id}" return f"asset://{create_resp.asset_id}"
def _seedance2_price_extractor(model_id: str, has_video_input: bool): def _seedance2_price_extractor(model_id: str, has_video_input: bool, resolution: str):
"""Returns a price_extractor closure for Seedance 2.0 poll_op.""" """Returns a price_extractor closure for Seedance 2.0 poll_op."""
rate = SEEDANCE2_PRICE_PER_1K_TOKENS.get((model_id, has_video_input)) rate = seedance2_price_per_1k_tokens(model_id, has_video_input, resolution)
if rate is None: if rate is None:
return None return None
@ -1621,7 +1621,7 @@ class ByteDance2TextToVideoNode(IO.ComfyNode):
IO.DynamicCombo.Input( IO.DynamicCombo.Input(
"model", "model",
options=[ options=[
IO.DynamicCombo.Option("Seedance 2.0", _seedance2_text_inputs(["480p", "720p", "1080p"])), IO.DynamicCombo.Option("Seedance 2.0", _seedance2_text_inputs(["480p", "720p", "1080p", "4k"])),
IO.DynamicCombo.Option("Seedance 2.0 Fast", _seedance2_text_inputs(["480p", "720p"])), IO.DynamicCombo.Option("Seedance 2.0 Fast", _seedance2_text_inputs(["480p", "720p"])),
], ],
tooltip="Seedance 2.0 for maximum quality; Seedance 2.0 Fast for speed optimization.", tooltip="Seedance 2.0 for maximum quality; Seedance 2.0 Fast for speed optimization.",
@ -1660,11 +1660,15 @@ class ByteDance2TextToVideoNode(IO.ComfyNode):
$rate480 := 10044; $rate480 := 10044;
$rate720 := 21600; $rate720 := 21600;
$rate1080 := 48800; $rate1080 := 48800;
$rate4k := 195200;
$m := widgets.model; $m := widgets.model;
$pricePer1K := $contains($m, "fast") ? 0.008008 : 0.01001;
$res := $lookup(widgets, "model.resolution"); $res := $lookup(widgets, "model.resolution");
$dur := $lookup(widgets, "model.duration"); $dur := $lookup(widgets, "model.duration");
$rate := $res = "1080p" ? $rate1080 : $pricePer1K := $res = "4k" ? 0.00572 :
$res = "1080p" ? 0.011011 :
$contains($m, "fast") ? 0.008008 : 0.01001;
$rate := $res = "4k" ? $rate4k :
$res = "1080p" ? $rate1080 :
$res = "720p" ? $rate720 : $res = "720p" ? $rate720 :
$rate480; $rate480;
$cost := $dur * $rate * $pricePer1K / 1000; $cost := $dur * $rate * $pricePer1K / 1000;
@ -1703,7 +1707,7 @@ class ByteDance2TextToVideoNode(IO.ComfyNode):
ApiEndpoint(path=f"{BYTEPLUS_SEEDANCE2_TASK_STATUS_ENDPOINT}/{initial_response.id}"), ApiEndpoint(path=f"{BYTEPLUS_SEEDANCE2_TASK_STATUS_ENDPOINT}/{initial_response.id}"),
response_model=TaskStatusResponse, response_model=TaskStatusResponse,
status_extractor=lambda r: r.status, status_extractor=lambda r: r.status,
price_extractor=_seedance2_price_extractor(model_id, has_video_input=False), price_extractor=_seedance2_price_extractor(model_id, has_video_input=False, resolution=model["resolution"]),
poll_interval=9, poll_interval=9,
) )
return IO.NodeOutput(await download_url_to_video_output(response.content.video_url)) return IO.NodeOutput(await download_url_to_video_output(response.content.video_url))
@ -1724,7 +1728,7 @@ class ByteDance2FirstLastFrameNode(IO.ComfyNode):
options=[ options=[
IO.DynamicCombo.Option( IO.DynamicCombo.Option(
"Seedance 2.0", "Seedance 2.0",
_seedance2_text_inputs(["480p", "720p", "1080p"], default_ratio="adaptive"), _seedance2_text_inputs(["480p", "720p", "1080p", "4k"], default_ratio="adaptive"),
), ),
IO.DynamicCombo.Option( IO.DynamicCombo.Option(
"Seedance 2.0 Fast", "Seedance 2.0 Fast",
@ -1791,11 +1795,15 @@ class ByteDance2FirstLastFrameNode(IO.ComfyNode):
$rate480 := 10044; $rate480 := 10044;
$rate720 := 21600; $rate720 := 21600;
$rate1080 := 48800; $rate1080 := 48800;
$rate4k := 195200;
$m := widgets.model; $m := widgets.model;
$pricePer1K := $contains($m, "fast") ? 0.008008 : 0.01001;
$res := $lookup(widgets, "model.resolution"); $res := $lookup(widgets, "model.resolution");
$dur := $lookup(widgets, "model.duration"); $dur := $lookup(widgets, "model.duration");
$rate := $res = "1080p" ? $rate1080 : $pricePer1K := $res = "4k" ? 0.00572 :
$res = "1080p" ? 0.011011 :
$contains($m, "fast") ? 0.008008 : 0.01001;
$rate := $res = "4k" ? $rate4k :
$res = "1080p" ? $rate1080 :
$res = "720p" ? $rate720 : $res = "720p" ? $rate720 :
$rate480; $rate480;
$cost := $dur * $rate * $pricePer1K / 1000; $cost := $dur * $rate * $pricePer1K / 1000;
@ -1913,7 +1921,7 @@ class ByteDance2FirstLastFrameNode(IO.ComfyNode):
ApiEndpoint(path=f"{BYTEPLUS_SEEDANCE2_TASK_STATUS_ENDPOINT}/{initial_response.id}"), ApiEndpoint(path=f"{BYTEPLUS_SEEDANCE2_TASK_STATUS_ENDPOINT}/{initial_response.id}"),
response_model=TaskStatusResponse, response_model=TaskStatusResponse,
status_extractor=lambda r: r.status, status_extractor=lambda r: r.status,
price_extractor=_seedance2_price_extractor(model_id, has_video_input=False), price_extractor=_seedance2_price_extractor(model_id, has_video_input=False, resolution=model["resolution"]),
poll_interval=9, poll_interval=9,
) )
return IO.NodeOutput(await download_url_to_video_output(response.content.video_url)) return IO.NodeOutput(await download_url_to_video_output(response.content.video_url))
@ -2010,7 +2018,7 @@ class ByteDance2ReferenceNode(IO.ComfyNode):
options=[ options=[
IO.DynamicCombo.Option( IO.DynamicCombo.Option(
"Seedance 2.0", "Seedance 2.0",
_seedance2_reference_inputs(["480p", "720p", "1080p"], default_ratio="adaptive"), _seedance2_reference_inputs(["480p", "720p", "1080p", "4k"], default_ratio="adaptive"),
), ),
IO.DynamicCombo.Option( IO.DynamicCombo.Option(
"Seedance 2.0 Fast", "Seedance 2.0 Fast",
@ -2056,13 +2064,19 @@ class ByteDance2ReferenceNode(IO.ComfyNode):
$rate480 := 10044; $rate480 := 10044;
$rate720 := 21600; $rate720 := 21600;
$rate1080 := 48800; $rate1080 := 48800;
$rate4k := 195200;
$m := widgets.model; $m := widgets.model;
$hasVideo := $lookup(inputGroups, "model.reference_videos") > 0; $hasVideo := $lookup(inputGroups, "model.reference_videos") > 0;
$noVideoPricePer1K := $contains($m, "fast") ? 0.008008 : 0.01001;
$videoPricePer1K := $contains($m, "fast") ? 0.004719 : 0.006149;
$res := $lookup(widgets, "model.resolution"); $res := $lookup(widgets, "model.resolution");
$dur := $lookup(widgets, "model.duration"); $dur := $lookup(widgets, "model.duration");
$rate := $res = "1080p" ? $rate1080 : $noVideoPricePer1K := $res = "4k" ? 0.00572 :
$res = "1080p" ? 0.011011 :
$contains($m, "fast") ? 0.008008 : 0.01001;
$videoPricePer1K := $res = "4k" ? 0.003432 :
$res = "1080p" ? 0.006721 :
$contains($m, "fast") ? 0.004719 : 0.006149;
$rate := $res = "4k" ? $rate4k :
$res = "1080p" ? $rate1080 :
$res = "720p" ? $rate720 : $res = "720p" ? $rate720 :
$rate480; $rate480;
$noVideoCost := $dur * $rate * $noVideoPricePer1K / 1000; $noVideoCost := $dur * $rate * $noVideoPricePer1K / 1000;
@ -2258,7 +2272,9 @@ class ByteDance2ReferenceNode(IO.ComfyNode):
ApiEndpoint(path=f"{BYTEPLUS_SEEDANCE2_TASK_STATUS_ENDPOINT}/{initial_response.id}"), ApiEndpoint(path=f"{BYTEPLUS_SEEDANCE2_TASK_STATUS_ENDPOINT}/{initial_response.id}"),
response_model=TaskStatusResponse, response_model=TaskStatusResponse,
status_extractor=lambda r: r.status, status_extractor=lambda r: r.status,
price_extractor=_seedance2_price_extractor(model_id, has_video_input=has_video_input), price_extractor=_seedance2_price_extractor(
model_id, has_video_input=has_video_input, resolution=model["resolution"]
),
poll_interval=9, poll_interval=9,
) )
return IO.NodeOutput(await download_url_to_video_output(response.content.video_url)) return IO.NodeOutput(await download_url_to_video_output(response.content.video_url))

View File

@ -30,7 +30,7 @@ from comfy_api_nodes.util import (
_GROK_VIDEO_MODEL_API_IDS = { _GROK_VIDEO_MODEL_API_IDS = {
"grok-imagine-video-1.5": "grok-imagine-video-1.5-preview", "grok-imagine-video-1.5": "grok-imagine-video-1.5",
} }
@ -521,8 +521,8 @@ class GrokVideoNode(IO.ComfyNode):
), ),
IO.Combo.Input( IO.Combo.Input(
"resolution", "resolution",
options=["480p", "720p"], options=["480p", "720p", "1080p"],
tooltip="The resolution of the output video.", tooltip="The resolution of the output video. 1080p is only available for grok-imagine-video-1.5.",
), ),
IO.Combo.Input( IO.Combo.Input(
"aspect_ratio", "aspect_ratio",
@ -570,11 +570,12 @@ class GrokVideoNode(IO.ComfyNode):
( (
$is15 := $contains(widgets.model, "1.5"); $is15 := $contains(widgets.model, "1.5");
$rate := $is15 $rate := $is15
? (widgets.resolution = "720p" ? 0.2002 : 0.1144) ? (widgets.resolution = "1080p" ? 0.25 : (widgets.resolution = "720p" ? 0.14 : 0.08))
: (widgets.resolution = "720p" ? 0.07 : 0.05); : (widgets.resolution = "720p" ? 0.07 : 0.05);
$imgCost := $is15 ? 0.0143 : 0.002; $imgCost := $is15 ? 0.01 : 0.002;
$base := $rate * widgets.duration; $base := $rate * widgets.duration;
{"type":"usd","usd": inputs.image.connected ? $base + $imgCost : $base} $total := inputs.image.connected ? $base + $imgCost : $base;
{"type":"usd","usd": $is15 ? $total * 1.43 : $total}
) )
""", """,
), ),
@ -593,6 +594,8 @@ class GrokVideoNode(IO.ComfyNode):
) -> IO.NodeOutput: ) -> IO.NodeOutput:
if image is None and model == "grok-imagine-video-1.5": if image is None and model == "grok-imagine-video-1.5":
raise ValueError(f"The '{model}' model requires an input image; connect one to the 'image' input.") raise ValueError(f"The '{model}' model requires an input image; connect one to the 'image' input.")
if resolution == "1080p" and model != "grok-imagine-video-1.5":
raise ValueError(f"1080p resolution is only available for grok-imagine-video-1.5, not '{model}'.")
image_url = None image_url = None
if image is not None: if image is not None:
if get_number_of_images(image) != 1: if get_number_of_images(image) != 1:

View File

@ -337,6 +337,36 @@ class ModelMergeQwenImage(comfy_extras.nodes_model_merging.ModelMergeBlocks):
return {"required": arg_dict} return {"required": arg_dict}
class ModelMergeKrea2(comfy_extras.nodes_model_merging.ModelMergeBlocks):
CATEGORY = "model/merging/model specific"
@classmethod
def INPUT_TYPES(s):
arg_dict = { "model1": ("MODEL",),
"model2": ("MODEL",)}
argument = ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01})
arg_dict["first."] = argument
arg_dict["tmlp."] = argument
arg_dict["txtmlp."] = argument
arg_dict["tproj."] = argument
for i in range(2):
arg_dict["txtfusion.layerwise_blocks.{}.".format(i)] = argument
arg_dict["txtfusion.projector."] = argument
for i in range(2):
arg_dict["txtfusion.refiner_blocks.{}.".format(i)] = argument
for i in range(28):
arg_dict["blocks.{}.".format(i)] = argument
arg_dict["last."] = argument
return {"required": arg_dict}
NODE_CLASS_MAPPINGS = { NODE_CLASS_MAPPINGS = {
"ModelMergeSD1": ModelMergeSD1, "ModelMergeSD1": ModelMergeSD1,
"ModelMergeSD2": ModelMergeSD1, #SD1 and SD2 have the same blocks "ModelMergeSD2": ModelMergeSD1, #SD1 and SD2 have the same blocks
@ -353,4 +383,5 @@ NODE_CLASS_MAPPINGS = {
"ModelMergeCosmosPredict2_2B": ModelMergeCosmosPredict2_2B, "ModelMergeCosmosPredict2_2B": ModelMergeCosmosPredict2_2B,
"ModelMergeCosmosPredict2_14B": ModelMergeCosmosPredict2_14B, "ModelMergeCosmosPredict2_14B": ModelMergeCosmosPredict2_14B,
"ModelMergeQwenImage": ModelMergeQwenImage, "ModelMergeQwenImage": ModelMergeQwenImage,
"ModelMergeKrea2": ModelMergeKrea2,
} }

View File

@ -1692,6 +1692,12 @@ paths:
schema: schema:
$ref: '#/components/schemas/ErrorResponse' $ref: '#/components/schemas/ErrorResponse'
description: Unsupported media type description: Unsupported media type
"422":
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: Validation error (e.g., disallowed model_type tag)
"500": "500":
content: content:
application/json: application/json:
@ -2137,6 +2143,12 @@ paths:
schema: schema:
$ref: '#/components/schemas/ErrorResponse' $ref: '#/components/schemas/ErrorResponse'
description: Source asset with given hash not found description: Source asset with given hash not found
"422":
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: Validation error (e.g., disallowed model_type tag)
"500": "500":
content: content:
application/json: application/json:
@ -2992,7 +3004,7 @@ paths:
format: uuid format: uuid
type: string type: string
- description: | - description: |
When present, each output item in the response receives a `short_url` field containing an owner-gated durable link for that asset. Omit this parameter (the default) to receive a response identical to the no-param baseline. The value selects the link's lifetime: use `ephemeral_tool_chain` for short-lived machine-to-machine handoffs (~15 minutes); use `default` for durable human-revisitable links (30 days). Links are minted only for the authenticated request owner and are not resolvable by other users. When present, each output item in the response receives a `short_url` field containing a short link for that asset. Omit this parameter (the default) to receive a response identical to the no-param baseline. The value selects the link's lifetime and auth model: use `ephemeral_tool_chain` for short-lived (≤5 minute) machine-to-machine handoffs — these are public bearer links where the link ID itself is the credential, so anyone holding the link can resolve it (intended for pasting into an agent/MCP tool chain); use `default` for durable (30 day) human-revisitable links, which are owner-gated and resolvable only by the authenticated owner. Links are always minted under the authenticated request owner's identity; the auth model is selected by the server and is never settable by the caller.
in: query in: query
name: short_link name: short_link
schema: schema:

View File

@ -1,5 +1,5 @@
comfyui-frontend-package==1.45.19 comfyui-frontend-package==1.45.19
comfyui-workflow-templates==0.10.3 comfyui-workflow-templates==0.10.2
comfyui-embedded-docs==0.5.5 comfyui-embedded-docs==0.5.5
torch torch
torchsde torchsde