From b633244635e577e199944cd4f027df79afa16dbf Mon Sep 17 00:00:00 2001 From: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Date: Thu, 30 Apr 2026 21:49:08 +0300 Subject: [PATCH] [Partner Nodes] ByteDance: virtual portrait library for regular images (#13638) * feat(api-nodes-bytedance): use the virtual portrait library for regular images Signed-off-by: bigcat88 * fix: include shape in image dedup hash Signed-off-by: bigcat88 --------- Signed-off-by: bigcat88 --- comfy_api_nodes/apis/bytedance.py | 5 ++++ comfy_api_nodes/nodes_bytedance.py | 38 ++++++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/comfy_api_nodes/apis/bytedance.py b/comfy_api_nodes/apis/bytedance.py index eafabbefe..c05bd6893 100644 --- a/comfy_api_nodes/apis/bytedance.py +++ b/comfy_api_nodes/apis/bytedance.py @@ -157,6 +157,11 @@ class SeedanceCreateAssetResponse(BaseModel): asset_id: str = Field(...) +class SeedanceVirtualLibraryCreateAssetRequest(BaseModel): + url: str = Field(..., description="Publicly accessible URL of the image asset to upload.") + hash: str = Field(..., description="Dedup key. Re-submitting the same hash returns the existing asset id.") + + # Dollars per 1K tokens, keyed by (model_id, has_video_input). SEEDANCE2_PRICE_PER_1K_TOKENS = { ("dreamina-seedance-2-0-260128", False): 0.007, diff --git a/comfy_api_nodes/nodes_bytedance.py b/comfy_api_nodes/nodes_bytedance.py index de192c5ac..fee0ab888 100644 --- a/comfy_api_nodes/nodes_bytedance.py +++ b/comfy_api_nodes/nodes_bytedance.py @@ -1,3 +1,4 @@ +import hashlib import logging import math import re @@ -20,6 +21,7 @@ from comfy_api_nodes.apis.bytedance import ( SeedanceCreateAssetResponse, SeedanceCreateVisualValidateSessionResponse, SeedanceGetVisualValidateSessionResponse, + SeedanceVirtualLibraryCreateAssetRequest, Seedream4Options, Seedream4TaskCreationRequest, TaskAudioContent, @@ -271,6 +273,30 @@ async def _wait_for_asset_active(cls: type[IO.ComfyNode], asset_id: str, group_i ) +async def _seedance_virtual_library_upload_image_asset( + cls: type[IO.ComfyNode], + image: torch.Tensor, + *, + wait_label: str = "Uploading image", +) -> str: + """Upload an image into the caller's per-customer Seedance virtual library.""" + public_url = await upload_image_to_comfyapi(cls, image, wait_label=wait_label) + normalized = image.detach().cpu().contiguous().to(torch.float32) + digest = hashlib.sha256() + digest.update(str(tuple(normalized.shape)).encode("utf-8")) + digest.update(b"\0") + digest.update(normalized.numpy().tobytes()) + image_hash = digest.hexdigest() + create_resp = await sync_op( + cls, + ApiEndpoint(path="/proxy/seedance/virtual-library/assets", method="POST"), + response_model=SeedanceCreateAssetResponse, + data=SeedanceVirtualLibraryCreateAssetRequest(url=public_url, hash=image_hash), + ) + await _wait_for_asset_active(cls, create_resp.asset_id, group_id="virtual-library") + return f"asset://{create_resp.asset_id}" + + def _seedance2_price_extractor(model_id: str, has_video_input: bool): """Returns a price_extractor closure for Seedance 2.0 poll_op.""" rate = SEEDANCE2_PRICE_PER_1K_TOKENS.get((model_id, has_video_input)) @@ -1507,7 +1533,9 @@ class ByteDance2FirstLastFrameNode(IO.ComfyNode): if first_frame_asset_id: first_frame_url = image_assets[first_frame_asset_id] else: - first_frame_url = await upload_image_to_comfyapi(cls, first_frame, wait_label="Uploading first frame.") + first_frame_url = await _seedance_virtual_library_upload_image_asset( + cls, first_frame, wait_label="Uploading first frame." + ) content: list[TaskTextContent | TaskImageContent] = [ TaskTextContent(text=model["prompt"]), @@ -1527,7 +1555,9 @@ class ByteDance2FirstLastFrameNode(IO.ComfyNode): content.append( TaskImageContent( image_url=TaskImageContentUrl( - url=await upload_image_to_comfyapi(cls, last_frame, wait_label="Uploading last frame.") + url=await _seedance_virtual_library_upload_image_asset( + cls, last_frame, wait_label="Uploading last frame." + ) ), role="last_frame", ), @@ -1805,9 +1835,9 @@ class ByteDance2ReferenceNode(IO.ComfyNode): content.append( TaskImageContent( image_url=TaskImageContentUrl( - url=await upload_image_to_comfyapi( + url=await _seedance_virtual_library_upload_image_asset( cls, - image=reference_images[key], + reference_images[key], wait_label=f"Uploading image {i}", ), ),