From 534c6ece24b9843c104740428137dc12fd791d04 Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Wed, 28 Jan 2026 23:29:09 -0500 Subject: [PATCH] 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