Skip to content

Commit 7288264

Browse files
committed
dev: RemoteComboOptions example
Signed-off-by: bigcat88 <bigcat88@icloud.com>
1 parent defb663 commit 7288264

4 files changed

Lines changed: 245 additions & 37 deletions

File tree

comfy_api_nodes/apis/bytedance.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ class GetAssetResponse(BaseModel):
132132
error: TaskStatusError | None = Field(None)
133133

134134

135+
class SeedanceCreateVisualValidateSessionRequest(BaseModel):
136+
name: str | None = Field(None, max_length=64)
137+
138+
135139
class SeedanceCreateVisualValidateSessionResponse(BaseModel):
136140
session_id: str = Field(...)
137141
h5_link: str = Field(...)
@@ -141,6 +145,7 @@ class SeedanceGetVisualValidateSessionResponse(BaseModel):
141145
session_id: str = Field(...)
142146
status: str = Field(...)
143147
group_id: str | None = Field(None)
148+
name: str | None = Field(None)
144149
error_code: str | None = Field(None)
145150
error_message: str | None = Field(None)
146151

comfy_api_nodes/nodes_bytedance.py

Lines changed: 153 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
Seedance2TaskCreationRequest,
2020
SeedanceCreateAssetRequest,
2121
SeedanceCreateAssetResponse,
22+
SeedanceCreateVisualValidateSessionRequest,
2223
SeedanceCreateVisualValidateSessionResponse,
2324
SeedanceGetVisualValidateSessionResponse,
2425
SeedanceVirtualLibraryCreateAssetRequest,
@@ -196,11 +197,16 @@ def _sub(m: "re.Match[str]") -> str:
196197
return _ASSET_REF_RE.sub(_sub, prompt)
197198

198199

199-
async def _obtain_group_id_via_h5_auth(cls: type[IO.ComfyNode]) -> str:
200+
async def _obtain_group_id_via_h5_auth(
201+
cls: type[IO.ComfyNode],
202+
group_name: str | None = None,
203+
) -> str:
204+
payload = SeedanceCreateVisualValidateSessionRequest(name=group_name)
200205
session = await sync_op(
201206
cls,
202207
ApiEndpoint(path="/proxy/seedance/visual-validate/sessions", method="POST"),
203208
response_model=SeedanceCreateVisualValidateSessionResponse,
209+
data=payload,
204210
)
205211
logger.warning("Seedance authentication required. Open link: %s", session.h5_link)
206212

@@ -229,10 +235,15 @@ async def _obtain_group_id_via_h5_auth(cls: type[IO.ComfyNode]) -> str:
229235
return result.group_id
230236

231237

232-
async def _resolve_group_id(cls: type[IO.ComfyNode], group_id: str) -> str:
238+
async def _resolve_group_id(
239+
cls: type[IO.ComfyNode],
240+
group_id: str,
241+
group_name: str | None = None,
242+
) -> str:
233243
if group_id and group_id.strip():
234244
return group_id.strip()
235-
return await _obtain_group_id_via_h5_auth(cls)
245+
label = (group_name or "").strip() or None
246+
return await _obtain_group_id_via_h5_auth(cls, group_name=label)
236247

237248

238249
async def _create_seedance_asset(
@@ -1936,6 +1947,43 @@ async def process_video_task(
19361947
return IO.NodeOutput(await download_url_to_video_output(response.content.video_url))
19371948

19381949

1950+
def _seedance_group_picker_input() -> IO.Combo.Input:
1951+
"""Combo populated from /proxy/seedance/groups. Empty selection triggers H5 enrollment."""
1952+
return IO.Combo.Input(
1953+
"group_id",
1954+
default="",
1955+
tooltip=(
1956+
"Pick an existing verified group, or leave empty to run real-person H5 "
1957+
"authentication and create a new group."
1958+
),
1959+
remote_combo=IO.RemoteComboOptions(
1960+
route="/proxy/seedance/groups",
1961+
response_key="groups",
1962+
item_schema=IO.RemoteItemSchema(
1963+
value_field="group_id",
1964+
label_field="name",
1965+
description_field="created_at",
1966+
search_fields=["name", "group_id"],
1967+
),
1968+
refresh=60_000,
1969+
),
1970+
optional=True,
1971+
)
1972+
1973+
1974+
def _seedance_group_name_input() -> IO.String.Input:
1975+
return IO.String.Input(
1976+
"group_name",
1977+
default="",
1978+
tooltip=(
1979+
"Optional label for a new group. Used only when group_id is empty; the label is "
1980+
"shown later in the group picker so you can identify this group at a glance. "
1981+
"Up to 64 characters."
1982+
),
1983+
optional=True,
1984+
)
1985+
1986+
19391987
class ByteDanceCreateImageAsset(IO.ComfyNode):
19401988

19411989
@classmethod
@@ -1946,22 +1994,14 @@ def define_schema(cls) -> IO.Schema:
19461994
category="api node/image/ByteDance",
19471995
description=(
19481996
"Create a Seedance 2.0 personal image asset. Uploads the input image and "
1949-
"registers it in the given asset group. If group_id is empty, runs a real-person "
1950-
"H5 authentication flow to create a new group before adding the asset."
1997+
"registers it in the selected asset group. Leave group_id empty to run a "
1998+
"real-person H5 authentication flow and create a new group; provide group_name "
1999+
"to label the new group."
19512000
),
19522001
inputs=[
19532002
IO.Image.Input("image", tooltip="Image to register as a personal asset."),
1954-
IO.String.Input(
1955-
"group_id",
1956-
default="",
1957-
tooltip="Reuse an existing Seedance asset group ID to skip repeated human verification for the "
1958-
"same person. Leave empty to run real-person authentication in the browser and create a new group.",
1959-
),
1960-
# IO.String.Input(
1961-
# "name",
1962-
# default="",
1963-
# tooltip="Asset name (up to 64 characters).",
1964-
# ),
2003+
_seedance_group_picker_input(),
2004+
_seedance_group_name_input(),
19652005
],
19662006
outputs=[
19672007
IO.String.Output(display_name="asset_id"),
@@ -1980,13 +2020,11 @@ async def execute(
19802020
cls,
19812021
image: Input.Image,
19822022
group_id: str = "",
1983-
# name: str = "",
2023+
group_name: str = "",
19842024
) -> IO.NodeOutput:
1985-
# if len(name) > 64:
1986-
# raise ValueError("Name of asset can not be greater then 64 symbols")
19872025
validate_image_dimensions(image, min_width=300, max_width=6000, min_height=300, max_height=6000)
19882026
validate_image_aspect_ratio(image, min_ratio=(0.4, 1), max_ratio=(2.5, 1))
1989-
resolved_group = await _resolve_group_id(cls, group_id)
2027+
resolved_group = await _resolve_group_id(cls, group_id, group_name=group_name)
19902028
asset_id = await _create_seedance_asset(
19912029
cls,
19922030
group_id=resolved_group,
@@ -2013,22 +2051,14 @@ def define_schema(cls) -> IO.Schema:
20132051
category="api node/video/ByteDance",
20142052
description=(
20152053
"Create a Seedance 2.0 personal video asset. Uploads the input video and "
2016-
"registers it in the given asset group. If group_id is empty, runs a real-person "
2017-
"H5 authentication flow to create a new group before adding the asset."
2054+
"registers it in the selected asset group. Leave group_id empty to run a "
2055+
"real-person H5 authentication flow and create a new group; provide group_name "
2056+
"to label the new group."
20182057
),
20192058
inputs=[
20202059
IO.Video.Input("video", tooltip="Video to register as a personal asset."),
2021-
IO.String.Input(
2022-
"group_id",
2023-
default="",
2024-
tooltip="Reuse an existing Seedance asset group ID to skip repeated human verification for the "
2025-
"same person. Leave empty to run real-person authentication in the browser and create a new group.",
2026-
),
2027-
# IO.String.Input(
2028-
# "name",
2029-
# default="",
2030-
# tooltip="Asset name (up to 64 characters).",
2031-
# ),
2060+
_seedance_group_picker_input(),
2061+
_seedance_group_name_input(),
20322062
],
20332063
outputs=[
20342064
IO.String.Output(display_name="asset_id"),
@@ -2047,10 +2077,8 @@ async def execute(
20472077
cls,
20482078
video: Input.Video,
20492079
group_id: str = "",
2050-
# name: str = "",
2080+
group_name: str = "",
20512081
) -> IO.NodeOutput:
2052-
# if len(name) > 64:
2053-
# raise ValueError("Name of asset can not be greater then 64 symbols")
20542082
validate_video_duration(video, min_duration=2, max_duration=15)
20552083
validate_video_dimensions(video, min_width=300, max_width=6000, min_height=300, max_height=6000)
20562084

@@ -2069,7 +2097,7 @@ async def execute(
20692097
if not (24 <= fps <= 60):
20702098
raise ValueError(f"Asset video FPS must be in [24, 60], got {fps:.2f}.")
20712099

2072-
resolved_group = await _resolve_group_id(cls, group_id)
2100+
resolved_group = await _resolve_group_id(cls, group_id, group_name=group_name)
20732101
asset_id = await _create_seedance_asset(
20742102
cls,
20752103
group_id=resolved_group,
@@ -2086,6 +2114,92 @@ async def execute(
20862114
return IO.NodeOutput(asset_id, resolved_group)
20872115

20882116

2117+
def _seedance_asset_picker_input(asset_type: str, preview_type: str) -> IO.Combo.Input:
2118+
"""Combo populated from /proxy/seedance/assets, scoped to one asset_type."""
2119+
return IO.Combo.Input(
2120+
"asset_id",
2121+
tooltip=(
2122+
f"Pick a previously-created Seedance {asset_type.lower()} asset. The dropdown shows "
2123+
"your assets across all your verified groups; type a group name to filter."
2124+
),
2125+
remote_combo=IO.RemoteComboOptions(
2126+
route=f"/proxy/seedance/assets?asset_type={asset_type}",
2127+
response_key="assets",
2128+
item_schema=IO.RemoteItemSchema(
2129+
value_field="asset_id",
2130+
label_field="name",
2131+
description_field="group_name",
2132+
preview_url_field="url",
2133+
preview_type=preview_type,
2134+
search_fields=["name", "asset_id", "group_name", "group_id"],
2135+
),
2136+
refresh=60_000,
2137+
),
2138+
)
2139+
2140+
2141+
class ByteDanceSelectImageAsset(IO.ComfyNode):
2142+
2143+
@classmethod
2144+
def define_schema(cls) -> IO.Schema:
2145+
return IO.Schema(
2146+
node_id="ByteDanceSelectImageAsset",
2147+
display_name="ByteDance Select Image Asset",
2148+
category="api node/image/ByteDance",
2149+
description=(
2150+
"Pick a previously-created Seedance image asset. Outputs the selected asset_id "
2151+
"for use with downstream Seedance 2.0 reference/first-last-frame nodes."
2152+
),
2153+
inputs=[
2154+
_seedance_asset_picker_input("Image", "image"),
2155+
],
2156+
outputs=[IO.String.Output(display_name="asset_id")],
2157+
hidden=[
2158+
IO.Hidden.auth_token_comfy_org,
2159+
IO.Hidden.api_key_comfy_org,
2160+
IO.Hidden.unique_id,
2161+
],
2162+
# is_api_node=True,
2163+
)
2164+
2165+
@classmethod
2166+
async def execute(cls, asset_id: str) -> IO.NodeOutput:
2167+
if not asset_id or not asset_id.strip():
2168+
raise ValueError("asset_id is required. Pick an asset from the dropdown.")
2169+
return IO.NodeOutput(asset_id.strip())
2170+
2171+
2172+
class ByteDanceSelectVideoAsset(IO.ComfyNode):
2173+
2174+
@classmethod
2175+
def define_schema(cls) -> IO.Schema:
2176+
return IO.Schema(
2177+
node_id="ByteDanceSelectVideoAsset",
2178+
display_name="ByteDance Select Video Asset",
2179+
category="api node/video/ByteDance",
2180+
description=(
2181+
"Pick a previously-created Seedance video asset. Outputs the selected asset_id "
2182+
"for use with downstream Seedance 2.0 reference/first-last-frame nodes."
2183+
),
2184+
inputs=[
2185+
_seedance_asset_picker_input("Video", "video"),
2186+
],
2187+
outputs=[IO.String.Output(display_name="asset_id")],
2188+
hidden=[
2189+
IO.Hidden.auth_token_comfy_org,
2190+
IO.Hidden.api_key_comfy_org,
2191+
IO.Hidden.unique_id,
2192+
],
2193+
# is_api_node=True,
2194+
)
2195+
2196+
@classmethod
2197+
async def execute(cls, asset_id: str) -> IO.NodeOutput:
2198+
if not asset_id or not asset_id.strip():
2199+
raise ValueError("asset_id is required. Pick an asset from the dropdown.")
2200+
return IO.NodeOutput(asset_id.strip())
2201+
2202+
20892203
class ByteDanceExtension(ComfyExtension):
20902204
@override
20912205
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
@@ -2101,6 +2215,8 @@ async def get_node_list(self) -> list[type[IO.ComfyNode]]:
21012215
ByteDance2ReferenceNode,
21022216
ByteDanceCreateImageAsset,
21032217
ByteDanceCreateVideoAsset,
2218+
ByteDanceSelectImageAsset,
2219+
ByteDanceSelectVideoAsset,
21042220
]
21052221

21062222

comfy_api_nodes/nodes_elevenlabs.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,44 @@ def execute(cls, voice: str) -> IO.NodeOutput:
233233
return IO.NodeOutput(voice_id)
234234

235235

236+
class ElevenLabsRichVoiceSelector(IO.ComfyNode):
237+
@classmethod
238+
def define_schema(cls) -> IO.Schema:
239+
return IO.Schema(
240+
node_id="ElevenLabsRichVoiceSelector",
241+
display_name="ElevenLabs Voice Selector (Rich)",
242+
category="api node/audio/ElevenLabs",
243+
description="Select an ElevenLabs voice with audio preview and rich metadata.",
244+
inputs=[
245+
IO.Combo.Input(
246+
"voice",
247+
remote_combo=IO.RemoteComboOptions(
248+
route="/proxy/elevenlabs/v2/voices?page_size=100",
249+
response_key="items",
250+
refresh_button=True,
251+
refresh=43200000,
252+
item_schema=IO.RemoteItemSchema(
253+
value_field="voice_id",
254+
label_field="name",
255+
preview_url_field="preview_url",
256+
preview_type="audio",
257+
search_fields=["name", "labels.gender", "labels.accent", "labels.use_case"],
258+
),
259+
),
260+
tooltip="Choose a voice with audio preview.",
261+
),
262+
],
263+
outputs=[
264+
IO.Custom(ELEVENLABS_VOICE).Output(display_name="voice"),
265+
],
266+
is_api_node=False,
267+
)
268+
269+
@classmethod
270+
def execute(cls, voice: str) -> IO.NodeOutput:
271+
return IO.NodeOutput(voice) # voice is already the voice_id from item_schema.value_field
272+
273+
236274
class ElevenLabsTextToSpeech(IO.ComfyNode):
237275
@classmethod
238276
def define_schema(cls) -> IO.Schema:
@@ -911,6 +949,7 @@ async def get_node_list(self) -> list[type[IO.ComfyNode]]:
911949
return [
912950
ElevenLabsSpeechToText,
913951
ElevenLabsVoiceSelector,
952+
ElevenLabsRichVoiceSelector,
914953
ElevenLabsTextToSpeech,
915954
ElevenLabsAudioIsolation,
916955
ElevenLabsTextToSoundEffects,

0 commit comments

Comments
 (0)