212 lines
8.7 KiB
Python
212 lines
8.7 KiB
Python
from .utils import runwareUtils as rwUtils
|
|
import comfy.model_management
|
|
|
|
|
|
class threeDInference:
|
|
"""Runware 3D Inference node for generating 3D models from images"""
|
|
|
|
# Available 3D models
|
|
THREED_MODELS = {
|
|
"Meta SAM 3D": "meta:sam@3d",
|
|
"TRELLIS.2": "microsoft:trellis-2@4b",
|
|
"Tripo 3D v3.1": "tripo:v3.1@0",
|
|
"Rodin Gen-1": "hyper3d:rodin@gen-1",
|
|
"Rodin Gen-2": "hyper3d:rodin@gen-2",
|
|
}
|
|
|
|
# Output formats
|
|
OUTPUT_FORMATS = ["GLB", "PLY", "FBX"]
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"Model": (list(cls.THREED_MODELS.keys()), {
|
|
"tooltip": "Select the 3D generation model to use.",
|
|
"default": "Meta SAM 3D",
|
|
}),
|
|
"positivePrompt": ("STRING", {
|
|
"multiline": True,
|
|
"placeholder": "Describe the 3D object you want to generate...",
|
|
"tooltip": "A text prompt describing the 3D object to generate."
|
|
}),
|
|
"useSeed": ("BOOLEAN", {
|
|
"tooltip": "Include seed in the API request. Turn off if the model should not receive a seed.",
|
|
"default": True,
|
|
}),
|
|
"seed": ("INT", {
|
|
"tooltip": "Seed for reproducibility. Only sent when 'Use Seed' is on.",
|
|
"default": 1,
|
|
"min": 1,
|
|
"max": 2147483647,
|
|
}),
|
|
"outputFormat": (cls.OUTPUT_FORMATS, {
|
|
"tooltip": "Output format for the 3D model file.",
|
|
"default": "GLB",
|
|
}),
|
|
},
|
|
"optional": {
|
|
"useOutputQuality": ("BOOLEAN", {
|
|
"tooltip": "Enable to include outputQuality in API request.",
|
|
"default": False,
|
|
}),
|
|
"outputQuality": ("INT", {
|
|
"tooltip": "Output quality (0-100). Only used when 'Use Output Quality' is enabled.",
|
|
"default": 95,
|
|
"min": 0,
|
|
"max": 100,
|
|
"step": 1,
|
|
}),
|
|
"inputs": ("RUNWARE3DINFERENCEINPUTS", {
|
|
"tooltip": "Connect Runware 3D Inference Inputs for image, mask, meshFile, images (array from images_1…4), etc.",
|
|
}),
|
|
"settings": ("RUNWARE3DINFERENCESETTINGS", {
|
|
"tooltip": "Connect Runware 3D Inference Settings for textureSize, decimationTarget, remesh, resolution, imageAutoFix, faceLimit, texture, pbr, quad, Tripo options, sparseStructure, shapeSlat, texSlat, etc.",
|
|
}),
|
|
},
|
|
}
|
|
|
|
DESCRIPTION = "Generate 3D models from images using Runware's 3D Inference API. Connect the output to 'Runware Save 3D' node to save the file."
|
|
FUNCTION = "generate3D"
|
|
RETURN_TYPES = ("STRING",)
|
|
RETURN_NAMES = ("3dObject",)
|
|
CATEGORY = "Runware"
|
|
|
|
def generate3D(self, **kwargs):
|
|
"""Generate 3D model from inputs"""
|
|
modelName = kwargs.get("Model", "Meta SAM 3D")
|
|
positivePrompt = kwargs.get("positivePrompt", "")
|
|
seed = kwargs.get("seed", 1)
|
|
use_seed = kwargs.get("useSeed", True)
|
|
outputFormat = kwargs.get("outputFormat", "GLB")
|
|
use_output_quality = kwargs.get("useOutputQuality", False)
|
|
output_quality = kwargs.get("outputQuality", 95)
|
|
inputs = kwargs.get("inputs", None)
|
|
settings = kwargs.get("settings", None)
|
|
|
|
# Get model AIR code
|
|
model = self.THREED_MODELS.get(modelName, "meta:sam@3d")
|
|
|
|
# Build generation config
|
|
genConfig = [
|
|
{
|
|
"taskType": "3dInference",
|
|
"taskUUID": rwUtils.genRandUUID(),
|
|
"model": model,
|
|
"numberResults": 1,
|
|
"outputType": "URL",
|
|
"includeCost": True,
|
|
"deliveryMethod": "async",
|
|
"outputFormat": outputFormat,
|
|
}
|
|
]
|
|
|
|
# Add positivePrompt if provided
|
|
if positivePrompt and positivePrompt.strip() != "":
|
|
genConfig[0]["positivePrompt"] = positivePrompt.strip()
|
|
|
|
if use_seed:
|
|
genConfig[0]["seed"] = seed
|
|
|
|
if use_output_quality:
|
|
genConfig[0]["outputQuality"] = int(output_quality)
|
|
|
|
# Handle inputs from 3D Inference Inputs node
|
|
if inputs is not None and isinstance(inputs, dict) and len(inputs) > 0:
|
|
genConfig[0]["inputs"] = inputs
|
|
|
|
# Handle settings from 3D Inference Settings node
|
|
if settings is not None and isinstance(settings, dict) and len(settings) > 0:
|
|
genConfig[0]["settings"] = settings
|
|
|
|
try:
|
|
print(f"[DEBUG] Sending 3D Inference Request:")
|
|
print(f"[DEBUG] Request Payload: {rwUtils.safe_json_dumps(genConfig, indent=2)}")
|
|
|
|
# Send initial request
|
|
genResult = rwUtils.inferenecRequest(genConfig)
|
|
|
|
print(f"[DEBUG] Received 3D Inference Response:")
|
|
print(f"[DEBUG] Response: {rwUtils.safe_json_dumps(genResult, indent=2)}")
|
|
|
|
except Exception as e:
|
|
raise
|
|
|
|
# Extract task UUID for polling
|
|
taskUUID = genConfig[0]["taskUUID"]
|
|
|
|
# Poll for 3D generation completion
|
|
while True:
|
|
# Check for interrupt before each poll
|
|
comfy.model_management.throw_exception_if_processing_interrupted()
|
|
|
|
# Poll for result
|
|
pollResult = rwUtils.pollVideoResult(taskUUID) # Reuse existing poll function
|
|
print(f"[Debugging] Poll result: {rwUtils.safe_json_dumps(pollResult, indent=2) if isinstance(pollResult, (dict, list)) else pollResult}")
|
|
|
|
# Check for errors
|
|
if pollResult and "errors" in pollResult and len(pollResult["errors"]) > 0:
|
|
error_info = pollResult["errors"][0]
|
|
error_message = error_info.get("message", "Unknown error")
|
|
|
|
if "responseContent" in error_info:
|
|
response_content = error_info["responseContent"]
|
|
if isinstance(response_content, str):
|
|
detailed_message = response_content
|
|
elif isinstance(response_content, dict):
|
|
detailed_message = response_content.get("message", str(response_content))
|
|
else:
|
|
detailed_message = str(response_content)
|
|
|
|
if detailed_message:
|
|
error_message = f"{error_message}\nProvider Error: {detailed_message}"
|
|
|
|
task_uuid = error_info.get("taskUUID", "unknown")
|
|
raise Exception(f"3D generation failed (Task: {task_uuid}): {error_message}")
|
|
|
|
# Check for successful completion
|
|
if pollResult and "data" in pollResult and len(pollResult["data"]) > 0:
|
|
data = pollResult["data"][0]
|
|
|
|
# Check if outputs.files exists (3D inference response format)
|
|
outputs = data.get("outputs", {})
|
|
files = outputs.get("files", [])
|
|
|
|
if len(files) > 0:
|
|
file_info = files[0]
|
|
file_url = file_info.get("url", "")
|
|
|
|
if file_url:
|
|
print(f"[3D Inference] Generated 3D file URL: {file_url}")
|
|
return (file_url,)
|
|
|
|
# Check status if available
|
|
if "status" in data:
|
|
status = data["status"]
|
|
if status == "success":
|
|
# Try alternative response format
|
|
if "outputs" in data and "files" in data["outputs"]:
|
|
files = data["outputs"]["files"]
|
|
if len(files) > 0:
|
|
file_url = files[0].get("url", "")
|
|
if file_url:
|
|
print(f"[3D Inference] Generated 3D file URL: {file_url}")
|
|
return (file_url,)
|
|
|
|
# Check for interrupt before waiting
|
|
comfy.model_management.throw_exception_if_processing_interrupted()
|
|
|
|
# Wait before next poll
|
|
for _ in range(10): # 10 x 0.1 second = 1 second total
|
|
comfy.model_management.throw_exception_if_processing_interrupted()
|
|
rwUtils.time.sleep(0.1)
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"Runware3DInference": threeDInference,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"Runware3DInference": "Runware 3D Inference",
|
|
}
|