11from typing_extensions import override
22
3- from comfy_api .latest import IO , ComfyExtension , Input
3+ from comfy_api .latest import IO , ComfyExtension , Input , Types
44from comfy_api_nodes .apis .hunyuan3d import (
55 Hunyuan3DViewImage ,
66 InputGenerateType ,
77 ResultFile3D ,
8+ TextureEditTaskRequest ,
89 To3DProTaskCreateResponse ,
910 To3DProTaskQueryRequest ,
1011 To3DProTaskRequest ,
1112 To3DProTaskResultResponse ,
13+ To3DUVFileInput ,
14+ To3DUVTaskRequest ,
1215)
1316from comfy_api_nodes .util import (
1417 ApiEndpoint ,
1518 download_url_to_file_3d ,
19+ download_url_to_image_tensor ,
1620 downscale_image_tensor_by_max_side ,
1721 poll_op ,
1822 sync_op ,
23+ upload_3d_model_to_comfyapi ,
1924 upload_image_to_comfyapi ,
2025 validate_image_dimensions ,
2126 validate_string ,
2227)
2328
2429
25- def get_file_from_response (response_objs : list [ResultFile3D ], file_type : str ) -> ResultFile3D | None :
30+ def _is_tencent_rate_limited (status : int , body : object ) -> bool :
31+ return (
32+ status == 400
33+ and isinstance (body , dict )
34+ and "RequestLimitExceeded" in str (body .get ("Response" , {}).get ("Error" , {}).get ("Code" , "" ))
35+ )
36+
37+
38+ def get_file_from_response (
39+ response_objs : list [ResultFile3D ], file_type : str , raise_if_not_found : bool = True
40+ ) -> ResultFile3D | None :
2641 for i in response_objs :
2742 if i .Type .lower () == file_type .lower ():
2843 return i
44+ if raise_if_not_found :
45+ raise ValueError (f"'{ file_type } ' file type is not found in the response." )
2946 return None
3047
3148
@@ -35,7 +52,7 @@ class TencentTextToModelNode(IO.ComfyNode):
3552 def define_schema (cls ):
3653 return IO .Schema (
3754 node_id = "TencentTextToModelNode" ,
38- display_name = "Hunyuan3D: Text to Model (Pro) " ,
55+ display_name = "Hunyuan3D: Text to Model" ,
3956 category = "api node/3d/Tencent" ,
4057 inputs = [
4158 IO .Combo .Input (
@@ -120,6 +137,7 @@ async def execute(
120137 EnablePBR = generate_type .get ("pbr" , None ),
121138 PolygonType = generate_type .get ("polygon_type" , None ),
122139 ),
140+ is_rate_limited = _is_tencent_rate_limited ,
123141 )
124142 if response .Error :
125143 raise ValueError (f"Task creation failed with code { response .Error .Code } : { response .Error .Message } " )
@@ -131,11 +149,14 @@ async def execute(
131149 response_model = To3DProTaskResultResponse ,
132150 status_extractor = lambda r : r .Status ,
133151 )
134- glb_result = get_file_from_response (result .ResultFile3Ds , "glb" )
135- obj_result = get_file_from_response (result .ResultFile3Ds , "obj" )
136- file_glb = await download_url_to_file_3d (glb_result .Url , "glb" , task_id = task_id ) if glb_result else None
137152 return IO .NodeOutput (
138- file_glb , file_glb , await download_url_to_file_3d (obj_result .Url , "obj" , task_id = task_id ) if obj_result else None
153+ f"{ task_id } .glb" ,
154+ await download_url_to_file_3d (
155+ get_file_from_response (result .ResultFile3Ds , "glb" ).Url , "glb" , task_id = task_id
156+ ),
157+ await download_url_to_file_3d (
158+ get_file_from_response (result .ResultFile3Ds , "obj" ).Url , "obj" , task_id = task_id
159+ ),
139160 )
140161
141162
@@ -145,7 +166,7 @@ class TencentImageToModelNode(IO.ComfyNode):
145166 def define_schema (cls ):
146167 return IO .Schema (
147168 node_id = "TencentImageToModelNode" ,
148- display_name = "Hunyuan3D: Image(s) to Model (Pro) " ,
169+ display_name = "Hunyuan3D: Image(s) to Model" ,
149170 category = "api node/3d/Tencent" ,
150171 inputs = [
151172 IO .Combo .Input (
@@ -268,6 +289,7 @@ async def execute(
268289 EnablePBR = generate_type .get ("pbr" , None ),
269290 PolygonType = generate_type .get ("polygon_type" , None ),
270291 ),
292+ is_rate_limited = _is_tencent_rate_limited ,
271293 )
272294 if response .Error :
273295 raise ValueError (f"Task creation failed with code { response .Error .Code } : { response .Error .Message } " )
@@ -279,11 +301,257 @@ async def execute(
279301 response_model = To3DProTaskResultResponse ,
280302 status_extractor = lambda r : r .Status ,
281303 )
282- glb_result = get_file_from_response (result .ResultFile3Ds , "glb" )
283- obj_result = get_file_from_response (result .ResultFile3Ds , "obj" )
284- file_glb = await download_url_to_file_3d (glb_result .Url , "glb" , task_id = task_id ) if glb_result else None
285304 return IO .NodeOutput (
286- file_glb , file_glb , await download_url_to_file_3d (obj_result .Url , "obj" , task_id = task_id ) if obj_result else None
305+ f"{ task_id } .glb" ,
306+ await download_url_to_file_3d (
307+ get_file_from_response (result .ResultFile3Ds , "glb" ).Url , "glb" , task_id = task_id
308+ ),
309+ await download_url_to_file_3d (
310+ get_file_from_response (result .ResultFile3Ds , "obj" ).Url , "obj" , task_id = task_id
311+ ),
312+ )
313+
314+
315+ class TencentModelTo3DUVNode (IO .ComfyNode ):
316+
317+ @classmethod
318+ def define_schema (cls ):
319+ return IO .Schema (
320+ node_id = "TencentModelTo3DUVNode" ,
321+ display_name = "Hunyuan3D: Model to UV" ,
322+ category = "api node/3d/Tencent" ,
323+ description = "Perform UV unfolding on a 3D model to generate UV texture. "
324+ "Input model must have less than 30000 faces." ,
325+ inputs = [
326+ IO .MultiType .Input (
327+ "model_3d" ,
328+ types = [IO .File3DGLB , IO .File3DOBJ , IO .File3DFBX , IO .File3DAny ],
329+ tooltip = "Input 3D model (GLB, OBJ, or FBX)" ,
330+ ),
331+ IO .Int .Input (
332+ "seed" ,
333+ default = 1 ,
334+ min = 0 ,
335+ max = 2147483647 ,
336+ display_mode = IO .NumberDisplay .number ,
337+ control_after_generate = True ,
338+ tooltip = "Seed controls whether the node should re-run; "
339+ "results are non-deterministic regardless of seed." ,
340+ ),
341+ ],
342+ outputs = [
343+ IO .File3DOBJ .Output (display_name = "OBJ" ),
344+ IO .File3DFBX .Output (display_name = "FBX" ),
345+ IO .Image .Output (),
346+ ],
347+ hidden = [
348+ IO .Hidden .auth_token_comfy_org ,
349+ IO .Hidden .api_key_comfy_org ,
350+ IO .Hidden .unique_id ,
351+ ],
352+ is_api_node = True ,
353+ price_badge = IO .PriceBadge (expr = '{"type":"usd","usd":0.2}' ),
354+ )
355+
356+ SUPPORTED_FORMATS = {"glb" , "obj" , "fbx" }
357+
358+ @classmethod
359+ async def execute (
360+ cls ,
361+ model_3d : Types .File3D ,
362+ seed : int ,
363+ ) -> IO .NodeOutput :
364+ _ = seed
365+ file_format = model_3d .format .lower ()
366+ if file_format not in cls .SUPPORTED_FORMATS :
367+ raise ValueError (
368+ f"Unsupported file format: '{ file_format } '. "
369+ f"Supported formats: { ', ' .join (sorted (cls .SUPPORTED_FORMATS ))} ."
370+ )
371+ response = await sync_op (
372+ cls ,
373+ ApiEndpoint (path = "/proxy/tencent/hunyuan/3d-uv" , method = "POST" ),
374+ response_model = To3DProTaskCreateResponse ,
375+ data = To3DUVTaskRequest (
376+ File = To3DUVFileInput (
377+ Type = file_format .upper (),
378+ Url = await upload_3d_model_to_comfyapi (cls , model_3d , file_format ),
379+ )
380+ ),
381+ is_rate_limited = _is_tencent_rate_limited ,
382+ )
383+ if response .Error :
384+ raise ValueError (f"Task creation failed with code { response .Error .Code } : { response .Error .Message } " )
385+ result = await poll_op (
386+ cls ,
387+ ApiEndpoint (path = "/proxy/tencent/hunyuan/3d-uv/query" , method = "POST" ),
388+ data = To3DProTaskQueryRequest (JobId = response .JobId ),
389+ response_model = To3DProTaskResultResponse ,
390+ status_extractor = lambda r : r .Status ,
391+ )
392+ return IO .NodeOutput (
393+ await download_url_to_file_3d (get_file_from_response (result .ResultFile3Ds , "obj" ).Url , "obj" ),
394+ await download_url_to_file_3d (get_file_from_response (result .ResultFile3Ds , "fbx" ).Url , "fbx" ),
395+ await download_url_to_image_tensor (get_file_from_response (result .ResultFile3Ds , "image" ).Url ),
396+ )
397+
398+
399+ class Tencent3DTextureEditNode (IO .ComfyNode ):
400+
401+ @classmethod
402+ def define_schema (cls ):
403+ return IO .Schema (
404+ node_id = "Tencent3DTextureEditNode" ,
405+ display_name = "Hunyuan3D: 3D Texture Edit" ,
406+ category = "api node/3d/Tencent" ,
407+ description = "After inputting the 3D model, perform 3D model texture redrawing." ,
408+ inputs = [
409+ IO .MultiType .Input (
410+ "model_3d" ,
411+ types = [IO .File3DFBX , IO .File3DAny ],
412+ tooltip = "3D model in FBX format. Model should have less than 100000 faces." ,
413+ ),
414+ IO .String .Input (
415+ "prompt" ,
416+ multiline = True ,
417+ default = "" ,
418+ tooltip = "Describes texture editing. Supports up to 1024 UTF-8 characters." ,
419+ ),
420+ IO .Int .Input (
421+ "seed" ,
422+ default = 0 ,
423+ min = 0 ,
424+ max = 2147483647 ,
425+ display_mode = IO .NumberDisplay .number ,
426+ control_after_generate = True ,
427+ tooltip = "Seed controls whether the node should re-run; "
428+ "results are non-deterministic regardless of seed." ,
429+ ),
430+ ],
431+ outputs = [
432+ IO .File3DGLB .Output (display_name = "GLB" ),
433+ IO .File3DFBX .Output (display_name = "FBX" ),
434+ ],
435+ hidden = [
436+ IO .Hidden .auth_token_comfy_org ,
437+ IO .Hidden .api_key_comfy_org ,
438+ IO .Hidden .unique_id ,
439+ ],
440+ is_api_node = True ,
441+ price_badge = IO .PriceBadge (
442+ expr = """{"type":"usd","usd": 0.6}""" ,
443+ ),
444+ )
445+
446+ @classmethod
447+ async def execute (
448+ cls ,
449+ model_3d : Types .File3D ,
450+ prompt : str ,
451+ seed : int ,
452+ ) -> IO .NodeOutput :
453+ _ = seed
454+ file_format = model_3d .format .lower ()
455+ if file_format != "fbx" :
456+ raise ValueError (f"Unsupported file format: '{ file_format } '. Only FBX format is supported." )
457+ validate_string (prompt , field_name = "prompt" , min_length = 1 , max_length = 1024 )
458+ model_url = await upload_3d_model_to_comfyapi (cls , model_3d , file_format )
459+ response = await sync_op (
460+ cls ,
461+ ApiEndpoint (path = "/proxy/tencent/hunyuan/3d-texture-edit" , method = "POST" ),
462+ response_model = To3DProTaskCreateResponse ,
463+ data = TextureEditTaskRequest (
464+ File3D = To3DUVFileInput (Type = file_format .upper (), Url = model_url ),
465+ Prompt = prompt ,
466+ EnablePBR = True ,
467+ ),
468+ is_rate_limited = _is_tencent_rate_limited ,
469+ )
470+ if response .Error :
471+ raise ValueError (f"Task creation failed with code { response .Error .Code } : { response .Error .Message } " )
472+
473+ result = await poll_op (
474+ cls ,
475+ ApiEndpoint (path = "/proxy/tencent/hunyuan/3d-texture-edit/query" , method = "POST" ),
476+ data = To3DProTaskQueryRequest (JobId = response .JobId ),
477+ response_model = To3DProTaskResultResponse ,
478+ status_extractor = lambda r : r .Status ,
479+ )
480+ return IO .NodeOutput (
481+ await download_url_to_file_3d (get_file_from_response (result .ResultFile3Ds , "glb" ).Url , "glb" ),
482+ await download_url_to_file_3d (get_file_from_response (result .ResultFile3Ds , "fbx" ).Url , "fbx" ),
483+ )
484+
485+
486+ class Tencent3DPartNode (IO .ComfyNode ):
487+
488+ @classmethod
489+ def define_schema (cls ):
490+ return IO .Schema (
491+ node_id = "Tencent3DPartNode" ,
492+ display_name = "Hunyuan3D: 3D Part" ,
493+ category = "api node/3d/Tencent" ,
494+ description = "Automatically perform component identification and generation based on the model structure." ,
495+ inputs = [
496+ IO .MultiType .Input (
497+ "model_3d" ,
498+ types = [IO .File3DFBX , IO .File3DAny ],
499+ tooltip = "3D model in FBX format. Model should have less than 30000 faces." ,
500+ ),
501+ IO .Int .Input (
502+ "seed" ,
503+ default = 0 ,
504+ min = 0 ,
505+ max = 2147483647 ,
506+ display_mode = IO .NumberDisplay .number ,
507+ control_after_generate = True ,
508+ tooltip = "Seed controls whether the node should re-run; "
509+ "results are non-deterministic regardless of seed." ,
510+ ),
511+ ],
512+ outputs = [
513+ IO .File3DFBX .Output (display_name = "FBX" ),
514+ ],
515+ hidden = [
516+ IO .Hidden .auth_token_comfy_org ,
517+ IO .Hidden .api_key_comfy_org ,
518+ IO .Hidden .unique_id ,
519+ ],
520+ is_api_node = True ,
521+ price_badge = IO .PriceBadge (expr = '{"type":"usd","usd":0.6}' ),
522+ )
523+
524+ @classmethod
525+ async def execute (
526+ cls ,
527+ model_3d : Types .File3D ,
528+ seed : int ,
529+ ) -> IO .NodeOutput :
530+ _ = seed
531+ file_format = model_3d .format .lower ()
532+ if file_format != "fbx" :
533+ raise ValueError (f"Unsupported file format: '{ file_format } '. Only FBX format is supported." )
534+ model_url = await upload_3d_model_to_comfyapi (cls , model_3d , file_format )
535+ response = await sync_op (
536+ cls ,
537+ ApiEndpoint (path = "/proxy/tencent/hunyuan/3d-part" , method = "POST" ),
538+ response_model = To3DProTaskCreateResponse ,
539+ data = To3DUVTaskRequest (
540+ File = To3DUVFileInput (Type = file_format .upper (), Url = model_url ),
541+ ),
542+ is_rate_limited = _is_tencent_rate_limited ,
543+ )
544+ if response .Error :
545+ raise ValueError (f"Task creation failed with code { response .Error .Code } : { response .Error .Message } " )
546+ result = await poll_op (
547+ cls ,
548+ ApiEndpoint (path = "/proxy/tencent/hunyuan/3d-part/query" , method = "POST" ),
549+ data = To3DProTaskQueryRequest (JobId = response .JobId ),
550+ response_model = To3DProTaskResultResponse ,
551+ status_extractor = lambda r : r .Status ,
552+ )
553+ return IO .NodeOutput (
554+ await download_url_to_file_3d (get_file_from_response (result .ResultFile3Ds , "fbx" ).Url , "fbx" ),
287555 )
288556
289557
@@ -293,6 +561,9 @@ async def get_node_list(self) -> list[type[IO.ComfyNode]]:
293561 return [
294562 TencentTextToModelNode ,
295563 TencentImageToModelNode ,
564+ # TencentModelTo3DUVNode,
565+ # Tencent3DTextureEditNode,
566+ Tencent3DPartNode ,
296567 ]
297568
298569
0 commit comments