diff --git a/__init__.py b/__init__.py index b79440f..f1d7118 100644 --- a/__init__.py +++ b/__init__.py @@ -72,6 +72,9 @@ from .modules.settings import RunwareSettings from .modules.videoAdvancedFeatureInputs import videoAdvancedFeatureInputs from .modules.advancedFeatureSettings.wanAnimateAdvancedFeatureSettings import RunwareWanAnimateAdvancedFeatureSettings from .modules.videoOutputs import RunwareVideoInferenceOutputs +from .modules.threeDInference import threeDInference +from .modules.threeDInferenceInputs import threeDInferenceInputs +from .modules.save3D import RunwareSave3D RUNWARE_COMFYUI_VERSION = "1.4.0 Beta" @@ -158,6 +161,9 @@ NODE_CLASS_MAPPINGS = { "Runware Video Advanced Feature Inputs": videoAdvancedFeatureInputs, "Runware Wan Animate Advanced Feature Settings": RunwareWanAnimateAdvancedFeatureSettings, "Runware Video Inference Outputs": RunwareVideoInferenceOutputs, + "Runware 3D Inference": threeDInference, + "Runware 3D Inference Inputs": threeDInferenceInputs, + "Runware Save 3D": RunwareSave3D, } NODE_DISPLAY_NAME_MAPPINGS = { diff --git a/clientlibs/main.js b/clientlibs/main.js index 22fde98..bcd3441 100644 --- a/clientlibs/main.js +++ b/clientlibs/main.js @@ -1,6 +1,6 @@ import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; -import { promptEnhanceHandler, syncDimensionsNodeHandler, searchNodeHandler, APIKeyHandler, captionNodeHandler, mediaUUIDHandler, videoTranscriptionHandler, videoOutputsHandler, handleCustomErrors, videoInferenceDimensionsHandler, videoModelSearchFilterHandler, audioModelSearchFilterHandler, useParameterToggleHandler, imageInferenceToggleHandler, upscalerToggleHandler, videoUpscalerToggleHandler, audioInferenceToggleHandler, acceleratorOptionsToggleHandler, bytedanceProviderSettingsToggleHandler, openaiProviderSettingsToggleHandler, lightricksProviderSettingsToggleHandler, klingProviderSettingsToggleHandler, lumaProviderSettingsToggleHandler, briaProviderSettingsToggleHandler, pixverseProviderSettingsToggleHandler, alibabaProviderSettingsToggleHandler, mireloProviderSettingsToggleHandler, googleProviderSettingsToggleHandler, syncProviderSettingsToggleHandler, syncSegmentToggleHandler, settingsToggleHandler, audioInputToggleHandler, speechInputToggleHandler, briaProviderMaskToggleHandler, wanAnimateAdvancedFeatureSettingsToggleHandler, videoAdvancedFeatureInputsToggleHandler, audioInferenceInputsToggleHandler } from "./utils.js"; +import { promptEnhanceHandler, syncDimensionsNodeHandler, searchNodeHandler, APIKeyHandler, captionNodeHandler, mediaUUIDHandler, save3DFilepathHandler, videoTranscriptionHandler, videoOutputsHandler, handleCustomErrors, videoInferenceDimensionsHandler, videoModelSearchFilterHandler, audioModelSearchFilterHandler, useParameterToggleHandler, imageInferenceToggleHandler, upscalerToggleHandler, videoUpscalerToggleHandler, audioInferenceToggleHandler, acceleratorOptionsToggleHandler, bytedanceProviderSettingsToggleHandler, openaiProviderSettingsToggleHandler, lightricksProviderSettingsToggleHandler, klingProviderSettingsToggleHandler, lumaProviderSettingsToggleHandler, briaProviderSettingsToggleHandler, pixverseProviderSettingsToggleHandler, alibabaProviderSettingsToggleHandler, mireloProviderSettingsToggleHandler, googleProviderSettingsToggleHandler, syncProviderSettingsToggleHandler, syncSegmentToggleHandler, settingsToggleHandler, audioInputToggleHandler, speechInputToggleHandler, briaProviderMaskToggleHandler, wanAnimateAdvancedFeatureSettingsToggleHandler, videoAdvancedFeatureInputsToggleHandler, audioInferenceInputsToggleHandler } from "./utils.js"; import { RUNWARE_NODE_TYPES, RUNWARE_NODE_PROPS, SEARCH_TERMS } from "./types.js"; const nodeInitList = []; @@ -10,6 +10,7 @@ app.registerExtension({ api.addEventListener('runwareError', handleCustomErrors); api.addEventListener('runwareImageCaption', captionNodeHandler); api.addEventListener('runwareMediaUUID', mediaUUIDHandler); + api.addEventListener('runwareSave3DFilepath', save3DFilepathHandler); api.addEventListener('runwareVideoTranscription', videoTranscriptionHandler); api.addEventListener('runwareVideoOutputs', videoOutputsHandler); }, @@ -52,6 +53,15 @@ app.registerExtension({ w.inputEl.style.fontFamily = "monospace"; } } + } else if(nodeClass === RUNWARE_NODE_TYPES.SAVE3D) { + // Style the filepath widget to show full path (like Media Upload mediaUUID) + const filepathWidget = node.widgets.find(widget => widget.name === "filepath"); + if(filepathWidget && filepathWidget.inputEl) { + filepathWidget.inputEl.style.width = "100%"; + filepathWidget.inputEl.style.minWidth = "300px"; + filepathWidget.inputEl.style.fontSize = "11px"; + filepathWidget.inputEl.style.fontFamily = "monospace"; + } } const nodeWidgets = node.widgets; diff --git a/clientlibs/types.js b/clientlibs/types.js index 591f170..40d6f38 100644 --- a/clientlibs/types.js +++ b/clientlibs/types.js @@ -122,6 +122,9 @@ const RUNWARE_NODE_TYPES = { VIDEOADVANCEDFEATUREINPUTS: "Runware Video Advanced Feature Inputs", WANANIMATEADVANCEDFEATURESETTINGS: "Runware Wan Animate Advanced Feature Settings", VIDEOINFERENCEOUTPUTS: "Runware Video Inference Outputs", + THREEDINFERENCE: "Runware 3D Inference", + THREEDINFERENCEINPUTS: "Runware 3D Inference Inputs", + SAVE3D: "Runware Save 3D", }; const RUNWARE_NODE_PROPS = { @@ -425,6 +428,18 @@ const RUNWARE_NODE_PROPS = { bgColor: DEFAULT_BGCOLOR, colorModeOnly: true, }, + [RUNWARE_NODE_TYPES.THREEDINFERENCE]: { + bgColor: DEFAULT_BGCOLOR, + promptEnhancer: true, + }, + [RUNWARE_NODE_TYPES.THREEDINFERENCEINPUTS]: { + bgColor: DEFAULT_BGCOLOR, + colorModeOnly: true, + }, + [RUNWARE_NODE_TYPES.SAVE3D]: { + bgColor: DEFAULT_BGCOLOR, + colorModeOnly: true, + }, }; export { diff --git a/clientlibs/utils.js b/clientlibs/utils.js index 3c08c24..af10901 100644 --- a/clientlibs/utils.js +++ b/clientlibs/utils.js @@ -70,6 +70,31 @@ function mediaUUIDHandler(msgEvent) { return false; } +function save3DFilepathHandler(msgEvent) { + const data = msgEvent.detail; + const filepath = data.filepath; + const nodeID = parseInt(data.nodeID, 10); + if (Number.isNaN(nodeID)) return false; + + if(data.success && filepath) { + const node = app.graph.getNodeById(nodeID); + if(node !== null && node !== undefined) { + const filepathWidget = node.widgets.find(widget => widget.name === "filepath"); + if(filepathWidget) { + if(filepathWidget.value !== undefined) { + filepathWidget.value = filepath; + } + if(filepathWidget.inputEl) { + filepathWidget.inputEl.value = filepath; + filepathWidget.inputEl.dispatchEvent(new Event('input', { bubbles: true })); + filepathWidget.inputEl.dispatchEvent(new Event('change', { bubbles: true })); + } + } + } + } + return false; +} + function captionNodeHandler(msgEvent) { const captionData = msgEvent.detail; const captionText = captionData.captionText; @@ -3193,6 +3218,7 @@ export { syncDimensionsNodeHandler, searchNodeHandler, mediaUUIDHandler, + save3DFilepathHandler, captionNodeHandler, videoTranscriptionHandler, videoOutputsHandler, diff --git a/modules/save3D.py b/modules/save3D.py new file mode 100644 index 0000000..a75ead3 --- /dev/null +++ b/modules/save3D.py @@ -0,0 +1,125 @@ +from .utils import runwareUtils as rwUtils +import folder_paths +import os +import re +import requests +from datetime import datetime + + +class RunwareSave3D: + """Runware Save 3D node for saving 3D model files from URL""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "3dObject": ("STRING", { + "tooltip": "3D file URL from Runware 3D Inference node." + }), + "filename_prefix": ("STRING", { + "default": "ComfyUI", + "tooltip": "Prefix for the filename." + }), + }, + "optional": { + "filepath": ("STRING", { + "placeholder": "Saved file path will appear here after execution.", + "tooltip": "This field is automatically populated with the saved file path after save.", + }), + }, + "hidden": {"node_id": "UNIQUE_ID"}, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("filepath",) + FUNCTION = "save_3d" + OUTPUT_NODE = True + CATEGORY = "Runware" + DESCRIPTION = "Save 3D model files (GLB/PLY) from Runware 3D Inference to the output folder." + + def save_3d(self, **kwargs): + """Save 3D file from URL""" + file_url = kwargs.get("3dObject", "") + filename_prefix = kwargs.get("filename_prefix", "ComfyUI") + + if not file_url or not file_url.strip(): + raise Exception("No 3D URL provided. Please connect the '3dObject' output from Runware 3D Inference node.") + + file_url = file_url.strip() + + # Get output directory + output_dir = folder_paths.get_output_directory() + threed_output_dir = os.path.join(output_dir, "3d_models") + os.makedirs(threed_output_dir, exist_ok=True) + + # Get extension from URL (handle query parameters) + url_without_params = file_url.split('?')[0] + extension = url_without_params.split('.')[-1].lower() + + if extension not in ["glb", "ply"]: + raise Exception(f"Unsupported 3D file extension '{extension}' from URL. Expected .glb or .ply.") + + # Sanitize filename_prefix to prevent path traversal or arbitrary subdir creation + prefix_basename = os.path.basename( + filename_prefix.replace(os.sep, "_").replace(os.altsep or os.sep, "_") + ) + safe_prefix = re.sub(r"[^\w\-]", "", prefix_basename) or "ComfyUI" + + # Generate filename with timestamp + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"{safe_prefix}_{timestamp}.{extension}" + filepath = os.path.join(threed_output_dir, filename) + + # Download with retry logic + max_retries = 10 + retry_delays = [2, 5, 10, 15, 20] + + for attempt in range(max_retries): + try: + response = requests.get(file_url, stream=True, timeout=60) + print(f"[Runware Save 3D] Download attempt {attempt + 1}: {response.status_code}") + + if response.status_code == 422: + print(f"[Runware Save 3D] Server still processing, waiting...") + rwUtils.time.sleep(retry_delays[min(attempt, len(retry_delays) - 1)]) + continue + + if response.status_code == 502: + print(f"[Runware Save 3D] Server error (502), retrying...") + rwUtils.time.sleep(retry_delays[min(attempt, len(retry_delays) - 1)]) + continue + + response.raise_for_status() + + with open(filepath, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + + print(f"[Runware Save 3D] Successfully saved to: {filepath}") + + # Send filepath to UI (like media upload mediaUUID) + rwUtils.sendSave3DFilepath(filepath, kwargs.get("node_id")) + + return {"result": (filepath,), "ui": {"text": (f"Saved: {filename}",)}} + + except requests.exceptions.RequestException as e: + print(f"[Runware Save 3D] Attempt {attempt + 1} failed: {e}") + if attempt < max_retries - 1: + rwUtils.time.sleep(retry_delays[min(attempt, len(retry_delays) - 1)]) + continue + else: + raise Exception(f"Failed to download 3D file after {max_retries} attempts: {e}") + except Exception as e: + print(f"[Runware Save 3D] Unexpected error: {e}") + raise Exception(f"Failed to save 3D file: {e}") + + raise Exception("Failed to download 3D file: max retries exceeded") + + +NODE_CLASS_MAPPINGS = { + "RunwareSave3D": RunwareSave3D, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "RunwareSave3D": "Runware Save 3D", +} diff --git a/modules/threeDInference.py b/modules/threeDInference.py new file mode 100644 index 0000000..f90fddc --- /dev/null +++ b/modules/threeDInference.py @@ -0,0 +1,178 @@ +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", + } + + # Output formats + OUTPUT_FORMATS = ["GLB", "PLY"] + + @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." + }), + "seed": ("INT", { + "tooltip": "Seed for reproducibility. Use the same seed to get the same result.", + "default": 1, + "min": 1, + "max": 2147483647, + }), + "outputFormat": (cls.OUTPUT_FORMATS, { + "tooltip": "Output format for the 3D model file.", + "default": "GLB", + }), + }, + "optional": { + "inputs": ("RUNWARE3DINFERENCEINPUTS", { + "tooltip": "Connect a Runware 3D Inference Inputs node to provide image and mask inputs.", + }), + }, + } + + 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) + outputFormat = kwargs.get("outputFormat", "GLB") + inputs = kwargs.get("inputs", 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() + + # Add seed + genConfig[0]["seed"] = seed + + # Handle inputs from 3D Inference Inputs node + if inputs is not None and isinstance(inputs, dict) and len(inputs) > 0: + genConfig[0]["inputs"] = inputs + + 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", +} diff --git a/modules/threeDInferenceInputs.py b/modules/threeDInferenceInputs.py new file mode 100644 index 0000000..6ffed19 --- /dev/null +++ b/modules/threeDInferenceInputs.py @@ -0,0 +1,49 @@ +from .utils import runwareUtils as rwUtils + + +class threeDInferenceInputs: + """3D Inference Inputs node for configuring 3D generation inputs""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": {}, + "optional": { + "image": ("IMAGE", { + "tooltip": "Input image for 3D inference. This image will be used as the source for generating the 3D model." + }), + "mask": ("IMAGE", { + "tooltip": "Optional mask image to specify a region of interest in the input image for 3D generation." + }), + } + } + + DESCRIPTION = "Configure custom inputs for Runware 3D Inference, including image and mask inputs." + FUNCTION = "createInputs" + RETURN_TYPES = ("RUNWARE3DINFERENCEINPUTS",) + RETURN_NAMES = ("Inputs",) + CATEGORY = "Runware" + + def createInputs(self, **kwargs): + """Create 3D inference inputs from provided parameters""" + image = kwargs.get("image", None) + mask = kwargs.get("mask", None) + + inputs = {} + + if image is not None: + inputs["image"] = rwUtils.convertTensor2IMG(image) + + if mask is not None: + inputs["mask"] = rwUtils.convertTensor2IMG(mask) + + return (inputs,) + + +NODE_CLASS_MAPPINGS = { + "Runware3DInferenceInputs": threeDInferenceInputs, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "Runware3DInferenceInputs": "Runware 3D Inference Inputs", +} diff --git a/modules/utils/runwareUtils.py b/modules/utils/runwareUtils.py index fa70e76..f624db9 100644 --- a/modules/utils/runwareUtils.py +++ b/modules/utils/runwareUtils.py @@ -295,6 +295,18 @@ def sendMediaUUID(mediaUUID, nodeID): }, ) + +def sendSave3DFilepath(filepath, nodeID): + PromptServer.instance.send_sync( + "runwareSave3DFilepath", + { + "success": True, + "filepath": filepath, + "nodeID": nodeID, + "widgetName": "filepath", + }, + ) + def sendImageCaption(captionText, nodeID): PromptServer.instance.send_sync( "runwareImageCaption", diff --git a/workflows/Runware_3D_Inference_meta_sam_3d.json b/workflows/Runware_3D_Inference_meta_sam_3d.json new file mode 100644 index 0000000..dbd0813 --- /dev/null +++ b/workflows/Runware_3D_Inference_meta_sam_3d.json @@ -0,0 +1 @@ +{"id":"4f919008-d74f-4dcb-baaa-c9f3e0cfce69","revision":0,"last_node_id":10,"last_link_id":6,"nodes":[{"id":4,"type":"LoadImage","pos":[-102.60188293457031,287.9990234375],"size":[274.080078125,314],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[3]},{"localized_name":"MASK","name":"MASK","type":"MASK","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.3.46","Node name for S&R":"LoadImage"},"widgets_values":["hat.png","image"]},{"id":2,"type":"Runware 3D Inference Inputs","pos":[-49.6855354309082,709.256591796875],"size":[222.00253295898438,46],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"image","name":"image","shape":7,"type":"IMAGE","link":3},{"localized_name":"mask","name":"mask","shape":7,"type":"IMAGE","link":null}],"outputs":[{"localized_name":"Inputs","name":"Inputs","type":"RUNWARE3DINFERENCEINPUTS","links":[2]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"938185cd42fdc0a25095c8e72ba42fdcef10913e","Node name for S&R":"Runware 3D Inference Inputs"},"widgets_values":[],"bgcolor":"#5345bf"},{"id":7,"type":"Runware Save 3D","pos":[680.3914794921875,391.1221618652344],"size":[234.08380126953125,123.42192840576172],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"3dObject","name":"3dObject","type":"STRING","widget":{"name":"3dObject"},"link":4},{"localized_name":"filename_prefix","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null},{"localized_name":"filepath","name":"filepath","shape":7,"type":"STRING","widget":{"name":"filepath"},"link":null}],"outputs":[{"localized_name":"filepath","name":"filepath","type":"STRING","links":[]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"ff6b7d8ad4471836cfbf1637e1ee1e24fe20bc48","Node name for S&R":"Runware Save 3D"},"widgets_values":["","ComfyUI","/Users/sirshenduganguly/Downloads/Runware-codebase/ComfyUI/output/3d_models/ComfyUI_20260130_101951.glb"],"bgcolor":"#5345bf"},{"id":1,"type":"Runware 3D Inference","pos":[215.56109619140625,360.20306396484375],"size":[400,200],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"inputs","name":"inputs","shape":7,"type":"RUNWARE3DINFERENCEINPUTS","link":2},{"localized_name":"Model","name":"Model","type":"COMBO","widget":{"name":"Model"},"link":null},{"localized_name":"positivePrompt","name":"positivePrompt","type":"STRING","widget":{"name":"positivePrompt"},"link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"outputFormat","name":"outputFormat","type":"COMBO","widget":{"name":"outputFormat"},"link":null}],"outputs":[{"localized_name":"3dObject","name":"3dObject","type":"STRING","links":[4]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"938185cd42fdc0a25095c8e72ba42fdcef10913e","Node name for S&R":"Runware 3D Inference"},"widgets_values":["Meta SAM 3D","a hat",1383646106,"randomize","PLY"],"bgcolor":"#5345bf"}],"links":[[2,2,0,1,0,"RUNWARE3DINFERENCEINPUTS"],[3,4,0,2,0,"IMAGE"],[4,1,0,7,0,"STRING"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.6830134553650705,"offset":[565.2445846003944,-50.69034697604053]}},"version":0.4} \ No newline at end of file