Skip to content

Commit 3b93d5d

Browse files
authored
feat(api-nodes): add TencentSmartTopology node (Comfy-Org#12741)
* feat(api-nodes): add TencentSmartTopology node * feat(api-nodes): enable TencentModelTo3DUV node * chore(Tencent endpoints): add "wait" to queued statuses
1 parent e544c65 commit 3b93d5d

File tree

3 files changed

+114
-13
lines changed

3 files changed

+114
-13
lines changed

comfy_api_nodes/apis/hunyuan3d.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,21 +66,31 @@ class To3DProTaskQueryRequest(BaseModel):
6666
JobId: str = Field(...)
6767

6868

69-
class To3DUVFileInput(BaseModel):
69+
class TaskFile3DInput(BaseModel):
7070
Type: str = Field(..., description="File type: GLB, OBJ, or FBX")
7171
Url: str = Field(...)
7272

7373

7474
class To3DUVTaskRequest(BaseModel):
75-
File: To3DUVFileInput = Field(...)
75+
File: TaskFile3DInput = Field(...)
76+
77+
78+
class To3DPartTaskRequest(BaseModel):
79+
File: TaskFile3DInput = Field(...)
7680

7781

7882
class TextureEditImageInfo(BaseModel):
7983
Url: str = Field(...)
8084

8185

8286
class TextureEditTaskRequest(BaseModel):
83-
File3D: To3DUVFileInput = Field(...)
87+
File3D: TaskFile3DInput = Field(...)
8488
Image: TextureEditImageInfo | None = Field(None)
8589
Prompt: str | None = Field(None)
8690
EnablePBR: bool | None = Field(None)
91+
92+
93+
class SmartTopologyRequest(BaseModel):
94+
File3D: TaskFile3DInput = Field(...)
95+
PolygonType: str | None = Field(...)
96+
FaceLevel: str | None = Field(...)

comfy_api_nodes/nodes_hunyuan3d.py

Lines changed: 100 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,19 @@
55
Hunyuan3DViewImage,
66
InputGenerateType,
77
ResultFile3D,
8+
SmartTopologyRequest,
9+
TaskFile3DInput,
810
TextureEditTaskRequest,
11+
To3DPartTaskRequest,
912
To3DProTaskCreateResponse,
1013
To3DProTaskQueryRequest,
1114
To3DProTaskRequest,
1215
To3DProTaskResultResponse,
13-
To3DUVFileInput,
1416
To3DUVTaskRequest,
1517
)
1618
from comfy_api_nodes.util import (
1719
ApiEndpoint,
1820
download_url_to_file_3d,
19-
download_url_to_image_tensor,
2021
downscale_image_tensor_by_max_side,
2122
poll_op,
2223
sync_op,
@@ -344,7 +345,6 @@ def define_schema(cls):
344345
outputs=[
345346
IO.File3DOBJ.Output(display_name="OBJ"),
346347
IO.File3DFBX.Output(display_name="FBX"),
347-
IO.Image.Output(),
348348
],
349349
hidden=[
350350
IO.Hidden.auth_token_comfy_org,
@@ -375,7 +375,7 @@ async def execute(
375375
ApiEndpoint(path="/proxy/tencent/hunyuan/3d-uv", method="POST"),
376376
response_model=To3DProTaskCreateResponse,
377377
data=To3DUVTaskRequest(
378-
File=To3DUVFileInput(
378+
File=TaskFile3DInput(
379379
Type=file_format.upper(),
380380
Url=await upload_3d_model_to_comfyapi(cls, model_3d, file_format),
381381
)
@@ -394,7 +394,6 @@ async def execute(
394394
return IO.NodeOutput(
395395
await download_url_to_file_3d(get_file_from_response(result.ResultFile3Ds, "obj").Url, "obj"),
396396
await download_url_to_file_3d(get_file_from_response(result.ResultFile3Ds, "fbx").Url, "fbx"),
397-
await download_url_to_image_tensor(get_file_from_response(result.ResultFile3Ds, "image").Url),
398397
)
399398

400399

@@ -463,7 +462,7 @@ async def execute(
463462
ApiEndpoint(path="/proxy/tencent/hunyuan/3d-texture-edit", method="POST"),
464463
response_model=To3DProTaskCreateResponse,
465464
data=TextureEditTaskRequest(
466-
File3D=To3DUVFileInput(Type=file_format.upper(), Url=model_url),
465+
File3D=TaskFile3DInput(Type=file_format.upper(), Url=model_url),
467466
Prompt=prompt,
468467
EnablePBR=True,
469468
),
@@ -538,8 +537,8 @@ async def execute(
538537
cls,
539538
ApiEndpoint(path="/proxy/tencent/hunyuan/3d-part", method="POST"),
540539
response_model=To3DProTaskCreateResponse,
541-
data=To3DUVTaskRequest(
542-
File=To3DUVFileInput(Type=file_format.upper(), Url=model_url),
540+
data=To3DPartTaskRequest(
541+
File=TaskFile3DInput(Type=file_format.upper(), Url=model_url),
543542
),
544543
is_rate_limited=_is_tencent_rate_limited,
545544
)
@@ -557,15 +556,107 @@ async def execute(
557556
)
558557

559558

559+
class TencentSmartTopologyNode(IO.ComfyNode):
560+
561+
@classmethod
562+
def define_schema(cls):
563+
return IO.Schema(
564+
node_id="TencentSmartTopologyNode",
565+
display_name="Hunyuan3D: Smart Topology",
566+
category="api node/3d/Tencent",
567+
description="Perform smart retopology on a 3D model. "
568+
"Supports GLB/OBJ formats; max 200MB; recommended for high-poly models.",
569+
inputs=[
570+
IO.MultiType.Input(
571+
"model_3d",
572+
types=[IO.File3DGLB, IO.File3DOBJ, IO.File3DAny],
573+
tooltip="Input 3D model (GLB or OBJ)",
574+
),
575+
IO.Combo.Input(
576+
"polygon_type",
577+
options=["triangle", "quadrilateral"],
578+
tooltip="Surface composition type.",
579+
),
580+
IO.Combo.Input(
581+
"face_level",
582+
options=["medium", "high", "low"],
583+
tooltip="Polygon reduction level.",
584+
),
585+
IO.Int.Input(
586+
"seed",
587+
default=0,
588+
min=0,
589+
max=2147483647,
590+
display_mode=IO.NumberDisplay.number,
591+
control_after_generate=True,
592+
tooltip="Seed controls whether the node should re-run; "
593+
"results are non-deterministic regardless of seed.",
594+
),
595+
],
596+
outputs=[
597+
IO.File3DOBJ.Output(display_name="OBJ"),
598+
],
599+
hidden=[
600+
IO.Hidden.auth_token_comfy_org,
601+
IO.Hidden.api_key_comfy_org,
602+
IO.Hidden.unique_id,
603+
],
604+
is_api_node=True,
605+
price_badge=IO.PriceBadge(expr='{"type":"usd","usd":1.0}'),
606+
)
607+
608+
SUPPORTED_FORMATS = {"glb", "obj"}
609+
610+
@classmethod
611+
async def execute(
612+
cls,
613+
model_3d: Types.File3D,
614+
polygon_type: str,
615+
face_level: str,
616+
seed: int,
617+
) -> IO.NodeOutput:
618+
_ = seed
619+
file_format = model_3d.format.lower()
620+
if file_format not in cls.SUPPORTED_FORMATS:
621+
raise ValueError(
622+
f"Unsupported file format: '{file_format}'. " f"Supported: {', '.join(sorted(cls.SUPPORTED_FORMATS))}."
623+
)
624+
model_url = await upload_3d_model_to_comfyapi(cls, model_3d, file_format)
625+
response = await sync_op(
626+
cls,
627+
ApiEndpoint(path="/proxy/tencent/hunyuan/3d-smart-topology", method="POST"),
628+
response_model=To3DProTaskCreateResponse,
629+
data=SmartTopologyRequest(
630+
File3D=TaskFile3DInput(Type=file_format.upper(), Url=model_url),
631+
PolygonType=polygon_type,
632+
FaceLevel=face_level,
633+
),
634+
is_rate_limited=_is_tencent_rate_limited,
635+
)
636+
if response.Error:
637+
raise ValueError(f"Task creation failed: [{response.Error.Code}] {response.Error.Message}")
638+
result = await poll_op(
639+
cls,
640+
ApiEndpoint(path="/proxy/tencent/hunyuan/3d-smart-topology/query", method="POST"),
641+
data=To3DProTaskQueryRequest(JobId=response.JobId),
642+
response_model=To3DProTaskResultResponse,
643+
status_extractor=lambda r: r.Status,
644+
)
645+
return IO.NodeOutput(
646+
await download_url_to_file_3d(get_file_from_response(result.ResultFile3Ds, "obj").Url, "obj"),
647+
)
648+
649+
560650
class TencentHunyuan3DExtension(ComfyExtension):
561651
@override
562652
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
563653
return [
564654
TencentTextToModelNode,
565655
TencentImageToModelNode,
566-
# TencentModelTo3DUVNode,
656+
TencentModelTo3DUVNode,
567657
# Tencent3DTextureEditNode,
568658
Tencent3DPartNode,
659+
TencentSmartTopologyNode,
569660
]
570661

571662

comfy_api_nodes/util/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ class _PollUIState:
8383
_RETRY_STATUS = {408, 500, 502, 503, 504} # status 429 is handled separately
8484
COMPLETED_STATUSES = ["succeeded", "succeed", "success", "completed", "finished", "done", "complete"]
8585
FAILED_STATUSES = ["cancelled", "canceled", "canceling", "fail", "failed", "error"]
86-
QUEUED_STATUSES = ["created", "queued", "queueing", "submitted", "initializing"]
86+
QUEUED_STATUSES = ["created", "queued", "queueing", "submitted", "initializing", "wait"]
8787

8888

8989
async def sync_op(

0 commit comments

Comments
 (0)