Merge pull request #83 from Runware/feature-videoOutput
Adding Runware Output Node
This commit is contained in:
@@ -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 = {
|
||||
|
||||
+14
-1
@@ -1,6 +1,6 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
import { promptEnhanceHandler, syncDimensionsNodeHandler, searchNodeHandler, APIKeyHandler, captionNodeHandler, mediaUUIDHandler, videoTranscriptionHandler, handleCustomErrors, videoInferenceDimensionsHandler, videoModelSearchFilterHandler, audioModelSearchFilterHandler, useParameterToggleHandler, imageInferenceToggleHandler, upscalerToggleHandler, videoUpscalerToggleHandler, audioInferenceToggleHandler, acceleratorOptionsToggleHandler, bytedanceProviderSettingsToggleHandler, openaiProviderSettingsToggleHandler, lightricksProviderSettingsToggleHandler, klingProviderSettingsToggleHandler, lumaProviderSettingsToggleHandler, briaProviderSettingsToggleHandler, pixverseProviderSettingsToggleHandler, alibabaProviderSettingsToggleHandler, mireloProviderSettingsToggleHandler, googleProviderSettingsToggleHandler, syncProviderSettingsToggleHandler, syncSegmentToggleHandler, settingsToggleHandler, audioInputToggleHandler, speechInputToggleHandler, briaProviderMaskToggleHandler, wanAnimateAdvancedFeatureSettingsToggleHandler, videoAdvancedFeatureInputsToggleHandler, audioInferenceInputsToggleHandler } from "./utils.js";
|
||||
import { promptEnhanceHandler, syncDimensionsNodeHandler, searchNodeHandler, APIKeyHandler, captionNodeHandler, mediaUUIDHandler, 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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -125,7 +125,8 @@ class videoBgRemoval:
|
||||
if status == "success":
|
||||
if "videoURL" in videoData or "videoBase64Data" in videoData:
|
||||
videos = rwUtils.convertVideoB64List(pollResult, 1920, 1080)
|
||||
return videos
|
||||
# SaveVideo expects a single video object, not a tuple
|
||||
return (videos[0],) if len(videos) > 0 else (None,)
|
||||
|
||||
comfy.model_management.throw_exception_if_processing_interrupted()
|
||||
|
||||
|
||||
@@ -141,8 +141,8 @@ class txt2vid:
|
||||
|
||||
DESCRIPTION = "Generates Videos Lightning Fast With Runware Video Inference Engine."
|
||||
FUNCTION = "generateVideo"
|
||||
RETURN_TYPES = ("VIDEO", "RUNWARETASK")
|
||||
RETURN_NAMES = ("VIDEO", "RW-Task")
|
||||
RETURN_TYPES = ("VIDEO", "RUNWARETASK", "RUNWARETASK")
|
||||
RETURN_NAMES = ("VIDEO", "RW-Task", "OUTPUT")
|
||||
CATEGORY = "Runware"
|
||||
|
||||
def generateVideo(self, **kwargs):
|
||||
@@ -325,7 +325,7 @@ class txt2vid:
|
||||
genConfig[0]["acceleration"] = acceleration
|
||||
|
||||
if (multiInferenceMode):
|
||||
return (None, genConfig)
|
||||
return (None, genConfig, None)
|
||||
else:
|
||||
try:
|
||||
# Debug: Print the request being sent
|
||||
@@ -388,7 +388,9 @@ class txt2vid:
|
||||
if status == "success":
|
||||
if "videoURL" in video_data or "videoBase64Data" in video_data:
|
||||
videos = rwUtils.convertVideoB64List(pollResult, width, height)
|
||||
return videos
|
||||
# SaveVideo expects a single video object, not a tuple
|
||||
video_output = videos[0] if len(videos) > 0 else None
|
||||
return (video_output, genConfig, pollResult)
|
||||
|
||||
# If status is "processing", continue polling
|
||||
|
||||
|
||||
@@ -43,6 +43,16 @@ class videoInferenceInputs:
|
||||
"Reference Voices": ("RUNWAREREFERENCEVOICES", {
|
||||
"tooltip": "Connect the Reference Voices node to provide reference voice URLs or mediaUUIDs."
|
||||
}),
|
||||
"draftId": ("STRING", {
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"tooltip": "Draft ID from a previous Bytedance video generation. Used when continuing or extending a draft output",
|
||||
}),
|
||||
"videoId": ("STRING", {
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"tooltip": "Video ID from a previous generation. Used when extending or editing an existing OpenAI Sora video",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +75,8 @@ class videoInferenceInputs:
|
||||
references = kwargs.get("References", None)
|
||||
referenceVideos = kwargs.get("Reference Videos", None)
|
||||
referenceVoices = kwargs.get("Reference Voices", None)
|
||||
draftId = kwargs.get("draftId", "")
|
||||
videoId = kwargs.get("videoId", "")
|
||||
|
||||
inputs = {}
|
||||
|
||||
@@ -112,4 +124,10 @@ class videoInferenceInputs:
|
||||
if referenceVoices is not None and len(referenceVoices) > 0:
|
||||
inputs["referenceVoices"] = referenceVoices
|
||||
|
||||
if draftId is not None and draftId.strip() != "":
|
||||
inputs["draftId"] = draftId.strip()
|
||||
|
||||
if videoId is not None and videoId.strip() != "":
|
||||
inputs["videoId"] = videoId.strip()
|
||||
|
||||
return (inputs,)
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user