Merge branch 'main' into fix-tiffNode/feature-outputNode

This commit is contained in:
Sirshendu Ganguly
2026-01-29 15:23:43 -05:00
committed by GitHub
15 changed files with 212 additions and 21 deletions
+2
View File
@@ -72,6 +72,7 @@ from .modules.settings import RunwareSettings
from .modules.videoAdvancedFeatureInputs import videoAdvancedFeatureInputs
from .modules.advancedFeatureSettings.wanAnimateAdvancedFeatureSettings import RunwareWanAnimateAdvancedFeatureSettings
from .modules.saveImage import RunwareSaveImage
from .modules.videoOutputs import RunwareVideoInferenceOutputs
RUNWARE_COMFYUI_VERSION = "1.4.0 Beta"
@@ -158,6 +159,7 @@ NODE_CLASS_MAPPINGS = {
"Runware Video Advanced Feature Inputs": videoAdvancedFeatureInputs,
"Runware Wan Animate Advanced Feature Settings": RunwareWanAnimateAdvancedFeatureSettings,
"Runware Save Image": RunwareSaveImage,
"Runware Video Inference Outputs": RunwareVideoInferenceOutputs,
}
NODE_DISPLAY_NAME_MAPPINGS = {
+14 -1
View File
@@ -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;
+5
View File
@@ -122,6 +122,7 @@ const RUNWARE_NODE_TYPES = {
VIDEOADVANCEDFEATUREINPUTS: "Runware Video Advanced Feature Inputs",
WANANIMATEADVANCEDFEATURESETTINGS: "Runware Wan Animate Advanced Feature Settings",
SAVEIMAGE: "Runware Save Image",
VIDEOINFERENCEOUTPUTS: "Runware Video Inference Outputs",
};
const RUNWARE_NODE_PROPS = {
@@ -425,6 +426,10 @@ const RUNWARE_NODE_PROPS = {
bgColor: DEFAULT_BGCOLOR,
colorModeOnly: true,
},
[RUNWARE_NODE_TYPES.VIDEOINFERENCEOUTPUTS]: {
bgColor: DEFAULT_BGCOLOR,
colorModeOnly: true,
},
};
export {
+48 -1
View File
@@ -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) {
@@ -1715,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)",
@@ -1801,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},
@@ -1869,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",
@@ -3149,6 +3195,7 @@ export {
mediaUUIDHandler,
captionNodeHandler,
videoTranscriptionHandler,
videoOutputsHandler,
handleCustomErrors,
APIKeyHandler,
videoInferenceDimensionsHandler,
@@ -47,6 +47,14 @@ class RunwareBytedanceProviderSettings:
"tooltip": "Enable audio generation support when available.",
"default": False,
}),
"useDraft": ("BOOLEAN", {
"tooltip": "Enable to include draft parameter in provider settings",
"default": False,
}),
"draft": ("BOOLEAN", {
"tooltip": "Enable draft mode for Bytedance provider",
"default": False,
}),
}
}
@@ -63,12 +71,14 @@ class RunwareBytedanceProviderSettings:
useMaxSequentialImages = kwargs.get("useMaxSequentialImages", False)
useFastMode = kwargs.get("useFastMode", False)
useAudio = kwargs.get("useAudio", False)
useDraft = kwargs.get("useDraft", False)
# Get value parameters
cameraFixed = kwargs.get("cameraFixed", False)
maxSequentialImages = kwargs.get("maxSequentialImages", 1)
fastMode = kwargs.get("fastMode", False)
audio = kwargs.get("audio", False)
draft = kwargs.get("draft", False)
# Build settings dictionary - only include what is enabled
settings = {}
@@ -88,6 +98,10 @@ class RunwareBytedanceProviderSettings:
# Add audio only if useAudio is enabled
if useAudio:
settings["audio"] = audio
# Add draft only if useDraft is enabled
if useDraft:
settings["draft"] = draft
# Clean up None values
settings = {k: v for k, v in settings.items() if v is not None}
+13
View File
@@ -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")
+2 -1
View File
@@ -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()
+6 -4
View File
@@ -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
+18
View File
@@ -43,6 +43,16 @@ class videoInferenceInputs:
"Reference Voices": ("RUNWAREREFERENCEVOICES", {
"tooltip": "Connect the Reference Voices node to provide reference voice URLs or mediaUUIDs."
}),
"draftId": ("STRING", {
"default": "",
"multiline": False,
"tooltip": "Draft ID from a previous Bytedance video generation. Used when continuing or extending a draft output",
}),
"videoId": ("STRING", {
"default": "",
"multiline": False,
"tooltip": "Video ID from a previous generation. Used when extending or editing an existing OpenAI Sora video",
}),
}
}
@@ -65,6 +75,8 @@ class videoInferenceInputs:
references = kwargs.get("References", None)
referenceVideos = kwargs.get("Reference Videos", None)
referenceVoices = kwargs.get("Reference Voices", None)
draftId = kwargs.get("draftId", "")
videoId = kwargs.get("videoId", "")
inputs = {}
@@ -112,4 +124,10 @@ class videoInferenceInputs:
if referenceVoices is not None and len(referenceVoices) > 0:
inputs["referenceVoices"] = referenceVoices
if draftId is not None and draftId.strip() != "":
inputs["draftId"] = draftId.strip()
if videoId is not None and videoId.strip() != "":
inputs["videoId"] = videoId.strip()
return (inputs,)
+10 -13
View File
@@ -47,6 +47,7 @@ class videoModelSearch:
"pixverse:1@3 (PixVerse v4.5)",
"pixverse:1@5-fast (PixVerse v5 Fast)",
"pixverse:1@6 (PixVerse v5.5)",
"pixverse:1@7 (PixVerse v5.6)",
"pixverse:lipsync@1 (PixVerse LipSync)",
],
"Vidu": [
@@ -148,6 +149,7 @@ class videoModelSearch:
"pixverse:1@3": {"width": 640, "height": 360},
"pixverse:1@5-fast": {"width": 640, "height": 360},
"pixverse:1@6": {"width": 640, "height": 360},
"pixverse:1@7": {"width": 640, "height": 360},
"pixverse:lipsync@1": {"width": 640, "height": 360},
# Vidu Models
@@ -248,6 +250,7 @@ class videoModelSearch:
"pixverse:1@3": "360p",
"pixverse:1@5-fast": "360p",
"pixverse:1@6": "360p",
"pixverse:1@7": "360p",
"pixverse:lipsync@1": "360p",
# Vidu Models
@@ -390,8 +393,8 @@ class videoModelSearch:
DESCRIPTION = "Directly Search and Connect Video Models to Runware Video Inference Nodes In ComfyUI."
FUNCTION = "videoModelSearch"
RETURN_TYPES = ("RUNWAREVIDEOMODEL", "INT", "INT")
RETURN_NAMES = ("Runware Video Model", "Width", "Height")
RETURN_TYPES = ("RUNWAREVIDEOMODEL",)
RETURN_NAMES = ("Runware Video Model",)
CATEGORY = "Runware"
@classmethod
@@ -421,33 +424,27 @@ class videoModelSearch:
# Use custom dimensions from Width/Height widgets
width = customWidth
height = customHeight
returnWidth = width
returnHeight = height
elif useCustomDimensions == "Disabled":
# Don't pass width/height (use None in dict, but return 0 for INT output)
# Don't pass width/height (use None in dict)
width = None
height = None
returnWidth = 0
returnHeight = 0
else: # "Model Default"
# Use model default dimensions
width = dimensions["width"]
height = dimensions["height"]
returnWidth = width
returnHeight = height
resultDict = {
"model": modelAirCode,
"useCustomDimensions": useCustomDimensions,
"width": width,
"height": height,
}
# Add resolution if enabled and model supports it
if useResolution and modelResolution is not None:
resultDict["resolution"] = resolution
resultDict["useResolution"] = True
else:
resultDict["useResolution"] = False
return (resultDict, returnWidth, returnHeight)
return (resultDict,)
+75
View File
@@ -0,0 +1,75 @@
from .utils import runwareUtils as rwUtils
class RunwareVideoInferenceOutputs:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"Output": (
"RUNWARETASK",
{
"tooltip": "Connect the OUTPUT from Runware Video Inference.",
},
),
},
"optional": {
"draftId": ("STRING", {
"placeholder": "Draft ID will appear here after generation.",
"tooltip": "This field will be automatically populated with the draft ID from the video inference output.",
}),
"videoId": ("STRING", {
"placeholder": "Video ID will appear here after generation.",
"tooltip": "This field will be automatically populated with the video ID from the video inference output.",
}),
},
"hidden": {"node_id": "UNIQUE_ID"},
}
DESCRIPTION = "Extracts Runware video inference outputs such as draftId and videoId."
FUNCTION = "get_outputs"
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("draftID", "videoID")
CATEGORY = "Runware"
OUTPUT_NODE = True # Ensure node executes even if outputs aren't used
def get_outputs(self, Output, draftId=None, videoId=None, **kwargs):
draft_id = ""
video_id = ""
try:
data_list = None
# Handle different possible response shapes defensively
if isinstance(Output, dict):
if "data" in Output:
data_list = Output.get("data", [])
elif "response" in Output and isinstance(Output["response"], dict):
data_list = Output["response"].get("data", [])
if data_list and len(data_list) > 0:
first = data_list[0]
outputs = first.get("outputs") or {}
if isinstance(outputs, dict):
# draftId is used by some providers
draft_id = outputs.get("draftId", "") or outputs.get("draftID", "")
# videoId / videoID used by other providers
video_id = outputs.get("videoId", "") or outputs.get("videoID", "")
# Send to frontend so values appear in textboxes (like Media Upload)
rwUtils.sendVideoOutputs(draft_id, video_id, kwargs.get("node_id"))
except Exception as e:
print(f"[Runware Video Outputs] Failed to parse outputs: {e}")
return (draft_id, video_id)
NODE_CLASS_MAPPINGS = {
"RunwareVideoInferenceOutputs": RunwareVideoInferenceOutputs,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"RunwareVideoInferenceOutputs": "Runware Video Inference Outputs",
}
+2 -1
View File
@@ -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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long