Merge main into fix-tiffNode/feature-outputNode
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -72,6 +72,10 @@ from .modules.settings import RunwareSettings
|
||||
from .modules.videoAdvancedFeatureInputs import videoAdvancedFeatureInputs
|
||||
from .modules.advancedFeatureSettings.wanAnimateAdvancedFeatureSettings import RunwareWanAnimateAdvancedFeatureSettings
|
||||
from .modules.saveImage import RunwareSaveImage
|
||||
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 +162,10 @@ NODE_CLASS_MAPPINGS = {
|
||||
"Runware Video Advanced Feature Inputs": videoAdvancedFeatureInputs,
|
||||
"Runware Wan Animate Advanced Feature Settings": RunwareWanAnimateAdvancedFeatureSettings,
|
||||
"Runware Save Image": RunwareSaveImage,
|
||||
"Runware Video Inference Outputs": RunwareVideoInferenceOutputs,
|
||||
"Runware 3D Inference": threeDInference,
|
||||
"Runware 3D Inference Inputs": threeDInferenceInputs,
|
||||
"Runware Save 3D": RunwareSave3D,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
|
||||
+24
-1
@@ -1,6 +1,6 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
import { promptEnhanceHandler, syncDimensionsNodeHandler, searchNodeHandler, APIKeyHandler, captionNodeHandler, mediaUUIDHandler, videoTranscriptionHandler, 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,7 +10,9 @@ 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);
|
||||
},
|
||||
|
||||
async nodeCreated(node) {
|
||||
@@ -39,6 +41,27 @@ app.registerExtension({
|
||||
mediaUUIDWidget.inputEl.style.fontSize = "11px";
|
||||
mediaUUIDWidget.inputEl.style.fontFamily = "monospace";
|
||||
}
|
||||
} else if(nodeClass === RUNWARE_NODE_TYPES.VIDEOINFERENCEOUTPUTS) {
|
||||
// Style draftId and videoId widgets to show full IDs (like Media Upload)
|
||||
const draftIdWidget = node.widgets.find(widget => widget.name === "draftId");
|
||||
const videoIdWidget = node.widgets.find(widget => widget.name === "videoId");
|
||||
for(const w of [draftIdWidget, videoIdWidget]) {
|
||||
if(w && w.inputEl) {
|
||||
w.inputEl.style.width = "100%";
|
||||
w.inputEl.style.minWidth = "300px";
|
||||
w.inputEl.style.fontSize = "11px";
|
||||
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;
|
||||
|
||||
@@ -122,6 +122,10 @@ const RUNWARE_NODE_TYPES = {
|
||||
VIDEOADVANCEDFEATUREINPUTS: "Runware Video Advanced Feature Inputs",
|
||||
WANANIMATEADVANCEDFEATURESETTINGS: "Runware Wan Animate Advanced Feature Settings",
|
||||
SAVEIMAGE: "Runware Save Image",
|
||||
VIDEOINFERENCEOUTPUTS: "Runware Video Inference Outputs",
|
||||
THREEDINFERENCE: "Runware 3D Inference",
|
||||
THREEDINFERENCEINPUTS: "Runware 3D Inference Inputs",
|
||||
SAVE3D: "Runware Save 3D",
|
||||
};
|
||||
|
||||
const RUNWARE_NODE_PROPS = {
|
||||
@@ -425,6 +429,22 @@ const RUNWARE_NODE_PROPS = {
|
||||
bgColor: DEFAULT_BGCOLOR,
|
||||
colorModeOnly: true,
|
||||
},
|
||||
[RUNWARE_NODE_TYPES.VIDEOINFERENCEOUTPUTS]: {
|
||||
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 {
|
||||
|
||||
+74
-1
@@ -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;
|
||||
@@ -106,6 +131,44 @@ function videoTranscriptionHandler(msgEvent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function videoOutputsHandler(msgEvent) {
|
||||
const outputData = msgEvent.detail;
|
||||
const draftId = outputData.draftId || "";
|
||||
const videoId = outputData.videoId || "";
|
||||
const nodeID = outputData.nodeID;
|
||||
|
||||
if(outputData.success && nodeID !== undefined && nodeID !== null) {
|
||||
const nodeIdInt = typeof nodeID === 'string' ? parseInt(nodeID) : nodeID;
|
||||
const outputNode = app.graph.getNodeById(nodeIdInt);
|
||||
|
||||
if(outputNode !== null && outputNode !== undefined) {
|
||||
const draftIdWidget = outputNode.widgets.find(widget => widget.name === "draftId");
|
||||
const videoIdWidget = outputNode.widgets.find(widget => widget.name === "videoId");
|
||||
|
||||
if(draftIdWidget) {
|
||||
if(draftIdWidget.value !== undefined) draftIdWidget.value = draftId;
|
||||
if(draftIdWidget.inputEl) {
|
||||
draftIdWidget.inputEl.value = draftId;
|
||||
draftIdWidget.inputEl.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
draftIdWidget.inputEl.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
}
|
||||
|
||||
if(videoIdWidget) {
|
||||
if(videoIdWidget.value !== undefined) videoIdWidget.value = videoId;
|
||||
if(videoIdWidget.inputEl) {
|
||||
videoIdWidget.inputEl.value = videoId;
|
||||
videoIdWidget.inputEl.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
videoIdWidget.inputEl.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
}
|
||||
|
||||
outputNode.setDirtyCanvas(true);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function notifyUser(message, type="info", title = "Runware", life = 4.5) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: type, // 'info', 'success', 'warn', 'error' \\
|
||||
@@ -1131,6 +1194,8 @@ function bytedanceProviderSettingsToggleHandler(bytedanceNode) {
|
||||
const fastModeWidget = bytedanceNode.widgets.find(w => w.name === "fastMode");
|
||||
const useAudioWidget = bytedanceNode.widgets.find(w => w.name === "useAudio");
|
||||
const audioWidget = bytedanceNode.widgets.find(w => w.name === "audio");
|
||||
const useDraftWidget = bytedanceNode.widgets.find(w => w.name === "useDraft");
|
||||
const draftWidget = bytedanceNode.widgets.find(w => w.name === "draft");
|
||||
|
||||
// Helper function to toggle widget enabled state (exact same pattern)
|
||||
function toggleWidgetState(useWidget, paramWidget, paramName) {
|
||||
@@ -1189,6 +1254,10 @@ function bytedanceProviderSettingsToggleHandler(bytedanceNode) {
|
||||
if (useAudioWidget && audioWidget) {
|
||||
toggleWidgetState(useAudioWidget, audioWidget, "audio");
|
||||
}
|
||||
|
||||
if (useDraftWidget && draftWidget) {
|
||||
toggleWidgetState(useDraftWidget, draftWidget, "draft");
|
||||
}
|
||||
}
|
||||
|
||||
function openaiProviderSettingsToggleHandler(openaiNode) {
|
||||
@@ -1715,7 +1784,7 @@ function videoModelSearchFilterHandler(videoModelSearchNode) {
|
||||
],
|
||||
"PixVerse": [
|
||||
"pixverse:1@1 (PixVerse v3.5)", "pixverse:1@2 (PixVerse v4)",
|
||||
"pixverse:1@3 (PixVerse v4.5)", "pixverse:1@5-fast (PixVerse v5 Fast)", "pixverse:1@6 (PixVerse v5.5)", "pixverse:lipsync@1 (PixVerse LipSync)",
|
||||
"pixverse:1@3 (PixVerse v4.5)", "pixverse:1@5-fast (PixVerse v5 Fast)", "pixverse:1@6 (PixVerse v5.5)", "pixverse:1@7 (PixVerse v5.6)", "pixverse:lipsync@1 (PixVerse LipSync)",
|
||||
],
|
||||
"Vidu": [
|
||||
"vidu:1@0 (Vidu Q1 Classic)", "vidu:1@1 (Vidu Q1)",
|
||||
@@ -1801,6 +1870,7 @@ function videoModelSearchFilterHandler(videoModelSearchNode) {
|
||||
"pixverse:1@2": {"width": 640, "height": 360},
|
||||
"pixverse:1@3": {"width": 640, "height": 360},
|
||||
"pixverse:1@6": {"width": 640, "height": 360},
|
||||
"pixverse:1@7": {"width": 640, "height": 360},
|
||||
"pixverse:lipsync@1": {"width": 640, "height": 360},
|
||||
"vidu:1@0": {"width": 1920, "height": 1080},
|
||||
"vidu:1@1": {"width": 1920, "height": 1080},
|
||||
@@ -1869,6 +1939,7 @@ function videoModelSearchFilterHandler(videoModelSearchNode) {
|
||||
"pixverse:1@2": "360p",
|
||||
"pixverse:1@3": "360p",
|
||||
"pixverse:1@6": "360p",
|
||||
"pixverse:1@7": "360p",
|
||||
"pixverse:lipsync@1": "360p",
|
||||
"vidu:1@0": "1080p",
|
||||
"vidu:1@1": "1080p",
|
||||
@@ -3147,8 +3218,10 @@ export {
|
||||
syncDimensionsNodeHandler,
|
||||
searchNodeHandler,
|
||||
mediaUUIDHandler,
|
||||
save3DFilepathHandler,
|
||||
captionNodeHandler,
|
||||
videoTranscriptionHandler,
|
||||
videoOutputsHandler,
|
||||
handleCustomErrors,
|
||||
APIKeyHandler,
|
||||
videoInferenceDimensionsHandler,
|
||||
|
||||
@@ -47,6 +47,14 @@ class RunwareBytedanceProviderSettings:
|
||||
"tooltip": "Enable audio generation support when available.",
|
||||
"default": False,
|
||||
}),
|
||||
"useDraft": ("BOOLEAN", {
|
||||
"tooltip": "Enable to include draft parameter in provider settings",
|
||||
"default": False,
|
||||
}),
|
||||
"draft": ("BOOLEAN", {
|
||||
"tooltip": "Enable draft mode for Bytedance provider",
|
||||
"default": False,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,12 +71,14 @@ class RunwareBytedanceProviderSettings:
|
||||
useMaxSequentialImages = kwargs.get("useMaxSequentialImages", False)
|
||||
useFastMode = kwargs.get("useFastMode", False)
|
||||
useAudio = kwargs.get("useAudio", False)
|
||||
useDraft = kwargs.get("useDraft", False)
|
||||
|
||||
# Get value parameters
|
||||
cameraFixed = kwargs.get("cameraFixed", False)
|
||||
maxSequentialImages = kwargs.get("maxSequentialImages", 1)
|
||||
fastMode = kwargs.get("fastMode", False)
|
||||
audio = kwargs.get("audio", False)
|
||||
draft = kwargs.get("draft", False)
|
||||
|
||||
# Build settings dictionary - only include what is enabled
|
||||
settings = {}
|
||||
@@ -88,6 +98,10 @@ class RunwareBytedanceProviderSettings:
|
||||
# Add audio only if useAudio is enabled
|
||||
if useAudio:
|
||||
settings["audio"] = audio
|
||||
|
||||
# Add draft only if useDraft is enabled
|
||||
if useDraft:
|
||||
settings["draft"] = draft
|
||||
|
||||
# Clean up None values
|
||||
settings = {k: v for k, v in settings.items() if v is not None}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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",
|
||||
@@ -317,6 +329,19 @@ def sendVideoTranscription(transcriptionText, nodeID):
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def sendVideoOutputs(draftId, videoId, nodeID):
|
||||
"""Send draftId and videoId to frontend for display in Video Inference Outputs node textboxes."""
|
||||
PromptServer.instance.send_sync(
|
||||
"runwareVideoOutputs",
|
||||
{
|
||||
"success": True,
|
||||
"draftId": draftId or "",
|
||||
"videoId": videoId or "",
|
||||
"nodeID": nodeID,
|
||||
},
|
||||
)
|
||||
|
||||
def inferenecRequest(genConfig):
|
||||
global RUNWARE_API_KEY, RUNWARE_API_BASE_URL, SESSION_TIMEOUT
|
||||
RUNWARE_API_KEY = os.getenv("RUNWARE_API_KEY")
|
||||
|
||||
@@ -125,7 +125,8 @@ class videoBgRemoval:
|
||||
if status == "success":
|
||||
if "videoURL" in videoData or "videoBase64Data" in videoData:
|
||||
videos = rwUtils.convertVideoB64List(pollResult, 1920, 1080)
|
||||
return videos
|
||||
# SaveVideo expects a single video object, not a tuple
|
||||
return (videos[0],) if len(videos) > 0 else (None,)
|
||||
|
||||
comfy.model_management.throw_exception_if_processing_interrupted()
|
||||
|
||||
|
||||
@@ -141,8 +141,8 @@ class txt2vid:
|
||||
|
||||
DESCRIPTION = "Generates Videos Lightning Fast With Runware Video Inference Engine."
|
||||
FUNCTION = "generateVideo"
|
||||
RETURN_TYPES = ("VIDEO", "RUNWARETASK")
|
||||
RETURN_NAMES = ("VIDEO", "RW-Task")
|
||||
RETURN_TYPES = ("VIDEO", "RUNWARETASK", "RUNWARETASK")
|
||||
RETURN_NAMES = ("VIDEO", "RW-Task", "OUTPUT")
|
||||
CATEGORY = "Runware"
|
||||
|
||||
def generateVideo(self, **kwargs):
|
||||
@@ -325,7 +325,7 @@ class txt2vid:
|
||||
genConfig[0]["acceleration"] = acceleration
|
||||
|
||||
if (multiInferenceMode):
|
||||
return (None, genConfig)
|
||||
return (None, genConfig, None)
|
||||
else:
|
||||
try:
|
||||
# Debug: Print the request being sent
|
||||
@@ -388,7 +388,9 @@ class txt2vid:
|
||||
if status == "success":
|
||||
if "videoURL" in video_data or "videoBase64Data" in video_data:
|
||||
videos = rwUtils.convertVideoB64List(pollResult, width, height)
|
||||
return videos
|
||||
# SaveVideo expects a single video object, not a tuple
|
||||
video_output = videos[0] if len(videos) > 0 else None
|
||||
return (video_output, genConfig, pollResult)
|
||||
|
||||
# If status is "processing", continue polling
|
||||
|
||||
|
||||
@@ -43,6 +43,16 @@ class videoInferenceInputs:
|
||||
"Reference Voices": ("RUNWAREREFERENCEVOICES", {
|
||||
"tooltip": "Connect the Reference Voices node to provide reference voice URLs or mediaUUIDs."
|
||||
}),
|
||||
"draftId": ("STRING", {
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"tooltip": "Draft ID from a previous Bytedance video generation. Used when continuing or extending a draft output",
|
||||
}),
|
||||
"videoId": ("STRING", {
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"tooltip": "Video ID from a previous generation. Used when extending or editing an existing OpenAI Sora video",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +75,8 @@ class videoInferenceInputs:
|
||||
references = kwargs.get("References", None)
|
||||
referenceVideos = kwargs.get("Reference Videos", None)
|
||||
referenceVoices = kwargs.get("Reference Voices", None)
|
||||
draftId = kwargs.get("draftId", "")
|
||||
videoId = kwargs.get("videoId", "")
|
||||
|
||||
inputs = {}
|
||||
|
||||
@@ -112,4 +124,10 @@ class videoInferenceInputs:
|
||||
if referenceVoices is not None and len(referenceVoices) > 0:
|
||||
inputs["referenceVoices"] = referenceVoices
|
||||
|
||||
if draftId is not None and draftId.strip() != "":
|
||||
inputs["draftId"] = draftId.strip()
|
||||
|
||||
if videoId is not None and videoId.strip() != "":
|
||||
inputs["videoId"] = videoId.strip()
|
||||
|
||||
return (inputs,)
|
||||
|
||||
+10
-13
@@ -47,6 +47,7 @@ class videoModelSearch:
|
||||
"pixverse:1@3 (PixVerse v4.5)",
|
||||
"pixverse:1@5-fast (PixVerse v5 Fast)",
|
||||
"pixverse:1@6 (PixVerse v5.5)",
|
||||
"pixverse:1@7 (PixVerse v5.6)",
|
||||
"pixverse:lipsync@1 (PixVerse LipSync)",
|
||||
],
|
||||
"Vidu": [
|
||||
@@ -148,6 +149,7 @@ class videoModelSearch:
|
||||
"pixverse:1@3": {"width": 640, "height": 360},
|
||||
"pixverse:1@5-fast": {"width": 640, "height": 360},
|
||||
"pixverse:1@6": {"width": 640, "height": 360},
|
||||
"pixverse:1@7": {"width": 640, "height": 360},
|
||||
"pixverse:lipsync@1": {"width": 640, "height": 360},
|
||||
|
||||
# Vidu Models
|
||||
@@ -248,6 +250,7 @@ class videoModelSearch:
|
||||
"pixverse:1@3": "360p",
|
||||
"pixverse:1@5-fast": "360p",
|
||||
"pixverse:1@6": "360p",
|
||||
"pixverse:1@7": "360p",
|
||||
"pixverse:lipsync@1": "360p",
|
||||
|
||||
# Vidu Models
|
||||
@@ -390,8 +393,8 @@ class videoModelSearch:
|
||||
|
||||
DESCRIPTION = "Directly Search and Connect Video Models to Runware Video Inference Nodes In ComfyUI."
|
||||
FUNCTION = "videoModelSearch"
|
||||
RETURN_TYPES = ("RUNWAREVIDEOMODEL", "INT", "INT")
|
||||
RETURN_NAMES = ("Runware Video Model", "Width", "Height")
|
||||
RETURN_TYPES = ("RUNWAREVIDEOMODEL",)
|
||||
RETURN_NAMES = ("Runware Video Model",)
|
||||
CATEGORY = "Runware"
|
||||
|
||||
@classmethod
|
||||
@@ -421,33 +424,27 @@ class videoModelSearch:
|
||||
# Use custom dimensions from Width/Height widgets
|
||||
width = customWidth
|
||||
height = customHeight
|
||||
returnWidth = width
|
||||
returnHeight = height
|
||||
elif useCustomDimensions == "Disabled":
|
||||
# Don't pass width/height (use None in dict, but return 0 for INT output)
|
||||
# Don't pass width/height (use None in dict)
|
||||
width = None
|
||||
height = None
|
||||
returnWidth = 0
|
||||
returnHeight = 0
|
||||
else: # "Model Default"
|
||||
# Use model default dimensions
|
||||
width = dimensions["width"]
|
||||
height = dimensions["height"]
|
||||
returnWidth = width
|
||||
returnHeight = height
|
||||
|
||||
|
||||
resultDict = {
|
||||
"model": modelAirCode,
|
||||
"useCustomDimensions": useCustomDimensions,
|
||||
"width": width,
|
||||
"height": height,
|
||||
}
|
||||
|
||||
|
||||
# Add resolution if enabled and model supports it
|
||||
if useResolution and modelResolution is not None:
|
||||
resultDict["resolution"] = resolution
|
||||
resultDict["useResolution"] = True
|
||||
else:
|
||||
resultDict["useResolution"] = False
|
||||
|
||||
return (resultDict, returnWidth, returnHeight)
|
||||
|
||||
return (resultDict,)
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
from .utils import runwareUtils as rwUtils
|
||||
|
||||
|
||||
class RunwareVideoInferenceOutputs:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"Output": (
|
||||
"RUNWARETASK",
|
||||
{
|
||||
"tooltip": "Connect the OUTPUT from Runware Video Inference.",
|
||||
},
|
||||
),
|
||||
},
|
||||
"optional": {
|
||||
"draftId": ("STRING", {
|
||||
"placeholder": "Draft ID will appear here after generation.",
|
||||
"tooltip": "This field will be automatically populated with the draft ID from the video inference output.",
|
||||
}),
|
||||
"videoId": ("STRING", {
|
||||
"placeholder": "Video ID will appear here after generation.",
|
||||
"tooltip": "This field will be automatically populated with the video ID from the video inference output.",
|
||||
}),
|
||||
},
|
||||
"hidden": {"node_id": "UNIQUE_ID"},
|
||||
}
|
||||
|
||||
DESCRIPTION = "Extracts Runware video inference outputs such as draftId and videoId."
|
||||
FUNCTION = "get_outputs"
|
||||
RETURN_TYPES = ("STRING", "STRING")
|
||||
RETURN_NAMES = ("draftID", "videoID")
|
||||
CATEGORY = "Runware"
|
||||
OUTPUT_NODE = True # Ensure node executes even if outputs aren't used
|
||||
|
||||
def get_outputs(self, Output, draftId=None, videoId=None, **kwargs):
|
||||
draft_id = ""
|
||||
video_id = ""
|
||||
|
||||
try:
|
||||
data_list = None
|
||||
|
||||
# Handle different possible response shapes defensively
|
||||
if isinstance(Output, dict):
|
||||
if "data" in Output:
|
||||
data_list = Output.get("data", [])
|
||||
elif "response" in Output and isinstance(Output["response"], dict):
|
||||
data_list = Output["response"].get("data", [])
|
||||
|
||||
if data_list and len(data_list) > 0:
|
||||
first = data_list[0]
|
||||
outputs = first.get("outputs") or {}
|
||||
if isinstance(outputs, dict):
|
||||
# draftId is used by some providers
|
||||
draft_id = outputs.get("draftId", "") or outputs.get("draftID", "")
|
||||
# videoId / videoID used by other providers
|
||||
video_id = outputs.get("videoId", "") or outputs.get("videoID", "")
|
||||
|
||||
# Send to frontend so values appear in textboxes (like Media Upload)
|
||||
rwUtils.sendVideoOutputs(draft_id, video_id, kwargs.get("node_id"))
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Runware Video Outputs] Failed to parse outputs: {e}")
|
||||
|
||||
return (draft_id, video_id)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"RunwareVideoInferenceOutputs": RunwareVideoInferenceOutputs,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"RunwareVideoInferenceOutputs": "Runware Video Inference Outputs",
|
||||
}
|
||||
|
||||
@@ -121,7 +121,8 @@ class videoUpscaler:
|
||||
if status == "success":
|
||||
if "mediaURL" in video_data or "videoURL" in video_data or "videoBase64Data" in video_data:
|
||||
videos = rwUtils.convertVideoB64List(pollResult, 1920, 1080)
|
||||
return videos
|
||||
# SaveVideo expects a single video object, not a tuple
|
||||
return (videos[0],) if len(videos) > 0 else (None,)
|
||||
|
||||
# If status is "processing", continue polling
|
||||
|
||||
|
||||
@@ -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}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user