$ cat node-template.py
3D Model Creation Legacy
// Generates a 3D model (GLB) from a single image using Hunyuan3D 2.1. Creates mesh, applies textures via multi-view generation, and exports a ready-to-use GLB file.
Process
3D
template.py
1import os2import sys3import json4import time5import random6import traceback78try:9 import requests10except ImportError:11 import subprocess12 subprocess.check_call([sys.executable, "-m", "pip", "install", "requests"])13 import requests1415COMFYUI_API_URL = os.getenv("COMFYUI_API_URL", "http://192.168.1.39:8188")16INPUT_DIR = "/data/input"17OUTPUT_DIR = "/data/output"1819# ---------- Workflow ----------20WORKFLOW = {21 "4": {22 "inputs": {"model_name": "Hunyuan3D-vae-v2-1-fp16.ckpt"},23 "class_type": "Hy3D21VAELoader",24 "_meta": {"title": "Hunyuan 3D 2.1 VAE Loader"},25 },26 "9": {27 "inputs": {28 "box_v": 1.01,29 "octree_resolution": 256,30 "num_chunks": 64000,31 "mc_level": 0,32 "mc_algo": "mc",33 "enable_flash_vdm": True,34 "force_offload": False,35 "vae": ["4", 0],36 "latents": ["37", 0],37 },38 "class_type": "Hy3D21VAEDecode",39 "_meta": {"title": "Hunyuan 3D 2.1 VAE Decoder"},40 },41 "14": {42 "inputs": {"image": "Flux2-Klein_00273_-Photoroom.png"},43 "class_type": "Hy3D21LoadImageWithTransparency",44 "_meta": {"title": "Hunyuan 3D 2.1 Load Image with Transparency"},45 },46 "19": {47 "inputs": {48 "camera_azimuths": "0, 90, 180, 270, 0, 180",49 "camera_elevations": "0, 0, 0, 0, 90, -90",50 "view_weights": "1, 0.5, 1, 0.5, 1, 1",51 "ortho_scale": 1.1000000000000003,52 },53 "class_type": "Hy3D21CameraConfig",54 "_meta": {"title": "Hunyuan 3D 2.1 Camera Config"},55 },56 "20": {57 "inputs": {58 "view_size": 768,59 "steps": 10,60 "guidance_scale": 3,61 "texture_size": 1024,62 "unwrap_mesh": False,63 "seed": 172988371457815,64 "trimesh": ["45", 0],65 "camera_config": ["19", 0],66 "image": ["14", 2],67 },68 "class_type": "Hy3DMultiViewsGenerator",69 "_meta": {"title": "Hunyuan 3D 2.1 MultiViews Generator"},70 },71 "21": {72 "inputs": {73 "pipeline": ["20", 0],74 "camera_config": ["19", 0],75 "albedo": ["20", 1],76 "mr": ["20", 2],77 },78 "class_type": "Hy3DBakeMultiViews",79 "_meta": {"title": "Hunyuan 3D 2.1 Bake MultiViews"},80 },81 "30": {82 "inputs": {"value": 200000},83 "class_type": "INTConstant",84 "_meta": {"title": "INT Constant"},85 },86 "32": {87 "inputs": {"string": "Hy21_Mesh"},88 "class_type": "StringConstant",89 "_meta": {"title": "String Constant"},90 },91 "37": {92 "inputs": {93 "model": "hunyuan3d-dit-v2-1-fp16.ckpt",94 "steps": 25,95 "guidance_scale": 7.5,96 "seed": 739367420076026,97 "attention_mode": "sdpa",98 "image": ["14", 2],99 },100 "class_type": "Hy3DMeshGenerator",101 "_meta": {"title": "Hunyuan 3D 2.1 Mesh Generator"},102 },103 "43": {104 "inputs": {105 "remove_floaters": True,106 "remove_degenerate_faces": True,107 "reduce_faces": True,108 "max_facenum": ["30", 0],109 "smooth_normals": False,110 "trimesh": ["9", 0],111 },112 "class_type": "Hy3D21PostprocessMesh",113 "_meta": {"title": "Hunyuan 3D 2.1 Post Process Trimesh"},114 },115 "44": {116 "inputs": {117 "filename_prefix": ["32", 0],118 "file_format": "glb",119 "save_file": True,120 "trimesh": ["43", 0],121 },122 "class_type": "Hy3D21ExportMesh",123 "_meta": {"title": "Hunyuan 3D 2.1 Export Mesh"},124 },125 "45": {126 "inputs": {"trimesh": ["43", 0]},127 "class_type": "Hy3D21MeshUVWrap",128 "_meta": {"title": "Hunyuan 3D 2.1 Mesh UV Wrap"},129 },130 "49": {131 "inputs": {132 "output_mesh_name": ["32", 0],133 "pipeline": ["21", 0],134 "albedo": ["21", 1],135 "albedo_mask": ["21", 2],136 "mr": ["21", 3],137 "mr_mask": ["21", 4],138 },139 "class_type": "Hy3DInPaint",140 "_meta": {"title": "Hunyuan 3D 2.1 InPaint"},141 },142 "27": {143 "inputs": {"model_file": ["49", 3], "image": ""},144 "class_type": "Preview3D",145 "_meta": {"title": "Preview 3D & Animation"},146 },147 "46": {148 "inputs": {"filename_prefix": "MV", "images": ["20", 1]},149 "class_type": "SaveImage",150 "_meta": {"title": "Save Image"},151 },152 "50": {153 "inputs": {"images": ["49", 0]},154 "class_type": "PreviewImage",155 "_meta": {"title": "Preview Image"},156 },157 "51": {158 "inputs": {"images": ["49", 1]},159 "class_type": "PreviewImage",160 "_meta": {"title": "Preview Image"},161 },162 "57": {163 "inputs": {164 "filename_prefix": "3D/emblema-model",165 "file_format": "glb",166 "save_file": True,167 "trimesh": ["49", 2],168 },169 "class_type": "Hy3D21ExportMesh",170 "_meta": {"title": "Hunyuan 3D 2.1 Export Mesh"},171 },172}173174175def upload_file_to_comfyui(local_path: str, content_type: str) -> str:176 """Upload a local file to ComfyUI and return the uploaded filename."""177 with open(local_path, "rb") as f:178 resp = requests.post(179 f"{COMFYUI_API_URL}/upload/image",180 files={"image": (os.path.basename(local_path), f, content_type)},181 timeout=30,182 )183 resp.raise_for_status()184 data = resp.json()185 return data["name"]186187188def build_workflow(image_name: str, steps: int, guidance_scale: float, max_faces: int, texture_size: int) -> dict:189 """Build a 3D model generation workflow with the given parameters."""190 import copy191192 wf = copy.deepcopy(WORKFLOW)193194 # Image (node 14): set uploaded filename on the Hy3D21LoadImageWithTransparency node195 wf["14"]["inputs"]["image"] = image_name196197 # Mesh generation (node 37): steps, guidance_scale, random seed198 wf["37"]["inputs"]["steps"] = int(steps)199 wf["37"]["inputs"]["guidance_scale"] = float(guidance_scale)200 wf["37"]["inputs"]["seed"] = random.randint(0, 2**31 - 1)201202 # Max faces (node 30): polygon count203 wf["30"]["inputs"]["value"] = int(max_faces)204205 # Multi-view texture (node 20): texture_size, random seed206 wf["20"]["inputs"]["texture_size"] = int(texture_size)207 wf["20"]["inputs"]["seed"] = random.randint(0, 2**31 - 1)208209 # Output prefix (node 32 + node 57)210 output_prefix = "emblema-3d-model"211 wf["32"]["inputs"]["string"] = output_prefix212 wf["57"]["inputs"]["filename_prefix"] = f"3D/{output_prefix}"213214 return wf215216217def submit_prompt(workflow: dict) -> str:218 """Submit workflow to ComfyUI and return prompt_id."""219 resp = requests.post(220 f"{COMFYUI_API_URL}/prompt",221 json={"prompt": workflow},222 timeout=30,223 )224 if resp.status_code != 200:225 try:226 error_detail = resp.json()227 except Exception:228 error_detail = resp.text229 raise RuntimeError(230 f"ComfyUI /prompt returned {resp.status_code}: {json.dumps(error_detail, indent=2) if isinstance(error_detail, dict) else error_detail}"231 )232 data = resp.json()233234 # ComfyUI returns 200 even when nodes have validation errors235 node_errors = data.get("node_errors", {})236 if node_errors:237 raise RuntimeError(238 f"ComfyUI workflow has node errors: {json.dumps(node_errors, indent=2)}"239 )240241 return data["prompt_id"]242243244def wait_for_result(prompt_id: str, timeout: int = 900, poll_interval: int = 5) -> dict:245 """Poll ComfyUI history until the prompt completes with outputs."""246 deadline = time.time() + timeout247 empty_complete_retries = 0248 max_empty_retries = 3 # grace period for output serialization lag249250 while time.time() < deadline:251 try:252 resp = requests.get(253 f"{COMFYUI_API_URL}/history/{prompt_id}",254 timeout=(10, 30),255 )256 resp.raise_for_status()257 history = resp.json()258 except requests.exceptions.RequestException:259 time.sleep(poll_interval)260 continue261262 if prompt_id in history:263 prompt_data = history[prompt_id]264 status = prompt_data.get("status", {})265266 if status.get("status_str") == "error":267 messages = status.get("messages", [])268 raise RuntimeError(269 f"ComfyUI prompt failed: {json.dumps(messages, indent=2)}"270 )271272 if status.get("completed", False):273 if prompt_data.get("outputs"):274 return prompt_data275276 # Completed but no outputs -- retry briefly for race condition277 empty_complete_retries += 1278 if empty_complete_retries >= max_empty_retries:279 raise RuntimeError(280 f"ComfyUI prompt completed but produced no outputs. "281 f"This usually means a node failed silently (missing custom node or model). "282 f"Status: {json.dumps(status, indent=2)}"283 )284285 time.sleep(poll_interval)286287 raise TimeoutError(f"ComfyUI prompt {prompt_id} did not complete within {timeout}s")288289290def download_output_model(prompt_data: dict, output_dir: str) -> str:291 """Download the generated GLB model from ComfyUI."""292 outputs = prompt_data.get("outputs", {})293294 # First: check "result" arrays for plain-string GLB filenames (e.g. Preview3D node)295 for node_id, node_output in outputs.items():296 result_list = node_output.get("result")297 if not result_list or not isinstance(result_list, list):298 continue299 for item in result_list:300 if isinstance(item, str) and item.lower().endswith(".glb"):301 resp = requests.get(302 f"{COMFYUI_API_URL}/view",303 params={"filename": item, "subfolder": "", "type": "output"},304 timeout=120,305 )306 resp.raise_for_status()307308 out_filename = f"generated_{item}"309 out_path = os.path.join(output_dir, out_filename)310 with open(out_path, "wb") as f:311 f.write(resp.content)312 return out_filename313314 # Known output keys that may contain 3D model files315 known_keys = ["meshes", "gifs", "videos", "images", "files", "3d", "mesh"]316317 for node_id, node_output in outputs.items():318 for key in known_keys:319 file_list = node_output.get(key)320 if not file_list or not isinstance(file_list, list):321 continue322323 for item in file_list:324 if not isinstance(item, dict):325 continue326 filename = item.get("filename", "")327 if filename.lower().endswith(".glb"):328 subfolder = item.get("subfolder", "")329 file_type = item.get("type", "output")330331 resp = requests.get(332 f"{COMFYUI_API_URL}/view",333 params={334 "filename": filename,335 "subfolder": subfolder,336 "type": file_type,337 },338 timeout=120,339 )340 resp.raise_for_status()341342 out_filename = f"generated_{filename}"343 out_path = os.path.join(output_dir, out_filename)344 with open(out_path, "wb") as f:345 f.write(resp.content)346347 return out_filename348349 # Fallback: scan ALL values in all node outputs for .glb files350 for node_id, node_output in outputs.items():351 for key, value in node_output.items():352 if not isinstance(value, list):353 continue354 for item in value:355 if not isinstance(item, dict):356 continue357 filename = item.get("filename", "")358 if filename.lower().endswith(".glb"):359 subfolder = item.get("subfolder", "")360 file_type = item.get("type", "output")361362 resp = requests.get(363 f"{COMFYUI_API_URL}/view",364 params={365 "filename": filename,366 "subfolder": subfolder,367 "type": file_type,368 },369 timeout=120,370 )371 resp.raise_for_status()372373 out_filename = f"generated_{filename}"374 out_path = os.path.join(output_dir, out_filename)375 with open(out_path, "wb") as f:376 f.write(resp.content)377378 return out_filename379380 raise RuntimeError(381 f"No GLB model found in ComfyUI response. Available outputs: {json.dumps(outputs, indent=2)}"382 )383384385def main():386 try:387 input_json = sys.stdin.read()388 execution_input = json.loads(input_json)389 inputs = execution_input.get("inputs", {})390391 image = inputs.get("image", "")392 steps = inputs.get("steps", 25)393 guidance_scale = inputs.get("guidance_scale", 7.5)394 max_faces = inputs.get("max_faces", 200000)395 texture_size = inputs.get("texture_size", "1024")396397 if not image:398 raise ValueError("Image input is required")399400 os.makedirs(OUTPUT_DIR, exist_ok=True)401402 # Upload image to ComfyUI403 image_path = os.path.join(INPUT_DIR, image)404 if not os.path.exists(image_path):405 raise FileNotFoundError(f"Input image not found: {image_path}")406 comfyui_image_name = upload_file_to_comfyui(image_path, "image/png")407408 # Build workflow, submit, wait, download409 workflow = build_workflow(410 comfyui_image_name, steps, guidance_scale, max_faces, int(texture_size),411 )412 prompt_id = submit_prompt(workflow)413 prompt_data = wait_for_result(prompt_id)414 out_filename = download_output_model(prompt_data, OUTPUT_DIR)415416 # Log metadata to stderr417 print(418 f"prompt_id={prompt_id}, steps={steps}, guidance_scale={guidance_scale}, "419 f"max_faces={max_faces}, texture_size={texture_size}",420 file=sys.stderr,421 )422423 # Flat output -- keys match OUTPUT_SCHEMA424 output = {425 "model": out_filename,426 }427 print(json.dumps(output, indent=2))428429 except Exception as e:430 error_output = {431 "error": str(e),432 "errorType": type(e).__name__,433 "traceback": traceback.format_exc(),434 }435 print(json.dumps(error_output), file=sys.stderr)436 sys.exit(1)437438439if __name__ == "__main__":440 main()