From 534c6ece24b9843c104740428137dc12fd791d04 Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Wed, 28 Jan 2026 23:29:09 -0500 Subject: [PATCH 1/9] Adding Runware Output Node --- __init__.py | 2 + clientlibs/main.js | 15 +++- clientlibs/types.js | 5 ++ clientlibs/utils.js | 45 +++++++++++ .../bytedanceImageProviderSettings.py | 14 ++++ modules/utils/runwareUtils.py | 13 ++++ modules/videoBgRemoval.py | 3 +- modules/videoInference.py | 10 ++- modules/videoInferenceInputs.py | 18 +++++ modules/videoOutputs.py | 75 +++++++++++++++++++ modules/videoUpscaler.py | 3 +- 11 files changed, 196 insertions(+), 7 deletions(-) create mode 100644 modules/videoOutputs.py diff --git a/__init__.py b/__init__.py index d08c07c..b79440f 100644 --- a/__init__.py +++ b/__init__.py @@ -71,6 +71,7 @@ from .modules.safetyInputs import safetyInputs from .modules.settings import RunwareSettings from .modules.videoAdvancedFeatureInputs import videoAdvancedFeatureInputs from .modules.advancedFeatureSettings.wanAnimateAdvancedFeatureSettings import RunwareWanAnimateAdvancedFeatureSettings +from .modules.videoOutputs import RunwareVideoInferenceOutputs RUNWARE_COMFYUI_VERSION = "1.4.0 Beta" @@ -156,6 +157,7 @@ NODE_CLASS_MAPPINGS = { "Runware Settings": RunwareSettings, "Runware Video Advanced Feature Inputs": videoAdvancedFeatureInputs, "Runware Wan Animate Advanced Feature Settings": RunwareWanAnimateAdvancedFeatureSettings, + "Runware Video Inference Outputs": RunwareVideoInferenceOutputs, } NODE_DISPLAY_NAME_MAPPINGS = { diff --git a/clientlibs/main.js b/clientlibs/main.js index 3041139..22fde98 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, 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, 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 = []; @@ -11,6 +11,7 @@ app.registerExtension({ api.addEventListener('runwareImageCaption', captionNodeHandler); api.addEventListener('runwareMediaUUID', mediaUUIDHandler); api.addEventListener('runwareVideoTranscription', videoTranscriptionHandler); + api.addEventListener('runwareVideoOutputs', videoOutputsHandler); }, async nodeCreated(node) { @@ -39,6 +40,18 @@ 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"; + } + } } const nodeWidgets = node.widgets; diff --git a/clientlibs/types.js b/clientlibs/types.js index 5d7d325..591f170 100644 --- a/clientlibs/types.js +++ b/clientlibs/types.js @@ -121,6 +121,7 @@ const RUNWARE_NODE_TYPES = { SETTINGS: "Runware Settings", VIDEOADVANCEDFEATUREINPUTS: "Runware Video Advanced Feature Inputs", WANANIMATEADVANCEDFEATURESETTINGS: "Runware Wan Animate Advanced Feature Settings", + VIDEOINFERENCEOUTPUTS: "Runware Video Inference Outputs", }; const RUNWARE_NODE_PROPS = { @@ -420,6 +421,10 @@ const RUNWARE_NODE_PROPS = { bgColor: DEFAULT_BGCOLOR, colorModeOnly: true, }, + [RUNWARE_NODE_TYPES.VIDEOINFERENCEOUTPUTS]: { + bgColor: DEFAULT_BGCOLOR, + colorModeOnly: true, + }, }; export { diff --git a/clientlibs/utils.js b/clientlibs/utils.js index 9a13edf..d03d23b 100644 --- a/clientlibs/utils.js +++ b/clientlibs/utils.js @@ -106,6 +106,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 +1169,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 +1229,10 @@ function bytedanceProviderSettingsToggleHandler(bytedanceNode) { if (useAudioWidget && audioWidget) { toggleWidgetState(useAudioWidget, audioWidget, "audio"); } + + if (useDraftWidget && draftWidget) { + toggleWidgetState(useDraftWidget, draftWidget, "draft"); + } } function openaiProviderSettingsToggleHandler(openaiNode) { @@ -3149,6 +3193,7 @@ export { mediaUUIDHandler, captionNodeHandler, videoTranscriptionHandler, + videoOutputsHandler, handleCustomErrors, APIKeyHandler, videoInferenceDimensionsHandler, diff --git a/modules/providerSettings/bytedanceImageProviderSettings.py b/modules/providerSettings/bytedanceImageProviderSettings.py index cd644d6..e8da86f 100644 --- a/modules/providerSettings/bytedanceImageProviderSettings.py +++ b/modules/providerSettings/bytedanceImageProviderSettings.py @@ -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} diff --git a/modules/utils/runwareUtils.py b/modules/utils/runwareUtils.py index 3be6cd9..fa70e76 100644 --- a/modules/utils/runwareUtils.py +++ b/modules/utils/runwareUtils.py @@ -317,6 +317,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") diff --git a/modules/videoBgRemoval.py b/modules/videoBgRemoval.py index 3b0debf..26c17e8 100644 --- a/modules/videoBgRemoval.py +++ b/modules/videoBgRemoval.py @@ -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() diff --git a/modules/videoInference.py b/modules/videoInference.py index 0e8525a..40d1033 100644 --- a/modules/videoInference.py +++ b/modules/videoInference.py @@ -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 diff --git a/modules/videoInferenceInputs.py b/modules/videoInferenceInputs.py index cc12b1a..d367eb6 100644 --- a/modules/videoInferenceInputs.py +++ b/modules/videoInferenceInputs.py @@ -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,) diff --git a/modules/videoOutputs.py b/modules/videoOutputs.py new file mode 100644 index 0000000..ee86f45 --- /dev/null +++ b/modules/videoOutputs.py @@ -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 = ("Draft ID", "Video ID") + 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", +} + diff --git a/modules/videoUpscaler.py b/modules/videoUpscaler.py index ea9b522..735d714 100644 --- a/modules/videoUpscaler.py +++ b/modules/videoUpscaler.py @@ -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 From a64e0c5839d32d9779684039a56e055375482e14 Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Wed, 28 Jan 2026 23:33:37 -0500 Subject: [PATCH 2/9] add workflow --- workflows/Runware_Video_Seedance_1_5_Pro_draftId.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 workflows/Runware_Video_Seedance_1_5_Pro_draftId.json diff --git a/workflows/Runware_Video_Seedance_1_5_Pro_draftId.json b/workflows/Runware_Video_Seedance_1_5_Pro_draftId.json new file mode 100644 index 0000000..4d1ef3c --- /dev/null +++ b/workflows/Runware_Video_Seedance_1_5_Pro_draftId.json @@ -0,0 +1 @@ +{"id":"6c4c28f8-b635-4821-80f9-9dfe0b5782a3","revision":0,"last_node_id":15,"last_link_id":14,"nodes":[{"id":6,"type":"Runware Bytedance Provider Settings","pos":[629.6443481445312,1101.2879638671875],"size":[336.4712829589844,274],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"useCameraFixed","name":"useCameraFixed","shape":7,"type":"BOOLEAN","widget":{"name":"useCameraFixed"},"link":null},{"localized_name":"cameraFixed","name":"cameraFixed","shape":7,"type":"BOOLEAN","widget":{"name":"cameraFixed"},"link":null},{"localized_name":"useMaxSequentialImages","name":"useMaxSequentialImages","shape":7,"type":"BOOLEAN","widget":{"name":"useMaxSequentialImages"},"link":null},{"localized_name":"maxSequentialImages","name":"maxSequentialImages","shape":7,"type":"INT","widget":{"name":"maxSequentialImages"},"link":null},{"localized_name":"useFastMode","name":"useFastMode","shape":7,"type":"BOOLEAN","widget":{"name":"useFastMode"},"link":null},{"localized_name":"fastMode","name":"fastMode","shape":7,"type":"BOOLEAN","widget":{"name":"fastMode"},"link":null},{"localized_name":"useAudio","name":"useAudio","shape":7,"type":"BOOLEAN","widget":{"name":"useAudio"},"link":null},{"localized_name":"audio","name":"audio","shape":7,"type":"BOOLEAN","widget":{"name":"audio"},"link":null},{"localized_name":"useDraft","name":"useDraft","shape":7,"type":"BOOLEAN","widget":{"name":"useDraft"},"link":null},{"localized_name":"draft","name":"draft","shape":7,"type":"BOOLEAN","widget":{"name":"draft"},"link":null}],"outputs":[{"localized_name":"providerSettings","name":"providerSettings","type":"RUNWAREPROVIDERSETTINGS","links":[5]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"8b8e95b0681cf99f2e9857fe92a83cd38cea8c11","Node name for S&R":"Runware Bytedance Provider Settings"},"widgets_values":[false,false,false,1,false,false,true,true,true,true],"bgcolor":"#5345bf"},{"id":5,"type":"Runware Video Model Search","pos":[-144.4015350341797,658.8407592773438],"size":[287.380859375,290],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"Model Search","name":"Model Search","type":"STRING","widget":{"name":"Model Search"},"link":null},{"localized_name":"Model Architecture","name":"Model Architecture","type":"COMBO","widget":{"name":"Model Architecture"},"link":null},{"localized_name":"VideoList","name":"VideoList","type":"COMBO","widget":{"name":"VideoList"},"link":null},{"localized_name":"Use Search Value","name":"Use Search Value","type":"BOOLEAN","widget":{"name":"Use Search Value"},"link":null},{"localized_name":"useCustomDimensions","name":"useCustomDimensions","type":"COMBO","widget":{"name":"useCustomDimensions"},"link":null},{"localized_name":"Width","name":"Width","type":"INT","widget":{"name":"Width"},"link":null},{"localized_name":"Height","name":"Height","type":"INT","widget":{"name":"Height"},"link":null},{"localized_name":"useResolution","name":"useResolution","type":"BOOLEAN","widget":{"name":"useResolution"},"link":null},{"localized_name":"resolution","name":"resolution","type":"COMBO","widget":{"name":"resolution"},"link":null}],"outputs":[{"localized_name":"Runware Video Model","name":"Runware Video Model","type":"RUNWAREVIDEOMODEL","links":[4]},{"localized_name":"Width","name":"Width","type":"INT","links":null},{"localized_name":"Height","name":"Height","type":"INT","links":null}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"aae3a8f4a4e3019c2b2debece39adf32b33cd078","Node name for S&R":"Runware Video Model Search"},"widgets_values":["","Bytedance","bytedance:seedance@1.5-pro (Seedance 1.5 Pro)",false,"Model Default",864,480,false,"1080p"],"bgcolor":"#5345bf"},{"id":2,"type":"SaveVideo","pos":[650.3009643554688,676.3864135742188],"size":[270,368],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"video","name":"video","type":"VIDEO","link":3},{"localized_name":"filename_prefix","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null},{"localized_name":"format","name":"format","type":"COMBO","widget":{"name":"format"},"link":null},{"localized_name":"codec","name":"codec","type":"COMBO","widget":{"name":"codec"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.46","Node name for S&R":"SaveVideo"},"widgets_values":["video/ComfyUI","auto","auto"]},{"id":1,"type":"Runware Video Inference","pos":[192.32894897460938,676.3584594726562],"size":[400,760],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"Model","name":"Model","type":"RUNWAREVIDEOMODEL","link":4},{"localized_name":"frameImages","name":"frameImages","shape":7,"type":"RUNWAREFRAMEIMAGES","link":null},{"localized_name":"referenceImages","name":"referenceImages","shape":7,"type":"RUNWAREREFERENCEIMAGES","link":null},{"localized_name":"providerSettings","name":"providerSettings","shape":7,"type":"RUNWAREPROVIDERSETTINGS","link":5},{"localized_name":"inputAudios","name":"inputAudios","shape":7,"type":"RUNWAREINPUTAUDIOS","link":null},{"localized_name":"referenceVideos","name":"referenceVideos","shape":7,"type":"RUNWAREREFERENCEVIDEOS","link":null},{"localized_name":"inputs","name":"inputs","shape":7,"type":"RUNWAREVIDEOINFERENCEINPUTS","link":null},{"localized_name":"safetyInputs","name":"safetyInputs","shape":7,"type":"RUNWARESAFETYINPUTS","link":null},{"localized_name":"videoAdvancedFeatureInputs","name":"videoAdvancedFeatureInputs","shape":7,"type":"RUNWAREVIDEOADVANCEDFEATUREINPUTS","link":null},{"localized_name":"Accelerator","name":"Accelerator","shape":7,"type":"RUNWAREACCELERATOR","link":null},{"localized_name":"Multi Inference Mode","name":"Multi Inference Mode","type":"BOOLEAN","widget":{"name":"Multi Inference Mode"},"link":null},{"localized_name":"Prompt Weighting","name":"Prompt Weighting","type":"COMBO","widget":{"name":"Prompt Weighting"},"link":null},{"localized_name":"useDuration","name":"useDuration","type":"BOOLEAN","widget":{"name":"useDuration"},"link":null},{"localized_name":"duration","name":"duration","type":"INT","widget":{"name":"duration"},"link":null},{"localized_name":"useFps","name":"useFps","type":"BOOLEAN","widget":{"name":"useFps"},"link":null},{"localized_name":"fps","name":"fps","type":"INT","widget":{"name":"fps"},"link":null},{"localized_name":"outputFormat","name":"outputFormat","type":"COMBO","widget":{"name":"outputFormat"},"link":null},{"localized_name":"useSeed","name":"useSeed","type":"BOOLEAN","widget":{"name":"useSeed"},"link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"useSteps","name":"useSteps","type":"BOOLEAN","widget":{"name":"useSteps"},"link":null},{"localized_name":"steps","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"useBatchSize","name":"useBatchSize","type":"BOOLEAN","widget":{"name":"useBatchSize"},"link":null},{"localized_name":"batchSize","name":"batchSize","type":"INT","widget":{"name":"batchSize"},"link":null},{"localized_name":"acceleration","name":"acceleration","type":"COMBO","widget":{"name":"acceleration"},"link":null},{"localized_name":"positivePrompt","name":"positivePrompt","shape":7,"type":"STRING","widget":{"name":"positivePrompt"},"link":null},{"localized_name":"negativePrompt","name":"negativePrompt","shape":7,"type":"STRING","widget":{"name":"negativePrompt"},"link":null},{"localized_name":"speechVoice","name":"speechVoice","shape":7,"type":"STRING","widget":{"name":"speechVoice"},"link":null},{"localized_name":"speechText","name":"speechText","shape":7,"type":"STRING","widget":{"name":"speechText"},"link":null}],"outputs":[{"localized_name":"VIDEO","name":"VIDEO","type":"VIDEO","links":[3]},{"localized_name":"RW-Task","name":"RW-Task","type":"RUNWARETASK","links":[]},{"localized_name":"OUTPUT","name":"OUTPUT","type":"RUNWARETASK","links":[14]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"aae3a8f4a4e3019c2b2debece39adf32b33cd078","Node name for S&R":"Runware Video Inference"},"widgets_values":[false,"Disabled",true,5,false,24,"mp4",true,1421673473,"randomize",false,20,false,1,"none","A confident man in his 30s stands at a podium, delivering a passionate speech to a large audience in a modern auditorium.","","",""],"bgcolor":"#5345bf"},{"id":15,"type":"Runware Video Inference Outputs","pos":[967.6734619140625,707.1176147460938],"size":[310.5494079589844,102],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"Output","name":"Output","type":"RUNWARETASK","link":14},{"localized_name":"draftId","name":"draftId","shape":7,"type":"STRING","widget":{"name":"draftId"},"link":null},{"localized_name":"videoId","name":"videoId","shape":7,"type":"STRING","widget":{"name":"videoId"},"link":null}],"outputs":[{"localized_name":"Draft ID","name":"Draft ID","type":"STRING","links":null},{"localized_name":"Video ID","name":"Video ID","type":"STRING","links":null}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"534c6ece24b9843c104740428137dc12fd791d04","Node name for S&R":"Runware Video Inference Outputs"},"widgets_values":["",""],"bgcolor":"#5345bf"}],"links":[[3,1,0,2,0,"VIDEO"],[4,5,0,1,0,"RUNWAREVIDEOMODEL"],[5,6,0,1,3,"RUNWAREPROVIDERSETTINGS"],[14,1,2,15,0,"RUNWARETASK"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.682112756645477,"offset":[547.1011561383572,-419.0151571108246]}},"version":0.4} \ No newline at end of file From a72a8a04616748ab9be583cdc3b22a23c5bd0ea1 Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Thu, 29 Jan 2026 11:52:30 -0500 Subject: [PATCH 3/9] Adding support for Sora remix --- modules/utils/runwareUtils.py | 2 +- modules/videoModelSearch.py | 20 +++++++------------- modules/videoOutputs.py | 2 +- workflows/Runware_Video_Sora_Remix.json | 1 + 4 files changed, 10 insertions(+), 15 deletions(-) create mode 100644 workflows/Runware_Video_Sora_Remix.json diff --git a/modules/utils/runwareUtils.py b/modules/utils/runwareUtils.py index fa70e76..2587abb 100644 --- a/modules/utils/runwareUtils.py +++ b/modules/utils/runwareUtils.py @@ -158,7 +158,7 @@ def getCustomEndpoint(): custom_endpoint = os.getenv("RUNWARE_CUSTOM_ENDPOINT") if custom_endpoint and isinstance(custom_endpoint, str): return custom_endpoint - return "https://api.runware.ai/v1" + return "https://api.runware.dev/v1" SESSION_TIMEOUT = getTimeout() RUNWARE_API_KEY = getAPIKey() diff --git a/modules/videoModelSearch.py b/modules/videoModelSearch.py index 855abd0..18fd54a 100644 --- a/modules/videoModelSearch.py +++ b/modules/videoModelSearch.py @@ -390,8 +390,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 +421,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,) diff --git a/modules/videoOutputs.py b/modules/videoOutputs.py index ee86f45..5e3ca5b 100644 --- a/modules/videoOutputs.py +++ b/modules/videoOutputs.py @@ -29,7 +29,7 @@ class RunwareVideoInferenceOutputs: DESCRIPTION = "Extracts Runware video inference outputs such as draftId and videoId." FUNCTION = "get_outputs" RETURN_TYPES = ("STRING", "STRING") - RETURN_NAMES = ("Draft ID", "Video ID") + RETURN_NAMES = ("draftID", "videoID") CATEGORY = "Runware" OUTPUT_NODE = True # Ensure node executes even if outputs aren't used diff --git a/workflows/Runware_Video_Sora_Remix.json b/workflows/Runware_Video_Sora_Remix.json new file mode 100644 index 0000000..2ccfe3a --- /dev/null +++ b/workflows/Runware_Video_Sora_Remix.json @@ -0,0 +1 @@ +{"id":"6c4c28f8-b635-4821-80f9-9dfe0b5782a3","revision":0,"last_node_id":21,"last_link_id":20,"nodes":[{"id":16,"type":"Runware Video Inference","pos":[1011.5185546875,679.8372192382812],"size":[400,760],"flags":{},"order":6,"mode":0,"inputs":[{"localized_name":"Model","name":"Model","type":"RUNWAREVIDEOMODEL","link":20},{"localized_name":"frameImages","name":"frameImages","shape":7,"type":"RUNWAREFRAMEIMAGES","link":null},{"localized_name":"referenceImages","name":"referenceImages","shape":7,"type":"RUNWAREREFERENCEIMAGES","link":null},{"localized_name":"providerSettings","name":"providerSettings","shape":7,"type":"RUNWAREPROVIDERSETTINGS","link":null},{"localized_name":"inputAudios","name":"inputAudios","shape":7,"type":"RUNWAREINPUTAUDIOS","link":null},{"localized_name":"referenceVideos","name":"referenceVideos","shape":7,"type":"RUNWAREREFERENCEVIDEOS","link":null},{"localized_name":"inputs","name":"inputs","shape":7,"type":"RUNWAREVIDEOINFERENCEINPUTS","link":17},{"localized_name":"safetyInputs","name":"safetyInputs","shape":7,"type":"RUNWARESAFETYINPUTS","link":null},{"localized_name":"videoAdvancedFeatureInputs","name":"videoAdvancedFeatureInputs","shape":7,"type":"RUNWAREVIDEOADVANCEDFEATUREINPUTS","link":null},{"localized_name":"Accelerator","name":"Accelerator","shape":7,"type":"RUNWAREACCELERATOR","link":null},{"localized_name":"Multi Inference Mode","name":"Multi Inference Mode","type":"BOOLEAN","widget":{"name":"Multi Inference Mode"},"link":null},{"localized_name":"Prompt Weighting","name":"Prompt Weighting","type":"COMBO","widget":{"name":"Prompt Weighting"},"link":null},{"localized_name":"useDuration","name":"useDuration","type":"BOOLEAN","widget":{"name":"useDuration"},"link":null},{"localized_name":"duration","name":"duration","type":"INT","widget":{"name":"duration"},"link":null},{"localized_name":"useFps","name":"useFps","type":"BOOLEAN","widget":{"name":"useFps"},"link":null},{"localized_name":"fps","name":"fps","type":"INT","widget":{"name":"fps"},"link":null},{"localized_name":"outputFormat","name":"outputFormat","type":"COMBO","widget":{"name":"outputFormat"},"link":null},{"localized_name":"useSeed","name":"useSeed","type":"BOOLEAN","widget":{"name":"useSeed"},"link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"useSteps","name":"useSteps","type":"BOOLEAN","widget":{"name":"useSteps"},"link":null},{"localized_name":"steps","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"useBatchSize","name":"useBatchSize","type":"BOOLEAN","widget":{"name":"useBatchSize"},"link":null},{"localized_name":"batchSize","name":"batchSize","type":"INT","widget":{"name":"batchSize"},"link":null},{"localized_name":"acceleration","name":"acceleration","type":"COMBO","widget":{"name":"acceleration"},"link":null},{"localized_name":"positivePrompt","name":"positivePrompt","shape":7,"type":"STRING","widget":{"name":"positivePrompt"},"link":null},{"localized_name":"negativePrompt","name":"negativePrompt","shape":7,"type":"STRING","widget":{"name":"negativePrompt"},"link":null},{"localized_name":"speechVoice","name":"speechVoice","shape":7,"type":"STRING","widget":{"name":"speechVoice"},"link":null},{"localized_name":"speechText","name":"speechText","shape":7,"type":"STRING","widget":{"name":"speechText"},"link":null}],"outputs":[{"localized_name":"VIDEO","name":"VIDEO","type":"VIDEO","links":[18]},{"localized_name":"RW-Task","name":"RW-Task","type":"RUNWARETASK","links":null},{"localized_name":"OUTPUT","name":"OUTPUT","type":"RUNWARETASK","links":null}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"e77fb39dcd950fbe9f6c04d025066341955bc42f","Node name for S&R":"Runware Video Inference"},"widgets_values":[false,"Disabled",false,5,false,24,"mp4",false,1971848550,"randomize",false,20,false,1,"none","let's make it in the amazon jungle instead","","",""],"bgcolor":"#5345bf"},{"id":1,"type":"Runware Video Inference","pos":[-144.00689697265625,656.3722534179688],"size":[400,760],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"Model","name":"Model","type":"RUNWAREVIDEOMODEL","link":19},{"localized_name":"frameImages","name":"frameImages","shape":7,"type":"RUNWAREFRAMEIMAGES","link":null},{"localized_name":"referenceImages","name":"referenceImages","shape":7,"type":"RUNWAREREFERENCEIMAGES","link":null},{"localized_name":"providerSettings","name":"providerSettings","shape":7,"type":"RUNWAREPROVIDERSETTINGS","link":null},{"localized_name":"inputAudios","name":"inputAudios","shape":7,"type":"RUNWAREINPUTAUDIOS","link":null},{"localized_name":"referenceVideos","name":"referenceVideos","shape":7,"type":"RUNWAREREFERENCEVIDEOS","link":null},{"localized_name":"inputs","name":"inputs","shape":7,"type":"RUNWAREVIDEOINFERENCEINPUTS","link":null},{"localized_name":"safetyInputs","name":"safetyInputs","shape":7,"type":"RUNWARESAFETYINPUTS","link":null},{"localized_name":"videoAdvancedFeatureInputs","name":"videoAdvancedFeatureInputs","shape":7,"type":"RUNWAREVIDEOADVANCEDFEATUREINPUTS","link":null},{"localized_name":"Accelerator","name":"Accelerator","shape":7,"type":"RUNWAREACCELERATOR","link":null},{"localized_name":"Multi Inference Mode","name":"Multi Inference Mode","type":"BOOLEAN","widget":{"name":"Multi Inference Mode"},"link":null},{"localized_name":"Prompt Weighting","name":"Prompt Weighting","type":"COMBO","widget":{"name":"Prompt Weighting"},"link":null},{"localized_name":"useDuration","name":"useDuration","type":"BOOLEAN","widget":{"name":"useDuration"},"link":null},{"localized_name":"duration","name":"duration","type":"INT","widget":{"name":"duration"},"link":null},{"localized_name":"useFps","name":"useFps","type":"BOOLEAN","widget":{"name":"useFps"},"link":null},{"localized_name":"fps","name":"fps","type":"INT","widget":{"name":"fps"},"link":null},{"localized_name":"outputFormat","name":"outputFormat","type":"COMBO","widget":{"name":"outputFormat"},"link":null},{"localized_name":"useSeed","name":"useSeed","type":"BOOLEAN","widget":{"name":"useSeed"},"link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"useSteps","name":"useSteps","type":"BOOLEAN","widget":{"name":"useSteps"},"link":null},{"localized_name":"steps","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"useBatchSize","name":"useBatchSize","type":"BOOLEAN","widget":{"name":"useBatchSize"},"link":null},{"localized_name":"batchSize","name":"batchSize","type":"INT","widget":{"name":"batchSize"},"link":null},{"localized_name":"acceleration","name":"acceleration","type":"COMBO","widget":{"name":"acceleration"},"link":null},{"localized_name":"positivePrompt","name":"positivePrompt","shape":7,"type":"STRING","widget":{"name":"positivePrompt"},"link":null},{"localized_name":"negativePrompt","name":"negativePrompt","shape":7,"type":"STRING","widget":{"name":"negativePrompt"},"link":null},{"localized_name":"speechVoice","name":"speechVoice","shape":7,"type":"STRING","widget":{"name":"speechVoice"},"link":null},{"localized_name":"speechText","name":"speechText","shape":7,"type":"STRING","widget":{"name":"speechText"},"link":null}],"outputs":[{"localized_name":"VIDEO","name":"VIDEO","type":"VIDEO","links":[3]},{"localized_name":"RW-Task","name":"RW-Task","type":"RUNWARETASK","links":[]},{"localized_name":"OUTPUT","name":"OUTPUT","type":"RUNWARETASK","links":[14]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"aae3a8f4a4e3019c2b2debece39adf32b33cd078","Node name for S&R":"Runware Video Inference"},"widgets_values":[false,"Disabled",false,5,false,24,"mp4",false,144770345,"randomize",false,20,false,1,"none","A beautiful sunset over mountains with clouds moving slowly.","","",""],"bgcolor":"#5345bf"},{"id":2,"type":"SaveVideo","pos":[298.1094970703125,659.2493286132812],"size":[270,368],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"video","name":"video","type":"VIDEO","link":3},{"localized_name":"filename_prefix","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null},{"localized_name":"format","name":"format","type":"COMBO","widget":{"name":"format"},"link":null},{"localized_name":"codec","name":"codec","type":"COMBO","widget":{"name":"codec"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.46","Node name for S&R":"SaveVideo"},"widgets_values":["video/ComfyUI","auto","auto"]},{"id":15,"type":"Runware Video Inference Outputs","pos":[299.017578125,1096.332275390625],"size":[310.5494079589844,102],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"Output","name":"Output","type":"RUNWARETASK","link":14},{"localized_name":"draftId","name":"draftId","shape":7,"type":"STRING","widget":{"name":"draftId"},"link":null},{"localized_name":"videoId","name":"videoId","shape":7,"type":"STRING","widget":{"name":"videoId"},"link":null}],"outputs":[{"localized_name":"draftID","name":"draftID","type":"STRING","links":null},{"localized_name":"videoID","name":"videoID","type":"STRING","links":[16]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"534c6ece24b9843c104740428137dc12fd791d04","Node name for S&R":"Runware Video Inference Outputs"},"widgets_values":["","video_697b8bf146048190bc95a6adad90ce6c09eefe0e438a7381"],"bgcolor":"#5345bf"},{"id":20,"type":"Runware Video Model Search","pos":[-459.10333251953125,670.7033081054688],"size":[287.380859375,250],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"Model Search","name":"Model Search","type":"STRING","widget":{"name":"Model Search"},"link":null},{"localized_name":"Model Architecture","name":"Model Architecture","type":"COMBO","widget":{"name":"Model Architecture"},"link":null},{"localized_name":"VideoList","name":"VideoList","type":"COMBO","widget":{"name":"VideoList"},"link":null},{"localized_name":"Use Search Value","name":"Use Search Value","type":"BOOLEAN","widget":{"name":"Use Search Value"},"link":null},{"localized_name":"useCustomDimensions","name":"useCustomDimensions","type":"COMBO","widget":{"name":"useCustomDimensions"},"link":null},{"localized_name":"Width","name":"Width","type":"INT","widget":{"name":"Width"},"link":null},{"localized_name":"Height","name":"Height","type":"INT","widget":{"name":"Height"},"link":null},{"localized_name":"useResolution","name":"useResolution","type":"BOOLEAN","widget":{"name":"useResolution"},"link":null},{"localized_name":"resolution","name":"resolution","type":"COMBO","widget":{"name":"resolution"},"link":null}],"outputs":[{"localized_name":"Runware Video Model","name":"Runware Video Model","type":"RUNWAREVIDEOMODEL","links":[19]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"e77fb39dcd950fbe9f6c04d025066341955bc42f","Node name for S&R":"Runware Video Model Search"},"widgets_values":["","OpenAI","openai:3@1 (OpenAI Sora 3.1)",false,"Model Default",1280,720,false,"1080p"],"bgcolor":"#5345bf"},{"id":21,"type":"Runware Video Model Search","pos":[591.4793090820312,671.404541015625],"size":[287.380859375,250],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"Model Search","name":"Model Search","type":"STRING","widget":{"name":"Model Search"},"link":null},{"localized_name":"Model Architecture","name":"Model Architecture","type":"COMBO","widget":{"name":"Model Architecture"},"link":null},{"localized_name":"VideoList","name":"VideoList","type":"COMBO","widget":{"name":"VideoList"},"link":null},{"localized_name":"Use Search Value","name":"Use Search Value","type":"BOOLEAN","widget":{"name":"Use Search Value"},"link":null},{"localized_name":"useCustomDimensions","name":"useCustomDimensions","type":"COMBO","widget":{"name":"useCustomDimensions"},"link":null},{"localized_name":"Width","name":"Width","type":"INT","widget":{"name":"Width"},"link":null},{"localized_name":"Height","name":"Height","type":"INT","widget":{"name":"Height"},"link":null},{"localized_name":"useResolution","name":"useResolution","type":"BOOLEAN","widget":{"name":"useResolution"},"link":null},{"localized_name":"resolution","name":"resolution","type":"COMBO","widget":{"name":"resolution"},"link":null}],"outputs":[{"localized_name":"Runware Video Model","name":"Runware Video Model","type":"RUNWAREVIDEOMODEL","links":[20]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"e77fb39dcd950fbe9f6c04d025066341955bc42f","Node name for S&R":"Runware Video Model Search"},"widgets_values":["","OpenAI","openai:3@1 (OpenAI Sora 3.1)",false,"Disabled",0,0,false,"1080p"],"bgcolor":"#5345bf"},{"id":18,"type":"Runware Video Inference Inputs","pos":[618.173583984375,992.7439575195312],"size":[320.0390625,290],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"Image","name":"Image","shape":7,"type":"IMAGE","link":null},{"localized_name":"Frame Images","name":"Frame Images","shape":7,"type":"RUNWAREVIDEOINPUTSFRAMEIMAGES","link":null},{"localized_name":"Audio Inputs","name":"Audio Inputs","shape":7,"type":"RUNWAREAUDIOINPUT","link":null},{"localized_name":"Speech Inputs","name":"Speech Inputs","shape":7,"type":"RUNWARESPEECHINPUT","link":null},{"localized_name":"Mask","name":"Mask","shape":7,"type":"IMAGE","link":null},{"localized_name":"Frame","name":"Frame","shape":7,"type":"IMAGE","link":null},{"localized_name":"References","name":"References","shape":7,"type":"RUNWAREVIDEOINPUTSREFERENCEIMAGES","link":null},{"localized_name":"Reference Videos","name":"Reference Videos","shape":7,"type":"RUNWAREREFERENCEVIDEOS","link":null},{"localized_name":"Reference Voices","name":"Reference Voices","shape":7,"type":"RUNWAREREFERENCEVOICES","link":null},{"localized_name":"Audio","name":"Audio","shape":7,"type":"STRING","widget":{"name":"Audio"},"link":null},{"localized_name":"Video","name":"Video","shape":7,"type":"STRING","widget":{"name":"Video"},"link":null},{"localized_name":"draftId","name":"draftId","shape":7,"type":"STRING","widget":{"name":"draftId"},"link":null},{"localized_name":"videoId","name":"videoId","shape":7,"type":"STRING","widget":{"name":"videoId"},"link":16}],"outputs":[{"localized_name":"Video Inference Inputs","name":"Video Inference Inputs","type":"RUNWAREVIDEOINFERENCEINPUTS","links":[17]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"e77fb39dcd950fbe9f6c04d025066341955bc42f","Node name for S&R":"Runware Video Inference Inputs"},"widgets_values":["","","",""],"bgcolor":"#5345bf"},{"id":19,"type":"SaveVideo","pos":[1425.71533203125,677.9049682617188],"size":[374,461.2901611328125],"flags":{},"order":7,"mode":0,"inputs":[{"localized_name":"video","name":"video","type":"VIDEO","link":18},{"localized_name":"filename_prefix","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null},{"localized_name":"format","name":"format","type":"COMBO","widget":{"name":"format"},"link":null},{"localized_name":"codec","name":"codec","type":"COMBO","widget":{"name":"codec"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.46","Node name for S&R":"SaveVideo"},"widgets_values":["video/ComfyUI","auto","auto"]}],"links":[[3,1,0,2,0,"VIDEO"],[14,1,2,15,0,"RUNWARETASK"],[16,15,1,18,12,"STRING"],[17,18,0,16,6,"RUNWAREVIDEOINFERENCEINPUTS"],[18,16,0,19,0,"VIDEO"],[19,20,0,1,0,"RUNWAREVIDEOMODEL"],[20,21,0,16,0,"RUNWAREVIDEOMODEL"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.682112756645477,"offset":[546.8362966107499,-418.55702171172015]}},"version":0.4} \ No newline at end of file From 461b5aac08e46fb84318f4f8ceb86fa2c7f6e1a1 Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Thu, 29 Jan 2026 12:05:11 -0500 Subject: [PATCH 4/9] fix url --- modules/utils/runwareUtils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/utils/runwareUtils.py b/modules/utils/runwareUtils.py index 2587abb..fa70e76 100644 --- a/modules/utils/runwareUtils.py +++ b/modules/utils/runwareUtils.py @@ -158,7 +158,7 @@ def getCustomEndpoint(): custom_endpoint = os.getenv("RUNWARE_CUSTOM_ENDPOINT") if custom_endpoint and isinstance(custom_endpoint, str): return custom_endpoint - return "https://api.runware.dev/v1" + return "https://api.runware.ai/v1" SESSION_TIMEOUT = getTimeout() RUNWARE_API_KEY = getAPIKey() From b26acaebf40764c3e2aa23424ea99b473b16115a Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Thu, 29 Jan 2026 14:16:34 -0500 Subject: [PATCH 5/9] Ad workflow --- clientlibs/utils.js | 4 +++- modules/videoModelSearch.py | 3 +++ workflows/Runware_Image_to_Video_Pixverse_v5.6.json | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 workflows/Runware_Image_to_Video_Pixverse_v5.6.json diff --git a/clientlibs/utils.js b/clientlibs/utils.js index d03d23b..3c08c24 100644 --- a/clientlibs/utils.js +++ b/clientlibs/utils.js @@ -1759,7 +1759,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)", @@ -1845,6 +1845,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}, @@ -1913,6 +1914,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", diff --git a/modules/videoModelSearch.py b/modules/videoModelSearch.py index 18fd54a..891ea51 100644 --- a/modules/videoModelSearch.py +++ b/modules/videoModelSearch.py @@ -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 diff --git a/workflows/Runware_Image_to_Video_Pixverse_v5.6.json b/workflows/Runware_Image_to_Video_Pixverse_v5.6.json new file mode 100644 index 0000000..23c50f1 --- /dev/null +++ b/workflows/Runware_Image_to_Video_Pixverse_v5.6.json @@ -0,0 +1 @@ +{"id":"5cf854c6-824e-4e51-995c-a2c146c44890","revision":0,"last_node_id":30,"last_link_id":44,"nodes":[{"id":1,"type":"SaveVideo","pos":[899.7476806640625,376.3740539550781],"size":[270,368],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"video","name":"video","type":"VIDEO","link":41},{"localized_name":"filename_prefix","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null},{"localized_name":"format","name":"format","type":"COMBO","widget":{"name":"format"},"link":null},{"localized_name":"codec","name":"codec","type":"COMBO","widget":{"name":"codec"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.46","Node name for S&R":"SaveVideo"},"widgets_values":["video/ComfyUI","mp4","auto"]},{"id":9,"type":"LoadImage","pos":[-214.6199188232422,362.6542053222656],"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":[5]},{"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":["6fq8JDSjb1osE_c3J_F2H.jpeg","image"]},{"id":8,"type":"Runware Frame Images","pos":[123.6376953125,654.8604125976562],"size":[270,190],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"image1","name":"image1","shape":7,"type":"IMAGE","link":5},{"localized_name":"image2","name":"image2","shape":7,"type":"IMAGE","link":null},{"localized_name":"image3","name":"image3","shape":7,"type":"IMAGE","link":null},{"localized_name":"image4","name":"image4","shape":7,"type":"IMAGE","link":null},{"localized_name":"frame1_position","name":"frame1_position","shape":7,"type":"COMBO","widget":{"name":"frame1_position"},"link":null},{"localized_name":"frame2_position","name":"frame2_position","shape":7,"type":"COMBO","widget":{"name":"frame2_position"},"link":null},{"localized_name":"frame3_position","name":"frame3_position","shape":7,"type":"COMBO","widget":{"name":"frame3_position"},"link":null},{"localized_name":"frame4_position","name":"frame4_position","shape":7,"type":"COMBO","widget":{"name":"frame4_position"},"link":null}],"outputs":[{"localized_name":"Frame Images","name":"Frame Images","type":"RUNWAREFRAMEIMAGES","links":[43]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"655118a9a1cddbf1a3de1d9d7dfa51311df49fe5","Node name for S&R":"Runware Frame Images"},"widgets_values":["first","auto","auto","auto"],"bgcolor":"#5345bf"},{"id":30,"type":"Runware Video Model Search","pos":[102.09335327148438,321.43450927734375],"size":[287.380859375,250],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"Model Search","name":"Model Search","type":"STRING","widget":{"name":"Model Search"},"link":null},{"localized_name":"Model Architecture","name":"Model Architecture","type":"COMBO","widget":{"name":"Model Architecture"},"link":null},{"localized_name":"VideoList","name":"VideoList","type":"COMBO","widget":{"name":"VideoList"},"link":null},{"localized_name":"Use Search Value","name":"Use Search Value","type":"BOOLEAN","widget":{"name":"Use Search Value"},"link":null},{"localized_name":"useCustomDimensions","name":"useCustomDimensions","type":"COMBO","widget":{"name":"useCustomDimensions"},"link":null},{"localized_name":"Width","name":"Width","type":"INT","widget":{"name":"Width"},"link":null},{"localized_name":"Height","name":"Height","type":"INT","widget":{"name":"Height"},"link":null},{"localized_name":"useResolution","name":"useResolution","type":"BOOLEAN","widget":{"name":"useResolution"},"link":null},{"localized_name":"resolution","name":"resolution","type":"COMBO","widget":{"name":"resolution"},"link":null}],"outputs":[{"localized_name":"Runware Video Model","name":"Runware Video Model","type":"RUNWAREVIDEOMODEL","links":[44]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"461b5aac08e46fb84318f4f8ceb86fa2c7f6e1a1","Node name for S&R":"Runware Video Model Search"},"widgets_values":["","PixVerse","pixverse:1@7 (PixVerse v5.6)",false,"Model Default",640,360,false,"1080p"],"bgcolor":"#5345bf"},{"id":26,"type":"Runware Video Inference","pos":[441.8916015625,314.3313293457031],"size":[400,760],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"Model","name":"Model","type":"RUNWAREVIDEOMODEL","link":44},{"localized_name":"frameImages","name":"frameImages","shape":7,"type":"RUNWAREFRAMEIMAGES","link":43},{"localized_name":"referenceImages","name":"referenceImages","shape":7,"type":"RUNWAREREFERENCEIMAGES","link":null},{"localized_name":"providerSettings","name":"providerSettings","shape":7,"type":"RUNWAREPROVIDERSETTINGS","link":null},{"localized_name":"inputAudios","name":"inputAudios","shape":7,"type":"RUNWAREINPUTAUDIOS","link":null},{"localized_name":"referenceVideos","name":"referenceVideos","shape":7,"type":"RUNWAREREFERENCEVIDEOS","link":null},{"localized_name":"inputs","name":"inputs","shape":7,"type":"RUNWAREVIDEOINFERENCEINPUTS","link":null},{"localized_name":"safetyInputs","name":"safetyInputs","shape":7,"type":"RUNWARESAFETYINPUTS","link":null},{"localized_name":"videoAdvancedFeatureInputs","name":"videoAdvancedFeatureInputs","shape":7,"type":"RUNWAREVIDEOADVANCEDFEATUREINPUTS","link":null},{"localized_name":"Accelerator","name":"Accelerator","shape":7,"type":"RUNWAREACCELERATOR","link":null},{"localized_name":"Multi Inference Mode","name":"Multi Inference Mode","type":"BOOLEAN","widget":{"name":"Multi Inference Mode"},"link":null},{"localized_name":"Prompt Weighting","name":"Prompt Weighting","type":"COMBO","widget":{"name":"Prompt Weighting"},"link":null},{"localized_name":"useDuration","name":"useDuration","type":"BOOLEAN","widget":{"name":"useDuration"},"link":null},{"localized_name":"duration","name":"duration","type":"INT","widget":{"name":"duration"},"link":null},{"localized_name":"useFps","name":"useFps","type":"BOOLEAN","widget":{"name":"useFps"},"link":null},{"localized_name":"fps","name":"fps","type":"INT","widget":{"name":"fps"},"link":null},{"localized_name":"outputFormat","name":"outputFormat","type":"COMBO","widget":{"name":"outputFormat"},"link":null},{"localized_name":"useSeed","name":"useSeed","type":"BOOLEAN","widget":{"name":"useSeed"},"link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"useSteps","name":"useSteps","type":"BOOLEAN","widget":{"name":"useSteps"},"link":null},{"localized_name":"steps","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"useBatchSize","name":"useBatchSize","type":"BOOLEAN","widget":{"name":"useBatchSize"},"link":null},{"localized_name":"batchSize","name":"batchSize","type":"INT","widget":{"name":"batchSize"},"link":null},{"localized_name":"acceleration","name":"acceleration","type":"COMBO","widget":{"name":"acceleration"},"link":null},{"localized_name":"positivePrompt","name":"positivePrompt","shape":7,"type":"STRING","widget":{"name":"positivePrompt"},"link":null},{"localized_name":"negativePrompt","name":"negativePrompt","shape":7,"type":"STRING","widget":{"name":"negativePrompt"},"link":null},{"localized_name":"speechVoice","name":"speechVoice","shape":7,"type":"STRING","widget":{"name":"speechVoice"},"link":null},{"localized_name":"speechText","name":"speechText","shape":7,"type":"STRING","widget":{"name":"speechText"},"link":null}],"outputs":[{"localized_name":"VIDEO","name":"VIDEO","type":"VIDEO","links":[41]},{"localized_name":"RW-Task","name":"RW-Task","type":"RUNWARETASK","links":null},{"localized_name":"OUTPUT","name":"OUTPUT","type":"RUNWARETASK","links":null}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"bdc86e015971881247addb07bb8f22f7d85c0062","Node name for S&R":"Runware Video Inference"},"widgets_values":[false,"Disabled",true,5,false,24,"mp4",true,1904306247,"randomize",false,20,true,1,"none","A chef cooking eggs","","",""],"bgcolor":"#5345bf"}],"links":[[5,9,0,8,0,"IMAGE"],[41,26,0,1,0,"VIDEO"],[43,8,0,26,1,"RUNWAREFRAMEIMAGES"],[44,30,0,26,0,"RUNWAREVIDEOMODEL"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.6830134553650705,"offset":[565.0372657527382,-50.90481474947803]}},"version":0.4} \ No newline at end of file From ff6b7d8ad4471836cfbf1637e1ee1e24fe20bc48 Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Thu, 29 Jan 2026 21:51:35 -0500 Subject: [PATCH 6/9] Add 3D infrence support --- __init__.py | 6 ++ clientlibs/types.js | 15 +++ modules/save3D.py | 109 +++++++++++++++++++ modules/threeDInference.py | 178 +++++++++++++++++++++++++++++++ modules/threeDInferenceInputs.py | 49 +++++++++ 5 files changed, 357 insertions(+) create mode 100644 modules/save3D.py create mode 100644 modules/threeDInference.py create mode 100644 modules/threeDInferenceInputs.py 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/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/modules/save3D.py b/modules/save3D.py new file mode 100644 index 0000000..1269fbb --- /dev/null +++ b/modules/save3D.py @@ -0,0 +1,109 @@ +from .utils import runwareUtils as rwUtils +import folder_paths +import os +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." + }), + } + } + + RETURN_TYPES = () + 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() + + # Validate extension + if extension not in ["glb"]: + extension = "glb" # Default to GLB if unknown + + # Generate filename with timestamp + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"{filename_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}") + + # Return UI info for ComfyUI + return {"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..f6c396a --- /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 e + + # 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", +} From 11fc63d4f663bbb1dc87d65eadd338b814de45d3 Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Thu, 29 Jan 2026 21:57:48 -0500 Subject: [PATCH 7/9] add workflow --- clientlibs/main.js | 12 ++++++++- clientlibs/utils.js | 25 +++++++++++++++++++ modules/save3D.py | 12 ++++++++- modules/utils/runwareUtils.py | 12 +++++++++ .../Runware_3D_Inference_meta_sam_3d.json | 1 + 5 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 workflows/Runware_3D_Inference_meta_sam_3d.json 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/utils.js b/clientlibs/utils.js index 3c08c24..2f3d6ab 100644 --- a/clientlibs/utils.js +++ b/clientlibs/utils.js @@ -70,6 +70,30 @@ function mediaUUIDHandler(msgEvent) { return false; } +function save3DFilepathHandler(msgEvent) { + const data = msgEvent.detail; + const filepath = data.filepath; + const nodeID = parseInt(data.nodeID); + + 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 +3217,7 @@ export { syncDimensionsNodeHandler, searchNodeHandler, mediaUUIDHandler, + save3DFilepathHandler, captionNodeHandler, videoTranscriptionHandler, videoOutputsHandler, diff --git a/modules/save3D.py b/modules/save3D.py index 1269fbb..dc59f9c 100644 --- a/modules/save3D.py +++ b/modules/save3D.py @@ -19,7 +19,14 @@ class RunwareSave3D: "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 = () @@ -83,6 +90,9 @@ class RunwareSave3D: 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 UI info for ComfyUI return {"ui": {"text": [f"Saved: {filename}"]}} 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..65944a0 --- /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":7,"last_link_id":4,"nodes":[{"id":2,"type":"Runware 3D Inference Inputs","pos":[-111.40933990478516,526.4013671875],"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":4,"type":"LoadImage","pos":[75.4135513305664,689.6184692382812],"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":7,"type":"Runware Save 3D","pos":[680.3914794921875,391.1221618652344],"size":[270,106],"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":[],"properties":{"cnr_id":"ComfyUI-Runware","ver":"ff6b7d8ad4471836cfbf1637e1ee1e24fe20bc48","Node name for S&R":"Runware Save 3D"},"widgets_values":["","ComfyUI",""],"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",1562538027,"randomize","GLB"],"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 From 6f128a1e9dd1ed5c60c526da1833f340fee5782f Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Fri, 30 Jan 2026 10:25:13 -0500 Subject: [PATCH 8/9] fix output --- modules/save3D.py | 12 ++++++------ workflows/Runware_3D_Inference_meta_sam_3d.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/save3D.py b/modules/save3D.py index dc59f9c..8598ff9 100644 --- a/modules/save3D.py +++ b/modules/save3D.py @@ -29,7 +29,8 @@ class RunwareSave3D: "hidden": {"node_id": "UNIQUE_ID"}, } - RETURN_TYPES = () + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("filepath",) FUNCTION = "save_3d" OUTPUT_NODE = True CATEGORY = "Runware" @@ -54,9 +55,8 @@ class RunwareSave3D: url_without_params = file_url.split('?')[0] extension = url_without_params.split('.')[-1].lower() - # Validate extension - if extension not in ["glb"]: - extension = "glb" # Default to GLB if unknown + if extension not in ["glb", "ply"]: + raise Exception(f"Unsupported 3D file extension '{extension}' from URL. Expected .glb or .ply.") # Generate filename with timestamp timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") @@ -93,8 +93,8 @@ class RunwareSave3D: # Send filepath to UI (like media upload mediaUUID) rwUtils.sendSave3DFilepath(filepath, kwargs.get("node_id")) - # Return UI info for ComfyUI - return {"ui": {"text": [f"Saved: {filename}"]}} + # Return filepath as output and UI info for ComfyUI + return (filepath, {"ui": {"text": [f"Saved: {filename}"]}}) except requests.exceptions.RequestException as e: print(f"[Runware Save 3D] Attempt {attempt + 1} failed: {e}") diff --git a/workflows/Runware_3D_Inference_meta_sam_3d.json b/workflows/Runware_3D_Inference_meta_sam_3d.json index 65944a0..dbd0813 100644 --- a/workflows/Runware_3D_Inference_meta_sam_3d.json +++ b/workflows/Runware_3D_Inference_meta_sam_3d.json @@ -1 +1 @@ -{"id":"4f919008-d74f-4dcb-baaa-c9f3e0cfce69","revision":0,"last_node_id":7,"last_link_id":4,"nodes":[{"id":2,"type":"Runware 3D Inference Inputs","pos":[-111.40933990478516,526.4013671875],"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":4,"type":"LoadImage","pos":[75.4135513305664,689.6184692382812],"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":7,"type":"Runware Save 3D","pos":[680.3914794921875,391.1221618652344],"size":[270,106],"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":[],"properties":{"cnr_id":"ComfyUI-Runware","ver":"ff6b7d8ad4471836cfbf1637e1ee1e24fe20bc48","Node name for S&R":"Runware Save 3D"},"widgets_values":["","ComfyUI",""],"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",1562538027,"randomize","GLB"],"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 +{"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 From 8182dc8bed37ba26cd6ce08a28545bfee2a387dc Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Mon, 2 Feb 2026 11:02:38 -0500 Subject: [PATCH 9/9] fixes --- clientlibs/utils.js | 5 +++-- modules/save3D.py | 12 +++++++++--- modules/threeDInference.py | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/clientlibs/utils.js b/clientlibs/utils.js index 2f3d6ab..af10901 100644 --- a/clientlibs/utils.js +++ b/clientlibs/utils.js @@ -73,8 +73,9 @@ function mediaUUIDHandler(msgEvent) { function save3DFilepathHandler(msgEvent) { const data = msgEvent.detail; const filepath = data.filepath; - const nodeID = parseInt(data.nodeID); - + 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) { diff --git a/modules/save3D.py b/modules/save3D.py index 8598ff9..a75ead3 100644 --- a/modules/save3D.py +++ b/modules/save3D.py @@ -1,6 +1,7 @@ from .utils import runwareUtils as rwUtils import folder_paths import os +import re import requests from datetime import datetime @@ -58,9 +59,15 @@ class RunwareSave3D: 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"{filename_prefix}_{timestamp}.{extension}" + filename = f"{safe_prefix}_{timestamp}.{extension}" filepath = os.path.join(threed_output_dir, filename) # Download with retry logic @@ -93,8 +100,7 @@ class RunwareSave3D: # Send filepath to UI (like media upload mediaUUID) rwUtils.sendSave3DFilepath(filepath, kwargs.get("node_id")) - # Return filepath as output and UI info for ComfyUI - return (filepath, {"ui": {"text": [f"Saved: {filename}"]}}) + return {"result": (filepath,), "ui": {"text": (f"Saved: {filename}",)}} except requests.exceptions.RequestException as e: print(f"[Runware Save 3D] Attempt {attempt + 1} failed: {e}") diff --git a/modules/threeDInference.py b/modules/threeDInference.py index f6c396a..f90fddc 100644 --- a/modules/threeDInference.py +++ b/modules/threeDInference.py @@ -97,7 +97,7 @@ class threeDInference: print(f"[DEBUG] Response: {rwUtils.safe_json_dumps(genResult, indent=2)}") except Exception as e: - raise e + raise # Extract task UUID for polling taskUUID = genConfig[0]["taskUUID"]