mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-05-04 21:36:14 +02:00
dev: RemoteComboOptions example
Signed-off-by: bigcat88 <bigcat88@icloud.com>
This commit is contained in:
parent
defb663b94
commit
7288264c7e
@ -132,6 +132,10 @@ class GetAssetResponse(BaseModel):
|
||||
error: TaskStatusError | None = Field(None)
|
||||
|
||||
|
||||
class SeedanceCreateVisualValidateSessionRequest(BaseModel):
|
||||
name: str | None = Field(None, max_length=64)
|
||||
|
||||
|
||||
class SeedanceCreateVisualValidateSessionResponse(BaseModel):
|
||||
session_id: str = Field(...)
|
||||
h5_link: str = Field(...)
|
||||
@ -141,6 +145,7 @@ class SeedanceGetVisualValidateSessionResponse(BaseModel):
|
||||
session_id: str = Field(...)
|
||||
status: str = Field(...)
|
||||
group_id: str | None = Field(None)
|
||||
name: str | None = Field(None)
|
||||
error_code: str | None = Field(None)
|
||||
error_message: str | None = Field(None)
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ from comfy_api_nodes.apis.bytedance import (
|
||||
Seedance2TaskCreationRequest,
|
||||
SeedanceCreateAssetRequest,
|
||||
SeedanceCreateAssetResponse,
|
||||
SeedanceCreateVisualValidateSessionRequest,
|
||||
SeedanceCreateVisualValidateSessionResponse,
|
||||
SeedanceGetVisualValidateSessionResponse,
|
||||
SeedanceVirtualLibraryCreateAssetRequest,
|
||||
@ -196,11 +197,16 @@ def _rewrite_asset_refs(prompt: str, labels: dict[int, str]) -> str:
|
||||
return _ASSET_REF_RE.sub(_sub, prompt)
|
||||
|
||||
|
||||
async def _obtain_group_id_via_h5_auth(cls: type[IO.ComfyNode]) -> str:
|
||||
async def _obtain_group_id_via_h5_auth(
|
||||
cls: type[IO.ComfyNode],
|
||||
group_name: str | None = None,
|
||||
) -> str:
|
||||
payload = SeedanceCreateVisualValidateSessionRequest(name=group_name)
|
||||
session = await sync_op(
|
||||
cls,
|
||||
ApiEndpoint(path="/proxy/seedance/visual-validate/sessions", method="POST"),
|
||||
response_model=SeedanceCreateVisualValidateSessionResponse,
|
||||
data=payload,
|
||||
)
|
||||
logger.warning("Seedance authentication required. Open link: %s", session.h5_link)
|
||||
|
||||
@ -229,10 +235,15 @@ async def _obtain_group_id_via_h5_auth(cls: type[IO.ComfyNode]) -> str:
|
||||
return result.group_id
|
||||
|
||||
|
||||
async def _resolve_group_id(cls: type[IO.ComfyNode], group_id: str) -> str:
|
||||
async def _resolve_group_id(
|
||||
cls: type[IO.ComfyNode],
|
||||
group_id: str,
|
||||
group_name: str | None = None,
|
||||
) -> str:
|
||||
if group_id and group_id.strip():
|
||||
return group_id.strip()
|
||||
return await _obtain_group_id_via_h5_auth(cls)
|
||||
label = (group_name or "").strip() or None
|
||||
return await _obtain_group_id_via_h5_auth(cls, group_name=label)
|
||||
|
||||
|
||||
async def _create_seedance_asset(
|
||||
@ -1936,6 +1947,43 @@ async def process_video_task(
|
||||
return IO.NodeOutput(await download_url_to_video_output(response.content.video_url))
|
||||
|
||||
|
||||
def _seedance_group_picker_input() -> IO.Combo.Input:
|
||||
"""Combo populated from /proxy/seedance/groups. Empty selection triggers H5 enrollment."""
|
||||
return IO.Combo.Input(
|
||||
"group_id",
|
||||
default="",
|
||||
tooltip=(
|
||||
"Pick an existing verified group, or leave empty to run real-person H5 "
|
||||
"authentication and create a new group."
|
||||
),
|
||||
remote_combo=IO.RemoteComboOptions(
|
||||
route="/proxy/seedance/groups",
|
||||
response_key="groups",
|
||||
item_schema=IO.RemoteItemSchema(
|
||||
value_field="group_id",
|
||||
label_field="name",
|
||||
description_field="created_at",
|
||||
search_fields=["name", "group_id"],
|
||||
),
|
||||
refresh=60_000,
|
||||
),
|
||||
optional=True,
|
||||
)
|
||||
|
||||
|
||||
def _seedance_group_name_input() -> IO.String.Input:
|
||||
return IO.String.Input(
|
||||
"group_name",
|
||||
default="",
|
||||
tooltip=(
|
||||
"Optional label for a new group. Used only when group_id is empty; the label is "
|
||||
"shown later in the group picker so you can identify this group at a glance. "
|
||||
"Up to 64 characters."
|
||||
),
|
||||
optional=True,
|
||||
)
|
||||
|
||||
|
||||
class ByteDanceCreateImageAsset(IO.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
@ -1946,22 +1994,14 @@ class ByteDanceCreateImageAsset(IO.ComfyNode):
|
||||
category="api node/image/ByteDance",
|
||||
description=(
|
||||
"Create a Seedance 2.0 personal image asset. Uploads the input image and "
|
||||
"registers it in the given asset group. If group_id is empty, runs a real-person "
|
||||
"H5 authentication flow to create a new group before adding the asset."
|
||||
"registers it in the selected asset group. Leave group_id empty to run a "
|
||||
"real-person H5 authentication flow and create a new group; provide group_name "
|
||||
"to label the new group."
|
||||
),
|
||||
inputs=[
|
||||
IO.Image.Input("image", tooltip="Image to register as a personal asset."),
|
||||
IO.String.Input(
|
||||
"group_id",
|
||||
default="",
|
||||
tooltip="Reuse an existing Seedance asset group ID to skip repeated human verification for the "
|
||||
"same person. Leave empty to run real-person authentication in the browser and create a new group.",
|
||||
),
|
||||
# IO.String.Input(
|
||||
# "name",
|
||||
# default="",
|
||||
# tooltip="Asset name (up to 64 characters).",
|
||||
# ),
|
||||
_seedance_group_picker_input(),
|
||||
_seedance_group_name_input(),
|
||||
],
|
||||
outputs=[
|
||||
IO.String.Output(display_name="asset_id"),
|
||||
@ -1980,13 +2020,11 @@ class ByteDanceCreateImageAsset(IO.ComfyNode):
|
||||
cls,
|
||||
image: Input.Image,
|
||||
group_id: str = "",
|
||||
# name: str = "",
|
||||
group_name: str = "",
|
||||
) -> IO.NodeOutput:
|
||||
# if len(name) > 64:
|
||||
# raise ValueError("Name of asset can not be greater then 64 symbols")
|
||||
validate_image_dimensions(image, min_width=300, max_width=6000, min_height=300, max_height=6000)
|
||||
validate_image_aspect_ratio(image, min_ratio=(0.4, 1), max_ratio=(2.5, 1))
|
||||
resolved_group = await _resolve_group_id(cls, group_id)
|
||||
resolved_group = await _resolve_group_id(cls, group_id, group_name=group_name)
|
||||
asset_id = await _create_seedance_asset(
|
||||
cls,
|
||||
group_id=resolved_group,
|
||||
@ -2013,22 +2051,14 @@ class ByteDanceCreateVideoAsset(IO.ComfyNode):
|
||||
category="api node/video/ByteDance",
|
||||
description=(
|
||||
"Create a Seedance 2.0 personal video asset. Uploads the input video and "
|
||||
"registers it in the given asset group. If group_id is empty, runs a real-person "
|
||||
"H5 authentication flow to create a new group before adding the asset."
|
||||
"registers it in the selected asset group. Leave group_id empty to run a "
|
||||
"real-person H5 authentication flow and create a new group; provide group_name "
|
||||
"to label the new group."
|
||||
),
|
||||
inputs=[
|
||||
IO.Video.Input("video", tooltip="Video to register as a personal asset."),
|
||||
IO.String.Input(
|
||||
"group_id",
|
||||
default="",
|
||||
tooltip="Reuse an existing Seedance asset group ID to skip repeated human verification for the "
|
||||
"same person. Leave empty to run real-person authentication in the browser and create a new group.",
|
||||
),
|
||||
# IO.String.Input(
|
||||
# "name",
|
||||
# default="",
|
||||
# tooltip="Asset name (up to 64 characters).",
|
||||
# ),
|
||||
_seedance_group_picker_input(),
|
||||
_seedance_group_name_input(),
|
||||
],
|
||||
outputs=[
|
||||
IO.String.Output(display_name="asset_id"),
|
||||
@ -2047,10 +2077,8 @@ class ByteDanceCreateVideoAsset(IO.ComfyNode):
|
||||
cls,
|
||||
video: Input.Video,
|
||||
group_id: str = "",
|
||||
# name: str = "",
|
||||
group_name: str = "",
|
||||
) -> IO.NodeOutput:
|
||||
# if len(name) > 64:
|
||||
# raise ValueError("Name of asset can not be greater then 64 symbols")
|
||||
validate_video_duration(video, min_duration=2, max_duration=15)
|
||||
validate_video_dimensions(video, min_width=300, max_width=6000, min_height=300, max_height=6000)
|
||||
|
||||
@ -2069,7 +2097,7 @@ class ByteDanceCreateVideoAsset(IO.ComfyNode):
|
||||
if not (24 <= fps <= 60):
|
||||
raise ValueError(f"Asset video FPS must be in [24, 60], got {fps:.2f}.")
|
||||
|
||||
resolved_group = await _resolve_group_id(cls, group_id)
|
||||
resolved_group = await _resolve_group_id(cls, group_id, group_name=group_name)
|
||||
asset_id = await _create_seedance_asset(
|
||||
cls,
|
||||
group_id=resolved_group,
|
||||
@ -2086,6 +2114,92 @@ class ByteDanceCreateVideoAsset(IO.ComfyNode):
|
||||
return IO.NodeOutput(asset_id, resolved_group)
|
||||
|
||||
|
||||
def _seedance_asset_picker_input(asset_type: str, preview_type: str) -> IO.Combo.Input:
|
||||
"""Combo populated from /proxy/seedance/assets, scoped to one asset_type."""
|
||||
return IO.Combo.Input(
|
||||
"asset_id",
|
||||
tooltip=(
|
||||
f"Pick a previously-created Seedance {asset_type.lower()} asset. The dropdown shows "
|
||||
"your assets across all your verified groups; type a group name to filter."
|
||||
),
|
||||
remote_combo=IO.RemoteComboOptions(
|
||||
route=f"/proxy/seedance/assets?asset_type={asset_type}",
|
||||
response_key="assets",
|
||||
item_schema=IO.RemoteItemSchema(
|
||||
value_field="asset_id",
|
||||
label_field="name",
|
||||
description_field="group_name",
|
||||
preview_url_field="url",
|
||||
preview_type=preview_type,
|
||||
search_fields=["name", "asset_id", "group_name", "group_id"],
|
||||
),
|
||||
refresh=60_000,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class ByteDanceSelectImageAsset(IO.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls) -> IO.Schema:
|
||||
return IO.Schema(
|
||||
node_id="ByteDanceSelectImageAsset",
|
||||
display_name="ByteDance Select Image Asset",
|
||||
category="api node/image/ByteDance",
|
||||
description=(
|
||||
"Pick a previously-created Seedance image asset. Outputs the selected asset_id "
|
||||
"for use with downstream Seedance 2.0 reference/first-last-frame nodes."
|
||||
),
|
||||
inputs=[
|
||||
_seedance_asset_picker_input("Image", "image"),
|
||||
],
|
||||
outputs=[IO.String.Output(display_name="asset_id")],
|
||||
hidden=[
|
||||
IO.Hidden.auth_token_comfy_org,
|
||||
IO.Hidden.api_key_comfy_org,
|
||||
IO.Hidden.unique_id,
|
||||
],
|
||||
# is_api_node=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(cls, asset_id: str) -> IO.NodeOutput:
|
||||
if not asset_id or not asset_id.strip():
|
||||
raise ValueError("asset_id is required. Pick an asset from the dropdown.")
|
||||
return IO.NodeOutput(asset_id.strip())
|
||||
|
||||
|
||||
class ByteDanceSelectVideoAsset(IO.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls) -> IO.Schema:
|
||||
return IO.Schema(
|
||||
node_id="ByteDanceSelectVideoAsset",
|
||||
display_name="ByteDance Select Video Asset",
|
||||
category="api node/video/ByteDance",
|
||||
description=(
|
||||
"Pick a previously-created Seedance video asset. Outputs the selected asset_id "
|
||||
"for use with downstream Seedance 2.0 reference/first-last-frame nodes."
|
||||
),
|
||||
inputs=[
|
||||
_seedance_asset_picker_input("Video", "video"),
|
||||
],
|
||||
outputs=[IO.String.Output(display_name="asset_id")],
|
||||
hidden=[
|
||||
IO.Hidden.auth_token_comfy_org,
|
||||
IO.Hidden.api_key_comfy_org,
|
||||
IO.Hidden.unique_id,
|
||||
],
|
||||
# is_api_node=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(cls, asset_id: str) -> IO.NodeOutput:
|
||||
if not asset_id or not asset_id.strip():
|
||||
raise ValueError("asset_id is required. Pick an asset from the dropdown.")
|
||||
return IO.NodeOutput(asset_id.strip())
|
||||
|
||||
|
||||
class ByteDanceExtension(ComfyExtension):
|
||||
@override
|
||||
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
||||
@ -2101,6 +2215,8 @@ class ByteDanceExtension(ComfyExtension):
|
||||
ByteDance2ReferenceNode,
|
||||
ByteDanceCreateImageAsset,
|
||||
ByteDanceCreateVideoAsset,
|
||||
ByteDanceSelectImageAsset,
|
||||
ByteDanceSelectVideoAsset,
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -233,6 +233,44 @@ class ElevenLabsVoiceSelector(IO.ComfyNode):
|
||||
return IO.NodeOutput(voice_id)
|
||||
|
||||
|
||||
class ElevenLabsRichVoiceSelector(IO.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls) -> IO.Schema:
|
||||
return IO.Schema(
|
||||
node_id="ElevenLabsRichVoiceSelector",
|
||||
display_name="ElevenLabs Voice Selector (Rich)",
|
||||
category="api node/audio/ElevenLabs",
|
||||
description="Select an ElevenLabs voice with audio preview and rich metadata.",
|
||||
inputs=[
|
||||
IO.Combo.Input(
|
||||
"voice",
|
||||
remote_combo=IO.RemoteComboOptions(
|
||||
route="/proxy/elevenlabs/v2/voices?page_size=100",
|
||||
response_key="items",
|
||||
refresh_button=True,
|
||||
refresh=43200000,
|
||||
item_schema=IO.RemoteItemSchema(
|
||||
value_field="voice_id",
|
||||
label_field="name",
|
||||
preview_url_field="preview_url",
|
||||
preview_type="audio",
|
||||
search_fields=["name", "labels.gender", "labels.accent", "labels.use_case"],
|
||||
),
|
||||
),
|
||||
tooltip="Choose a voice with audio preview.",
|
||||
),
|
||||
],
|
||||
outputs=[
|
||||
IO.Custom(ELEVENLABS_VOICE).Output(display_name="voice"),
|
||||
],
|
||||
is_api_node=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, voice: str) -> IO.NodeOutput:
|
||||
return IO.NodeOutput(voice) # voice is already the voice_id from item_schema.value_field
|
||||
|
||||
|
||||
class ElevenLabsTextToSpeech(IO.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls) -> IO.Schema:
|
||||
@ -911,6 +949,7 @@ class ElevenLabsExtension(ComfyExtension):
|
||||
return [
|
||||
ElevenLabsSpeechToText,
|
||||
ElevenLabsVoiceSelector,
|
||||
ElevenLabsRichVoiceSelector,
|
||||
ElevenLabsTextToSpeech,
|
||||
ElevenLabsAudioIsolation,
|
||||
ElevenLabsTextToSoundEffects,
|
||||
|
||||
@ -3288,6 +3288,53 @@ class KlingAvatarNode(IO.ComfyNode):
|
||||
return IO.NodeOutput(await download_url_to_video_output(final_response.data.task_result.videos[0].url))
|
||||
|
||||
|
||||
KLING_ELEMENT_ID = "KLING_ELEMENT_ID"
|
||||
|
||||
|
||||
class KlingElementSelector(IO.ComfyNode):
|
||||
"""Select a Kling preset element (character, scene, effect, etc.) for use in video generation."""
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls) -> IO.Schema:
|
||||
return IO.Schema(
|
||||
node_id="KlingElementSelector",
|
||||
display_name="Kling Element Selector",
|
||||
category="api node/video/Kling",
|
||||
description="Browse and select a Kling preset element with image preview. Elements provide consistent characters, scenes, costumes, and effects for video generation.",
|
||||
inputs=[
|
||||
IO.Combo.Input(
|
||||
"element",
|
||||
remote_combo=IO.RemoteComboOptions(
|
||||
route="/proxy/kling/v1/general/advanced-presets-elements",
|
||||
refresh_button=True,
|
||||
refresh=43200000,
|
||||
response_key="data",
|
||||
item_schema=IO.RemoteItemSchema(
|
||||
value_field="task_result.elements.0.element_id",
|
||||
label_field="task_result.elements.0.element_name",
|
||||
preview_url_field="task_result.elements.0.element_image_list.frontal_image",
|
||||
preview_type="image",
|
||||
description_field="task_result.elements.0.element_description",
|
||||
search_fields=["task_result.elements.0.element_name", "task_result.elements.0.element_description"],
|
||||
),
|
||||
),
|
||||
tooltip="Select a preset element to use in video generation.",
|
||||
),
|
||||
],
|
||||
outputs=[IO.Custom(KLING_ELEMENT_ID).Output(display_name="element_id")],
|
||||
hidden=[
|
||||
IO.Hidden.auth_token_comfy_org,
|
||||
IO.Hidden.api_key_comfy_org,
|
||||
IO.Hidden.unique_id,
|
||||
],
|
||||
is_api_node=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(cls, element: str) -> IO.NodeOutput:
|
||||
return IO.NodeOutput(element)
|
||||
|
||||
|
||||
class KlingExtension(ComfyExtension):
|
||||
@override
|
||||
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
||||
@ -3317,6 +3364,7 @@ class KlingExtension(ComfyExtension):
|
||||
KlingVideoNode,
|
||||
KlingFirstLastFrameNode,
|
||||
KlingAvatarNode,
|
||||
KlingElementSelector,
|
||||
]
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user