From 7288264c7eadc1294823eb689efde9e8ec5b1eac Mon Sep 17 00:00:00 2001 From: bigcat88 Date: Thu, 16 Apr 2026 15:36:59 +0300 Subject: [PATCH] dev: RemoteComboOptions example Signed-off-by: bigcat88 --- comfy_api_nodes/apis/bytedance.py | 5 + comfy_api_nodes/nodes_bytedance.py | 190 ++++++++++++++++++++++------ comfy_api_nodes/nodes_elevenlabs.py | 39 ++++++ comfy_api_nodes/nodes_kling.py | 48 +++++++ 4 files changed, 245 insertions(+), 37 deletions(-) diff --git a/comfy_api_nodes/apis/bytedance.py b/comfy_api_nodes/apis/bytedance.py index c05bd6893..b66a0342f 100644 --- a/comfy_api_nodes/apis/bytedance.py +++ b/comfy_api_nodes/apis/bytedance.py @@ -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) diff --git a/comfy_api_nodes/nodes_bytedance.py b/comfy_api_nodes/nodes_bytedance.py index 2f241a775..1e43e31b1 100644 --- a/comfy_api_nodes/nodes_bytedance.py +++ b/comfy_api_nodes/nodes_bytedance.py @@ -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, ] diff --git a/comfy_api_nodes/nodes_elevenlabs.py b/comfy_api_nodes/nodes_elevenlabs.py index e452daf77..40b191d5a 100644 --- a/comfy_api_nodes/nodes_elevenlabs.py +++ b/comfy_api_nodes/nodes_elevenlabs.py @@ -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, diff --git a/comfy_api_nodes/nodes_kling.py b/comfy_api_nodes/nodes_kling.py index efd58fac3..55605d7bc 100644 --- a/comfy_api_nodes/nodes_kling.py +++ b/comfy_api_nodes/nodes_kling.py @@ -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, ]