mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-07-12 17:37:17 +08:00
Merge branch 'master' of https://github.com/Comfy-Org/ComfyUI into alexis/seed
This commit is contained in:
commit
1255e4e3e9
@ -893,6 +893,14 @@ class Tracks(ComfyTypeIO):
|
|||||||
track_visibility: torch.Tensor
|
track_visibility: torch.Tensor
|
||||||
Type = TrackDict
|
Type = TrackDict
|
||||||
|
|
||||||
|
@comfytype(io_type="DICT")
|
||||||
|
class Dict(ComfyTypeIO):
|
||||||
|
Type = dict
|
||||||
|
|
||||||
|
@comfytype(io_type="ARRAY")
|
||||||
|
class Array(ComfyTypeIO):
|
||||||
|
Type = list
|
||||||
|
|
||||||
@comfytype(io_type="COMFY_MULTITYPED_V3")
|
@comfytype(io_type="COMFY_MULTITYPED_V3")
|
||||||
class MultiType:
|
class MultiType:
|
||||||
Type = Any
|
Type = Any
|
||||||
@ -1281,6 +1289,19 @@ class Color(ComfyTypeIO):
|
|||||||
def as_dict(self):
|
def as_dict(self):
|
||||||
return super().as_dict()
|
return super().as_dict()
|
||||||
|
|
||||||
|
|
||||||
|
@comfytype(io_type="COLORS")
|
||||||
|
class Colors(ComfyTypeIO):
|
||||||
|
Type = list[Color.Type]
|
||||||
|
|
||||||
|
class Input(WidgetInput):
|
||||||
|
def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str=None,
|
||||||
|
socketless: bool=True, default: list[str]=None, advanced: bool=None):
|
||||||
|
super().__init__(id, display_name, optional, tooltip, None, default, socketless, None, None, None, None, advanced)
|
||||||
|
if default is None:
|
||||||
|
self.default = []
|
||||||
|
|
||||||
|
|
||||||
@comfytype(io_type="BOUNDING_BOX")
|
@comfytype(io_type="BOUNDING_BOX")
|
||||||
class BoundingBox(ComfyTypeIO):
|
class BoundingBox(ComfyTypeIO):
|
||||||
class BoundingBoxDict(TypedDict):
|
class BoundingBoxDict(TypedDict):
|
||||||
@ -1328,6 +1349,20 @@ class Curve(ComfyTypeIO):
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
@comfytype(io_type="BOUNDING_BOXES")
|
||||||
|
class BoundingBoxes(ComfyTypeIO):
|
||||||
|
class BoundingBoxWithMetadata(BoundingBox.BoundingBoxDict):
|
||||||
|
metadata: dict
|
||||||
|
Type = list[BoundingBoxWithMetadata]
|
||||||
|
|
||||||
|
class Input(WidgetInput):
|
||||||
|
def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str=None,
|
||||||
|
socketless: bool=True, default: list[dict]=None, advanced: bool=None):
|
||||||
|
super().__init__(id, display_name, optional, tooltip, None, default, socketless, None, None, None, None, advanced)
|
||||||
|
if default is None:
|
||||||
|
self.default = []
|
||||||
|
|
||||||
|
|
||||||
@comfytype(io_type="HISTOGRAM")
|
@comfytype(io_type="HISTOGRAM")
|
||||||
class Histogram(ComfyTypeIO):
|
class Histogram(ComfyTypeIO):
|
||||||
"""A histogram represented as a list of bin counts."""
|
"""A histogram represented as a list of bin counts."""
|
||||||
@ -2378,6 +2413,8 @@ __all__ = [
|
|||||||
"AnyType",
|
"AnyType",
|
||||||
"MultiType",
|
"MultiType",
|
||||||
"Tracks",
|
"Tracks",
|
||||||
|
"Dict",
|
||||||
|
"Array",
|
||||||
"Color",
|
"Color",
|
||||||
# Dynamic Types
|
# Dynamic Types
|
||||||
"MatchType",
|
"MatchType",
|
||||||
@ -2396,6 +2433,8 @@ __all__ = [
|
|||||||
"PriceBadgeDepends",
|
"PriceBadgeDepends",
|
||||||
"PriceBadge",
|
"PriceBadge",
|
||||||
"BoundingBox",
|
"BoundingBox",
|
||||||
|
"BoundingBoxes",
|
||||||
|
"Colors",
|
||||||
"Curve",
|
"Curve",
|
||||||
"Histogram",
|
"Histogram",
|
||||||
"Range",
|
"Range",
|
||||||
|
|||||||
@ -163,15 +163,31 @@ 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,
|
||||||
|
("dreamina-seedance-2-0-mini", False, "480p"): 0.0035,
|
||||||
|
("dreamina-seedance-2-0-mini", True, "480p"): 0.0021,
|
||||||
|
("dreamina-seedance-2-0-mini", False, "720p"): 0.0035,
|
||||||
|
("dreamina-seedance-2-0-mini", True, "720p"): 0.0021,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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),
|
||||||
@ -266,6 +282,10 @@ SEEDANCE2_REF_VIDEO_PIXEL_LIMITS = {
|
|||||||
"480p": {"min": 409_600, "max": 927_408},
|
"480p": {"min": 409_600, "max": 927_408},
|
||||||
"720p": {"min": 409_600, "max": 927_408},
|
"720p": {"min": 409_600, "max": 927_408},
|
||||||
},
|
},
|
||||||
|
"dreamina-seedance-2-0-mini": {
|
||||||
|
"480p": {"min": 409_600, "max": 927_408},
|
||||||
|
"720p": {"min": 409_600, "max": 927_408},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# The time in this dictionary are given for 10 seconds duration.
|
# The time in this dictionary are given for 10 seconds duration.
|
||||||
|
|||||||
@ -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,
|
||||||
@ -89,6 +89,7 @@ BYTEPLUS_SEEDANCE2_TASK_STATUS_ENDPOINT = "/proxy/byteplus-seedance2/api/v3/cont
|
|||||||
SEEDANCE_MODELS = {
|
SEEDANCE_MODELS = {
|
||||||
"Seedance 2.0": "dreamina-seedance-2-0-260128",
|
"Seedance 2.0": "dreamina-seedance-2-0-260128",
|
||||||
"Seedance 2.0 Fast": "dreamina-seedance-2-0-fast-260128",
|
"Seedance 2.0 Fast": "dreamina-seedance-2-0-fast-260128",
|
||||||
|
"Seedance 2.0 Mini": "dreamina-seedance-2-0-mini",
|
||||||
}
|
}
|
||||||
|
|
||||||
DEPRECATED_MODELS = {"seedance-1-0-lite-t2v-250428", "seedance-1-0-lite-i2v-250428"}
|
DEPRECATED_MODELS = {"seedance-1-0-lite-t2v-250428", "seedance-1-0-lite-i2v-250428"}
|
||||||
@ -141,7 +142,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 +378,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,10 +1622,12 @@ 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"])),
|
||||||
|
IO.DynamicCombo.Option("Seedance 2.0 Mini", _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; Fast for speed optimization; "
|
||||||
|
"Mini for the fastest, lowest-cost generation.",
|
||||||
),
|
),
|
||||||
IO.Int.Input(
|
IO.Int.Input(
|
||||||
"seed",
|
"seed",
|
||||||
@ -1660,11 +1663,16 @@ 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, "mini") ? 0.005005 :
|
||||||
|
$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 +1711,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,14 +1732,19 @@ 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",
|
||||||
_seedance2_text_inputs(["480p", "720p"], default_ratio="adaptive"),
|
_seedance2_text_inputs(["480p", "720p"], default_ratio="adaptive"),
|
||||||
),
|
),
|
||||||
|
IO.DynamicCombo.Option(
|
||||||
|
"Seedance 2.0 Mini",
|
||||||
|
_seedance2_text_inputs(["480p", "720p"], default_ratio="adaptive"),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
tooltip="Seedance 2.0 for maximum quality; Seedance 2.0 Fast for speed optimization.",
|
tooltip="Seedance 2.0 for maximum quality; Fast for speed optimization; "
|
||||||
|
"Mini for the fastest, lowest-cost generation.",
|
||||||
),
|
),
|
||||||
IO.Image.Input(
|
IO.Image.Input(
|
||||||
"first_frame",
|
"first_frame",
|
||||||
@ -1791,11 +1804,16 @@ 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, "mini") ? 0.005005 :
|
||||||
|
$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 +1931,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,14 +2028,19 @@ 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",
|
||||||
_seedance2_reference_inputs(["480p", "720p"], default_ratio="adaptive"),
|
_seedance2_reference_inputs(["480p", "720p"], default_ratio="adaptive"),
|
||||||
),
|
),
|
||||||
|
IO.DynamicCombo.Option(
|
||||||
|
"Seedance 2.0 Mini",
|
||||||
|
_seedance2_reference_inputs(["480p", "720p"], default_ratio="adaptive"),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
tooltip="Seedance 2.0 for maximum quality; Seedance 2.0 Fast for speed optimization.",
|
tooltip="Seedance 2.0 for maximum quality; Fast for speed optimization; "
|
||||||
|
"Mini for the fastest, lowest-cost generation.",
|
||||||
),
|
),
|
||||||
IO.Int.Input(
|
IO.Int.Input(
|
||||||
"seed",
|
"seed",
|
||||||
@ -2056,13 +2079,21 @@ 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, "mini") ? 0.005005 :
|
||||||
|
$contains($m, "fast") ? 0.008008 : 0.01001;
|
||||||
|
$videoPricePer1K := $res = "4k" ? 0.003432 :
|
||||||
|
$res = "1080p" ? 0.006721 :
|
||||||
|
$contains($m, "mini") ? 0.003003 :
|
||||||
|
$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 +2289,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))
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
23
comfy_extras/color_util.py
Normal file
23
comfy_extras/color_util.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
def hex_to_rgb(value: str) -> tuple[int, int, int]:
|
||||||
|
h = value.lstrip("#")
|
||||||
|
if len(h) != 6:
|
||||||
|
return (255, 255, 255)
|
||||||
|
try:
|
||||||
|
return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
|
||||||
|
except ValueError:
|
||||||
|
return (255, 255, 255)
|
||||||
|
|
||||||
|
|
||||||
|
def readable_color(rgb: tuple[int, int, int]) -> tuple[int, int, int]:
|
||||||
|
r, g, b = rgb
|
||||||
|
lum = 0.299 * r + 0.587 * g + 0.114 * b
|
||||||
|
if lum >= 130:
|
||||||
|
return (r, g, b)
|
||||||
|
t = (130 - lum) / (255 - lum)
|
||||||
|
return (round(r + (255 - r) * t), round(g + (255 - g) * t), round(b + (255 - b) * t))
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_palette(colors) -> list[str]:
|
||||||
|
if isinstance(colors, dict):
|
||||||
|
colors = colors.values()
|
||||||
|
return [c.upper() for c in colors if isinstance(c, str) and c]
|
||||||
253
comfy_extras/nodes_bounding_boxes.py
Normal file
253
comfy_extras/nodes_bounding_boxes.py
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
from PIL import Image, ImageDraw, ImageEnhance, ImageFont
|
||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
|
from comfy_api.latest import ComfyExtension, io
|
||||||
|
from comfy_extras.color_util import hex_to_rgb, normalize_palette, readable_color
|
||||||
|
|
||||||
|
_PREVIEW_LONG_EDGE = 1024
|
||||||
|
_PREVIEW_DIM = 0.25
|
||||||
|
|
||||||
|
|
||||||
|
def pixels_to_fractions(box: dict, width: int, height: int) -> dict:
|
||||||
|
w = width or 1
|
||||||
|
h = height or 1
|
||||||
|
return {
|
||||||
|
"x": box.get("x", 0) / w,
|
||||||
|
"y": box.get("y", 0) / h,
|
||||||
|
"w": box.get("width", 0) / w,
|
||||||
|
"h": box.get("height", 0) / h,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def fractions_to_pixels(box: dict, width: int, height: int) -> dict:
|
||||||
|
x, y = box.get("x", 0.0), box.get("y", 0.0)
|
||||||
|
w, h = box.get("w", 0.0), box.get("h", 0.0)
|
||||||
|
if w < 0:
|
||||||
|
x, w = x + w, -w
|
||||||
|
if h < 0:
|
||||||
|
y, h = y + h, -h
|
||||||
|
return {
|
||||||
|
"x": round(x * width),
|
||||||
|
"y": round(y * height),
|
||||||
|
"width": round(w * width),
|
||||||
|
"height": round(h * height),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def fractions_to_bbox_frame(boxes: list, width: int, height: int) -> list:
|
||||||
|
pixels = [
|
||||||
|
fractions_to_pixels(box, width, height)
|
||||||
|
for box in boxes
|
||||||
|
if isinstance(box, dict)
|
||||||
|
]
|
||||||
|
return [pixels] if pixels else []
|
||||||
|
|
||||||
|
|
||||||
|
def _font(size: int):
|
||||||
|
try:
|
||||||
|
return ImageFont.load_default(size)
|
||||||
|
except Exception:
|
||||||
|
return ImageFont.load_default()
|
||||||
|
|
||||||
|
|
||||||
|
def _wrap(draw, text: str, font, max_w: float) -> list[str]:
|
||||||
|
lines = []
|
||||||
|
for para in text.split("\n"):
|
||||||
|
line = ""
|
||||||
|
for word in para.split():
|
||||||
|
test = word if not line else line + " " + word
|
||||||
|
if line and draw.textlength(test, font=font) > max_w:
|
||||||
|
lines.append(line)
|
||||||
|
line = word
|
||||||
|
else:
|
||||||
|
line = test
|
||||||
|
lines.append(line)
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def _bg_from_image(image) -> Image.Image | None:
|
||||||
|
if image is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
arr = (image[0].detach().cpu().numpy() * 255).clip(0, 255).astype(np.uint8)
|
||||||
|
return Image.fromarray(arr)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def render_preview(regions, width, height, bg=None):
|
||||||
|
if bg is not None:
|
||||||
|
iw, ih = bg.size
|
||||||
|
long_edge = max(iw, ih) or 1
|
||||||
|
scale = min(1.0, _PREVIEW_LONG_EDGE / long_edge)
|
||||||
|
rw, rh = max(1, round(iw * scale)), max(1, round(ih * scale))
|
||||||
|
base = bg.convert("RGB").resize((rw, rh), Image.LANCZOS)
|
||||||
|
base = ImageEnhance.Brightness(base).enhance(_PREVIEW_DIM)
|
||||||
|
img = base.convert("RGBA")
|
||||||
|
else:
|
||||||
|
long_edge = max(width, height) or 1
|
||||||
|
scale = min(1.0, _PREVIEW_LONG_EDGE / long_edge)
|
||||||
|
rw, rh = max(1, round(width * scale)), max(1, round(height * scale))
|
||||||
|
grey = round(_PREVIEW_DIM * 128)
|
||||||
|
img = Image.new("RGBA", (rw, rh), (grey, grey, grey, 255))
|
||||||
|
|
||||||
|
overlay = Image.new("RGBA", (rw, rh), (0, 0, 0, 0))
|
||||||
|
draw = ImageDraw.Draw(overlay)
|
||||||
|
fs = max(10, round(rh / 64))
|
||||||
|
font = _font(fs)
|
||||||
|
tag_font = _font(max(9, fs - 2))
|
||||||
|
line_h = fs + 2
|
||||||
|
|
||||||
|
for i, region in enumerate(regions):
|
||||||
|
if not isinstance(region, dict):
|
||||||
|
continue
|
||||||
|
palette = [c for c in (region.get("palette") or []) if c]
|
||||||
|
r, g, b = hex_to_rgb(palette[0]) if palette else (140, 140, 140)
|
||||||
|
x1 = max(0, min(rw, round(region.get("x", 0) * rw)))
|
||||||
|
y1 = max(0, min(rh, round(region.get("y", 0) * rh)))
|
||||||
|
x2 = max(0, min(rw, round((region.get("x", 0) + region.get("w", 0)) * rw)))
|
||||||
|
y2 = max(0, min(rh, round((region.get("y", 0) + region.get("h", 0)) * rh)))
|
||||||
|
if x2 < x1:
|
||||||
|
x1, x2 = x2, x1
|
||||||
|
if y2 < y1:
|
||||||
|
y1, y2 = y2, y1
|
||||||
|
|
||||||
|
draw.rectangle([x1, y1, x2, y2], outline=(r, g, b, 255), width=2)
|
||||||
|
|
||||||
|
swatches = palette[:5]
|
||||||
|
if swatches and (x2 - x1) > 2:
|
||||||
|
sh = max(5, fs // 2)
|
||||||
|
seg = (x2 - x1) / len(swatches)
|
||||||
|
for p, hexc in enumerate(swatches):
|
||||||
|
sx = x1 + round(p * seg)
|
||||||
|
draw.rectangle([sx, y1, x1 + round((p + 1) * seg), y1 + sh], fill=hex_to_rgb(hexc))
|
||||||
|
|
||||||
|
etype = "text" if region.get("type") == "text" else "obj"
|
||||||
|
tag = str(i + 1).zfill(2)
|
||||||
|
tw = draw.textlength(tag, font=tag_font)
|
||||||
|
draw.rectangle([x1, y1, x1 + tw + 6, y1 + fs + 2], fill=(r, g, b, 255))
|
||||||
|
tag_fill = (0, 0, 0, 255) if (0.299 * r + 0.587 * g + 0.114 * b) > 140 else (255, 255, 255, 255)
|
||||||
|
draw.text((x1 + 3, y1 + 1), tag, fill=tag_fill, font=tag_font)
|
||||||
|
|
||||||
|
body = region.get("desc", "") or ""
|
||||||
|
if etype == "text" and region.get("text"):
|
||||||
|
body = '"%s"%s' % (region["text"], " — " + body if body else "")
|
||||||
|
if body and (x2 - x1) > 8:
|
||||||
|
ty = y1 + fs + 5
|
||||||
|
for line in _wrap(draw, body, font, x2 - x1 - 8):
|
||||||
|
if ty > y2:
|
||||||
|
break
|
||||||
|
draw.text((x1 + 4, ty), line, fill=readable_color((r, g, b)) + (255,), font=font)
|
||||||
|
ty += line_h
|
||||||
|
|
||||||
|
composed = Image.alpha_composite(img, overlay).convert("RGB")
|
||||||
|
arr = np.asarray(composed, dtype=np.float32) / 255.0
|
||||||
|
return torch.from_numpy(arr).unsqueeze(0)
|
||||||
|
|
||||||
|
|
||||||
|
def boxes_to_regions(boxes, width: int, height: int) -> list:
|
||||||
|
regions: list = []
|
||||||
|
if not isinstance(boxes, list):
|
||||||
|
return regions
|
||||||
|
for box in boxes:
|
||||||
|
if not isinstance(box, dict):
|
||||||
|
continue
|
||||||
|
meta = box.get("metadata")
|
||||||
|
meta = meta if isinstance(meta, dict) else {}
|
||||||
|
regions.append({
|
||||||
|
**pixels_to_fractions(box, width, height),
|
||||||
|
"type": meta.get("type", "obj"),
|
||||||
|
"text": meta.get("text", ""),
|
||||||
|
"desc": meta.get("desc", ""),
|
||||||
|
"palette": meta.get("palette", []),
|
||||||
|
})
|
||||||
|
return regions
|
||||||
|
|
||||||
|
|
||||||
|
def _norm_bbox(region: dict) -> list[int]:
|
||||||
|
def grid(value: float) -> int:
|
||||||
|
return max(0, min(1000, round(value * 1000)))
|
||||||
|
|
||||||
|
x, y = region.get("x", 0.0), region.get("y", 0.0)
|
||||||
|
w, h = region.get("w", 0.0), region.get("h", 0.0)
|
||||||
|
ymin, xmin, ymax, xmax = grid(y), grid(x), grid(y + h), grid(x + w)
|
||||||
|
if ymin > ymax:
|
||||||
|
ymin, ymax = ymax, ymin
|
||||||
|
if xmin > xmax:
|
||||||
|
xmin, xmax = xmax, xmin
|
||||||
|
return [ymin, xmin, ymax, xmax]
|
||||||
|
|
||||||
|
|
||||||
|
def build_elements(regions: list) -> list:
|
||||||
|
elements = []
|
||||||
|
for region in regions:
|
||||||
|
if not isinstance(region, dict):
|
||||||
|
continue
|
||||||
|
etype = "text" if region.get("type") == "text" else "obj"
|
||||||
|
element = {"type": etype}
|
||||||
|
element["bbox"] = _norm_bbox(region)
|
||||||
|
if etype == "text":
|
||||||
|
element["text"] = region.get("text", "")
|
||||||
|
element["desc"] = region.get("desc", "")
|
||||||
|
palette = normalize_palette(region.get("palette", []))
|
||||||
|
if palette:
|
||||||
|
element["color_palette"] = palette[:5]
|
||||||
|
elements.append(element)
|
||||||
|
return elements
|
||||||
|
|
||||||
|
|
||||||
|
class CreateBoundingBoxes(io.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
editor_state = io.BoundingBoxes.Input(
|
||||||
|
"editor_state",
|
||||||
|
socketless=False,
|
||||||
|
tooltip="Draw bounding boxes and set each box type, text, description, color palette. Start with background element first and foreground last.",
|
||||||
|
)
|
||||||
|
return io.Schema(
|
||||||
|
node_id="CreateBoundingBoxes",
|
||||||
|
display_name="Create Bounding Boxes",
|
||||||
|
category="utilities",
|
||||||
|
description="Draw bounding boxes in a canvas. Outputs Ideogram prompt elements, pixel-space bounding boxes, and a preview image.",
|
||||||
|
inputs=[
|
||||||
|
io.Image.Input(
|
||||||
|
"background",
|
||||||
|
optional=True,
|
||||||
|
tooltip="Optional image used as background in the canvas and preview.",
|
||||||
|
),
|
||||||
|
io.Int.Input("width", default=1024, min=64, max=16384, step=16,
|
||||||
|
tooltip="Width of the canvas and the pixel grid for the bounding boxes."),
|
||||||
|
io.Int.Input("height", default=1024, min=64, max=16384, step=16,
|
||||||
|
tooltip="Height of the canvas and the pixel grid for the bounding boxes."),
|
||||||
|
editor_state,
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.Image.Output(display_name="preview"),
|
||||||
|
io.BoundingBox.Output(display_name="bboxes"),
|
||||||
|
io.Array.Output(display_name="elements"),
|
||||||
|
],
|
||||||
|
is_experimental=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute(cls, width, height, editor_state=None, background=None) -> io.NodeOutput:
|
||||||
|
regions = boxes_to_regions(editor_state, width, height)
|
||||||
|
preview = render_preview(regions, width, height, _bg_from_image(background))
|
||||||
|
return io.NodeOutput(
|
||||||
|
preview,
|
||||||
|
fractions_to_bbox_frame(regions, width, height),
|
||||||
|
build_elements(regions),
|
||||||
|
ui={"dims": [width, height]},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BoundingBoxesExtension(ComfyExtension):
|
||||||
|
@override
|
||||||
|
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
||||||
|
return [CreateBoundingBoxes]
|
||||||
|
|
||||||
|
|
||||||
|
async def comfy_entrypoint() -> BoundingBoxesExtension:
|
||||||
|
return BoundingBoxesExtension()
|
||||||
@ -1,5 +1,6 @@
|
|||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
from comfy_api.latest import ComfyExtension, io
|
from comfy_api.latest import ComfyExtension, io
|
||||||
|
from comfy_extras.color_util import hex_to_rgb
|
||||||
|
|
||||||
|
|
||||||
class ColorToRGBInt(io.ComfyNode):
|
class ColorToRGBInt(io.ComfyNode):
|
||||||
@ -24,9 +25,11 @@ class ColorToRGBInt(io.ComfyNode):
|
|||||||
# expect format #RRGGBB
|
# expect format #RRGGBB
|
||||||
if len(color) != 7 or color[0] != "#":
|
if len(color) != 7 or color[0] != "#":
|
||||||
raise ValueError("Color must be in format #RRGGBB")
|
raise ValueError("Color must be in format #RRGGBB")
|
||||||
r = int(color[1:3], 16)
|
try:
|
||||||
g = int(color[3:5], 16)
|
int(color[1:], 16)
|
||||||
b = int(color[5:7], 16)
|
except ValueError:
|
||||||
|
raise ValueError("Color must be in format #RRGGBB") from None
|
||||||
|
r, g, b = hex_to_rgb(color)
|
||||||
|
|
||||||
rgb_int = r * 256 * 256 + g * 256 + b
|
rgb_int = r * 256 * 256 + g * 256 + b
|
||||||
return io.NodeOutput(rgb_int, color)
|
return io.NodeOutput(rgb_int, color)
|
||||||
|
|||||||
77
comfy_extras/nodes_json_prompt.py
Normal file
77
comfy_extras/nodes_json_prompt.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
|
from comfy_api.latest import ComfyExtension, io
|
||||||
|
from comfy_extras.color_util import normalize_palette
|
||||||
|
|
||||||
|
|
||||||
|
class BuildJsonPromptIdeogram(io.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
color_palette = io.Colors.Input(
|
||||||
|
"color_palette",
|
||||||
|
socketless=False,
|
||||||
|
tooltip="Hex color codes that steer the image's dominant colors. Up to 16 entries.",
|
||||||
|
)
|
||||||
|
return io.Schema(
|
||||||
|
node_id="BuildJsonPromptIdeogram",
|
||||||
|
display_name="Build JSON Prompt (Ideogram)",
|
||||||
|
category="text",
|
||||||
|
description="Build a JSON prompt for the Ideogram 4 model.",
|
||||||
|
inputs=[
|
||||||
|
io.Array.Input("element", tooltip="Prompt elements from the node Create Bounding Boxes."),
|
||||||
|
io.String.Input("high_level_description", multiline=True, default="",
|
||||||
|
tooltip="Optional description of the image in one or two sentences. Strongly recommended."),
|
||||||
|
io.String.Input("background", multiline=True, default="",
|
||||||
|
tooltip="Mandatory description of the image background or environment."),
|
||||||
|
io.DynamicCombo.Input("style", options=[
|
||||||
|
io.DynamicCombo.Option("none", []),
|
||||||
|
io.DynamicCombo.Option("photo", [io.String.Input("photo", default="", tooltip="Camera or lens details for photographic outputs (e.g. 35mm, f/1.4, bokeh).")]),
|
||||||
|
io.DynamicCombo.Option("art_style", [io.String.Input("art_style", default="", tooltip="Art style description (e.g. flat vector illustration, bold outlines).")]),
|
||||||
|
]),
|
||||||
|
io.String.Input("aesthetics", default="", tooltip="Mandatory aesthetic keywords (e.g. moody, cinematic, desaturated)."),
|
||||||
|
io.String.Input("lighting", default="", tooltip="Mandatory lighting description (e.g. golden hour, rim light, dramatic shadows)."),
|
||||||
|
io.String.Input("medium", default="", tooltip="Mandatory medium type (e.g. photograph, illustration, 3d_render, painting, graphic_design). When style = photo, set to photograph."),
|
||||||
|
color_palette,
|
||||||
|
],
|
||||||
|
outputs=[io.Dict.Output(display_name="prompt")],
|
||||||
|
is_experimental=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute(cls, element, style, high_level_description="", background="",
|
||||||
|
aesthetics="", lighting="", medium="", color_palette=None) -> io.NodeOutput:
|
||||||
|
elements = element if isinstance(element, list) else []
|
||||||
|
kind = style.get("style", "none") if isinstance(style, dict) else "none"
|
||||||
|
photo = style.get("photo", "") if isinstance(style, dict) else ""
|
||||||
|
art_style = style.get("art_style", "") if isinstance(style, dict) else ""
|
||||||
|
palette = normalize_palette(color_palette or [])
|
||||||
|
|
||||||
|
caption: dict = {}
|
||||||
|
if high_level_description.strip():
|
||||||
|
caption["high_level_description"] = high_level_description
|
||||||
|
if kind != "none":
|
||||||
|
style_desc: dict = {"aesthetics": aesthetics, "lighting": lighting}
|
||||||
|
if kind == "photo":
|
||||||
|
style_desc["photo"] = photo
|
||||||
|
style_desc["medium"] = medium
|
||||||
|
else:
|
||||||
|
style_desc["medium"] = medium
|
||||||
|
style_desc["art_style"] = art_style
|
||||||
|
if palette:
|
||||||
|
style_desc["color_palette"] = palette
|
||||||
|
caption["style_description"] = style_desc
|
||||||
|
caption["compositional_deconstruction"] = {
|
||||||
|
"background": background,
|
||||||
|
"elements": elements,
|
||||||
|
}
|
||||||
|
return io.NodeOutput(caption)
|
||||||
|
|
||||||
|
|
||||||
|
class JsonPromptExtension(ComfyExtension):
|
||||||
|
@override
|
||||||
|
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
||||||
|
return [BuildJsonPromptIdeogram]
|
||||||
|
|
||||||
|
|
||||||
|
async def comfy_entrypoint() -> JsonPromptExtension:
|
||||||
|
return JsonPromptExtension()
|
||||||
@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ class SeedNode(io.ComfyNode):
|
|||||||
search_aliases=["seed", "random"],
|
search_aliases=["seed", "random"],
|
||||||
category="utilities",
|
category="utilities",
|
||||||
inputs=[
|
inputs=[
|
||||||
io.Int.Input("seed", min=0, max=sys.maxsize, control_after_generate=io.ControlAfterGenerate.fixed, component="SetRandomInt"),
|
io.Int.Input("seed", min=0, max=sys.maxsize, control_after_generate=io.ControlAfterGenerate.fixed),
|
||||||
],
|
],
|
||||||
outputs=[io.Int.Output(display_name="seed")],
|
outputs=[io.Int.Output(display_name="seed")],
|
||||||
)
|
)
|
||||||
|
|||||||
@ -440,6 +440,57 @@ class JsonExtractString(io.ComfyNode):
|
|||||||
except (json.JSONDecodeError, TypeError):
|
except (json.JSONDecodeError, TypeError):
|
||||||
return io.NodeOutput("")
|
return io.NodeOutput("")
|
||||||
|
|
||||||
|
|
||||||
|
def _dump_json(value, indent):
|
||||||
|
return json.dumps(value, ensure_ascii=False, indent=indent or None)
|
||||||
|
|
||||||
|
|
||||||
|
class ConvertDictionaryToString(io.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return io.Schema(
|
||||||
|
node_id="ConvertDictionaryToString",
|
||||||
|
display_name="Convert Dictionary to String",
|
||||||
|
category="text",
|
||||||
|
search_aliases=["json", "dict to json", "stringify", "serialize", "dict to string"],
|
||||||
|
inputs=[
|
||||||
|
io.Dict.Input("dictionary"),
|
||||||
|
io.Int.Input("indent", default=2, min=0, max=8,
|
||||||
|
tooltip="Spaces per indent level. 0 produces compact single-line string."),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.String.Output(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute(cls, dictionary, indent=2):
|
||||||
|
return io.NodeOutput(_dump_json(dictionary, indent))
|
||||||
|
|
||||||
|
|
||||||
|
class ConvertArrayToString(io.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return io.Schema(
|
||||||
|
node_id="ConvertArrayToString",
|
||||||
|
display_name="Convert Array to String",
|
||||||
|
category="text",
|
||||||
|
search_aliases=["json", "list to json", "stringify", "serialize", "list to string", "array to json"],
|
||||||
|
inputs=[
|
||||||
|
io.Array.Input("array"),
|
||||||
|
io.Int.Input("indent", default=2, min=0, max=8,
|
||||||
|
tooltip="Spaces per indent level. 0 produces compact single-line string."),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.String.Output(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute(cls, array, indent=2):
|
||||||
|
return io.NodeOutput(_dump_json(array, indent))
|
||||||
|
|
||||||
|
|
||||||
class StringExtension(ComfyExtension):
|
class StringExtension(ComfyExtension):
|
||||||
@override
|
@override
|
||||||
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
||||||
@ -457,6 +508,8 @@ class StringExtension(ComfyExtension):
|
|||||||
RegexExtract,
|
RegexExtract,
|
||||||
RegexReplace,
|
RegexReplace,
|
||||||
JsonExtractString,
|
JsonExtractString,
|
||||||
|
ConvertDictionaryToString,
|
||||||
|
ConvertArrayToString,
|
||||||
]
|
]
|
||||||
|
|
||||||
async def comfy_entrypoint() -> StringExtension:
|
async def comfy_entrypoint() -> StringExtension:
|
||||||
|
|||||||
2
nodes.py
2
nodes.py
@ -2374,6 +2374,8 @@ async def init_builtin_extra_nodes():
|
|||||||
"nodes_images.py",
|
"nodes_images.py",
|
||||||
"nodes_video_model.py",
|
"nodes_video_model.py",
|
||||||
"nodes_ideogram4.py",
|
"nodes_ideogram4.py",
|
||||||
|
"nodes_bounding_boxes.py",
|
||||||
|
"nodes_json_prompt.py",
|
||||||
"nodes_train.py",
|
"nodes_train.py",
|
||||||
"nodes_dataset.py",
|
"nodes_dataset.py",
|
||||||
"nodes_sag.py",
|
"nodes_sag.py",
|
||||||
|
|||||||
14
openapi.yaml
14
openapi.yaml
@ -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:
|
||||||
|
|||||||
@ -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.7
|
||||||
comfyui-embedded-docs==0.5.5
|
comfyui-embedded-docs==0.5.5
|
||||||
torch
|
torch
|
||||||
torchsde
|
torchsde
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user