Merge main into fix-tiffNode/feature-outputNode

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Sirshendu Ganguly
2026-02-02 13:37:05 -05:00
co-authored by Cursor
19 changed files with 634 additions and 21 deletions
+8
View File
@@ -72,6 +72,10 @@ from .modules.settings import RunwareSettings
from .modules.videoAdvancedFeatureInputs import videoAdvancedFeatureInputs
from .modules.advancedFeatureSettings.wanAnimateAdvancedFeatureSettings import RunwareWanAnimateAdvancedFeatureSettings
from .modules.saveImage import RunwareSaveImage
from .modules.videoOutputs import RunwareVideoInferenceOutputs
from .modules.threeDInference import threeDInference
from .modules.threeDInferenceInputs import threeDInferenceInputs
from .modules.save3D import RunwareSave3D
RUNWARE_COMFYUI_VERSION = "1.4.0 Beta"
@@ -158,6 +162,10 @@ NODE_CLASS_MAPPINGS = {
"Runware Video Advanced Feature Inputs": videoAdvancedFeatureInputs,
"Runware Wan Animate Advanced Feature Settings": RunwareWanAnimateAdvancedFeatureSettings,
"Runware Save Image": RunwareSaveImage,
"Runware Video Inference Outputs": RunwareVideoInferenceOutputs,
"Runware 3D Inference": threeDInference,
"Runware 3D Inference Inputs": threeDInferenceInputs,
"Runware Save 3D": RunwareSave3D,
}
NODE_DISPLAY_NAME_MAPPINGS = {
+24 -1
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, save3DFilepathHandler, videoTranscriptionHandler, videoOutputsHandler, handleCustomErrors, videoInferenceDimensionsHandler, videoModelSearchFilterHandler, audioModelSearchFilterHandler, useParameterToggleHandler, imageInferenceToggleHandler, upscalerToggleHandler, videoUpscalerToggleHandler, audioInferenceToggleHandler, acceleratorOptionsToggleHandler, bytedanceProviderSettingsToggleHandler, openaiProviderSettingsToggleHandler, lightricksProviderSettingsToggleHandler, klingProviderSettingsToggleHandler, lumaProviderSettingsToggleHandler, briaProviderSettingsToggleHandler, pixverseProviderSettingsToggleHandler, alibabaProviderSettingsToggleHandler, mireloProviderSettingsToggleHandler, googleProviderSettingsToggleHandler, syncProviderSettingsToggleHandler, syncSegmentToggleHandler, settingsToggleHandler, audioInputToggleHandler, speechInputToggleHandler, briaProviderMaskToggleHandler, wanAnimateAdvancedFeatureSettingsToggleHandler, videoAdvancedFeatureInputsToggleHandler, audioInferenceInputsToggleHandler } from "./utils.js";
import { RUNWARE_NODE_TYPES, RUNWARE_NODE_PROPS, SEARCH_TERMS } from "./types.js";
const nodeInitList = [];
@@ -10,7 +10,9 @@ app.registerExtension({
api.addEventListener('runwareError', handleCustomErrors);
api.addEventListener('runwareImageCaption', captionNodeHandler);
api.addEventListener('runwareMediaUUID', mediaUUIDHandler);
api.addEventListener('runwareSave3DFilepath', save3DFilepathHandler);
api.addEventListener('runwareVideoTranscription', videoTranscriptionHandler);
api.addEventListener('runwareVideoOutputs', videoOutputsHandler);
},
async nodeCreated(node) {
@@ -39,6 +41,27 @@ app.registerExtension({
mediaUUIDWidget.inputEl.style.fontSize = "11px";
mediaUUIDWidget.inputEl.style.fontFamily = "monospace";
}
} else if(nodeClass === RUNWARE_NODE_TYPES.VIDEOINFERENCEOUTPUTS) {
// Style draftId and videoId widgets to show full IDs (like Media Upload)
const draftIdWidget = node.widgets.find(widget => widget.name === "draftId");
const videoIdWidget = node.widgets.find(widget => widget.name === "videoId");
for(const w of [draftIdWidget, videoIdWidget]) {
if(w && w.inputEl) {
w.inputEl.style.width = "100%";
w.inputEl.style.minWidth = "300px";
w.inputEl.style.fontSize = "11px";
w.inputEl.style.fontFamily = "monospace";
}
}
} else if(nodeClass === RUNWARE_NODE_TYPES.SAVE3D) {
// Style the filepath widget to show full path (like Media Upload mediaUUID)
const filepathWidget = node.widgets.find(widget => widget.name === "filepath");
if(filepathWidget && filepathWidget.inputEl) {
filepathWidget.inputEl.style.width = "100%";
filepathWidget.inputEl.style.minWidth = "300px";
filepathWidget.inputEl.style.fontSize = "11px";
filepathWidget.inputEl.style.fontFamily = "monospace";
}
}
const nodeWidgets = node.widgets;
+20
View File
@@ -122,6 +122,10 @@ const RUNWARE_NODE_TYPES = {
VIDEOADVANCEDFEATUREINPUTS: "Runware Video Advanced Feature Inputs",
WANANIMATEADVANCEDFEATURESETTINGS: "Runware Wan Animate Advanced Feature Settings",
SAVEIMAGE: "Runware Save Image",
VIDEOINFERENCEOUTPUTS: "Runware Video Inference Outputs",
THREEDINFERENCE: "Runware 3D Inference",
THREEDINFERENCEINPUTS: "Runware 3D Inference Inputs",
SAVE3D: "Runware Save 3D",
};
const RUNWARE_NODE_PROPS = {
@@ -425,6 +429,22 @@ const RUNWARE_NODE_PROPS = {
bgColor: DEFAULT_BGCOLOR,
colorModeOnly: true,
},
[RUNWARE_NODE_TYPES.VIDEOINFERENCEOUTPUTS]: {
bgColor: DEFAULT_BGCOLOR,
colorModeOnly: true,
},
[RUNWARE_NODE_TYPES.THREEDINFERENCE]: {
bgColor: DEFAULT_BGCOLOR,
promptEnhancer: true,
},
[RUNWARE_NODE_TYPES.THREEDINFERENCEINPUTS]: {
bgColor: DEFAULT_BGCOLOR,
colorModeOnly: true,
},
[RUNWARE_NODE_TYPES.SAVE3D]: {
bgColor: DEFAULT_BGCOLOR,
colorModeOnly: true,
},
};
export {
+74 -1
View File
@@ -70,6 +70,31 @@ function mediaUUIDHandler(msgEvent) {
return false;
}
function save3DFilepathHandler(msgEvent) {
const data = msgEvent.detail;
const filepath = data.filepath;
const nodeID = parseInt(data.nodeID, 10);
if (Number.isNaN(nodeID)) return false;
if(data.success && filepath) {
const node = app.graph.getNodeById(nodeID);
if(node !== null && node !== undefined) {
const filepathWidget = node.widgets.find(widget => widget.name === "filepath");
if(filepathWidget) {
if(filepathWidget.value !== undefined) {
filepathWidget.value = filepath;
}
if(filepathWidget.inputEl) {
filepathWidget.inputEl.value = filepath;
filepathWidget.inputEl.dispatchEvent(new Event('input', { bubbles: true }));
filepathWidget.inputEl.dispatchEvent(new Event('change', { bubbles: true }));
}
}
}
}
return false;
}
function captionNodeHandler(msgEvent) {
const captionData = msgEvent.detail;
const captionText = captionData.captionText;
@@ -106,6 +131,44 @@ function videoTranscriptionHandler(msgEvent) {
return false;
}
function videoOutputsHandler(msgEvent) {
const outputData = msgEvent.detail;
const draftId = outputData.draftId || "";
const videoId = outputData.videoId || "";
const nodeID = outputData.nodeID;
if(outputData.success && nodeID !== undefined && nodeID !== null) {
const nodeIdInt = typeof nodeID === 'string' ? parseInt(nodeID) : nodeID;
const outputNode = app.graph.getNodeById(nodeIdInt);
if(outputNode !== null && outputNode !== undefined) {
const draftIdWidget = outputNode.widgets.find(widget => widget.name === "draftId");
const videoIdWidget = outputNode.widgets.find(widget => widget.name === "videoId");
if(draftIdWidget) {
if(draftIdWidget.value !== undefined) draftIdWidget.value = draftId;
if(draftIdWidget.inputEl) {
draftIdWidget.inputEl.value = draftId;
draftIdWidget.inputEl.dispatchEvent(new Event('input', { bubbles: true }));
draftIdWidget.inputEl.dispatchEvent(new Event('change', { bubbles: true }));
}
}
if(videoIdWidget) {
if(videoIdWidget.value !== undefined) videoIdWidget.value = videoId;
if(videoIdWidget.inputEl) {
videoIdWidget.inputEl.value = videoId;
videoIdWidget.inputEl.dispatchEvent(new Event('input', { bubbles: true }));
videoIdWidget.inputEl.dispatchEvent(new Event('change', { bubbles: true }));
}
}
outputNode.setDirtyCanvas(true);
}
}
return false;
}
function notifyUser(message, type="info", title = "Runware", life = 4.5) {
app.extensionManager.toast.add({
severity: type, // 'info', 'success', 'warn', 'error' \\
@@ -1131,6 +1194,8 @@ function bytedanceProviderSettingsToggleHandler(bytedanceNode) {
const fastModeWidget = bytedanceNode.widgets.find(w => w.name === "fastMode");
const useAudioWidget = bytedanceNode.widgets.find(w => w.name === "useAudio");
const audioWidget = bytedanceNode.widgets.find(w => w.name === "audio");
const useDraftWidget = bytedanceNode.widgets.find(w => w.name === "useDraft");
const draftWidget = bytedanceNode.widgets.find(w => w.name === "draft");
// Helper function to toggle widget enabled state (exact same pattern)
function toggleWidgetState(useWidget, paramWidget, paramName) {
@@ -1189,6 +1254,10 @@ function bytedanceProviderSettingsToggleHandler(bytedanceNode) {
if (useAudioWidget && audioWidget) {
toggleWidgetState(useAudioWidget, audioWidget, "audio");
}
if (useDraftWidget && draftWidget) {
toggleWidgetState(useDraftWidget, draftWidget, "draft");
}
}
function openaiProviderSettingsToggleHandler(openaiNode) {
@@ -1715,7 +1784,7 @@ function videoModelSearchFilterHandler(videoModelSearchNode) {
],
"PixVerse": [
"pixverse:1@1 (PixVerse v3.5)", "pixverse:1@2 (PixVerse v4)",
"pixverse:1@3 (PixVerse v4.5)", "pixverse:1@5-fast (PixVerse v5 Fast)", "pixverse:1@6 (PixVerse v5.5)", "pixverse:lipsync@1 (PixVerse LipSync)",
"pixverse:1@3 (PixVerse v4.5)", "pixverse:1@5-fast (PixVerse v5 Fast)", "pixverse:1@6 (PixVerse v5.5)", "pixverse:1@7 (PixVerse v5.6)", "pixverse:lipsync@1 (PixVerse LipSync)",
],
"Vidu": [
"vidu:1@0 (Vidu Q1 Classic)", "vidu:1@1 (Vidu Q1)",
@@ -1801,6 +1870,7 @@ function videoModelSearchFilterHandler(videoModelSearchNode) {
"pixverse:1@2": {"width": 640, "height": 360},
"pixverse:1@3": {"width": 640, "height": 360},
"pixverse:1@6": {"width": 640, "height": 360},
"pixverse:1@7": {"width": 640, "height": 360},
"pixverse:lipsync@1": {"width": 640, "height": 360},
"vidu:1@0": {"width": 1920, "height": 1080},
"vidu:1@1": {"width": 1920, "height": 1080},
@@ -1869,6 +1939,7 @@ function videoModelSearchFilterHandler(videoModelSearchNode) {
"pixverse:1@2": "360p",
"pixverse:1@3": "360p",
"pixverse:1@6": "360p",
"pixverse:1@7": "360p",
"pixverse:lipsync@1": "360p",
"vidu:1@0": "1080p",
"vidu:1@1": "1080p",
@@ -3147,8 +3218,10 @@ export {
syncDimensionsNodeHandler,
searchNodeHandler,
mediaUUIDHandler,
save3DFilepathHandler,
captionNodeHandler,
videoTranscriptionHandler,
videoOutputsHandler,
handleCustomErrors,
APIKeyHandler,
videoInferenceDimensionsHandler,
@@ -47,6 +47,14 @@ class RunwareBytedanceProviderSettings:
"tooltip": "Enable audio generation support when available.",
"default": False,
}),
"useDraft": ("BOOLEAN", {
"tooltip": "Enable to include draft parameter in provider settings",
"default": False,
}),
"draft": ("BOOLEAN", {
"tooltip": "Enable draft mode for Bytedance provider",
"default": False,
}),
}
}
@@ -63,12 +71,14 @@ class RunwareBytedanceProviderSettings:
useMaxSequentialImages = kwargs.get("useMaxSequentialImages", False)
useFastMode = kwargs.get("useFastMode", False)
useAudio = kwargs.get("useAudio", False)
useDraft = kwargs.get("useDraft", False)
# Get value parameters
cameraFixed = kwargs.get("cameraFixed", False)
maxSequentialImages = kwargs.get("maxSequentialImages", 1)
fastMode = kwargs.get("fastMode", False)
audio = kwargs.get("audio", False)
draft = kwargs.get("draft", False)
# Build settings dictionary - only include what is enabled
settings = {}
@@ -88,6 +98,10 @@ class RunwareBytedanceProviderSettings:
# Add audio only if useAudio is enabled
if useAudio:
settings["audio"] = audio
# Add draft only if useDraft is enabled
if useDraft:
settings["draft"] = draft
# Clean up None values
settings = {k: v for k, v in settings.items() if v is not None}
+125
View File
@@ -0,0 +1,125 @@
from .utils import runwareUtils as rwUtils
import folder_paths
import os
import re
import requests
from datetime import datetime
class RunwareSave3D:
"""Runware Save 3D node for saving 3D model files from URL"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"3dObject": ("STRING", {
"tooltip": "3D file URL from Runware 3D Inference node."
}),
"filename_prefix": ("STRING", {
"default": "ComfyUI",
"tooltip": "Prefix for the filename."
}),
},
"optional": {
"filepath": ("STRING", {
"placeholder": "Saved file path will appear here after execution.",
"tooltip": "This field is automatically populated with the saved file path after save.",
}),
},
"hidden": {"node_id": "UNIQUE_ID"},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("filepath",)
FUNCTION = "save_3d"
OUTPUT_NODE = True
CATEGORY = "Runware"
DESCRIPTION = "Save 3D model files (GLB/PLY) from Runware 3D Inference to the output folder."
def save_3d(self, **kwargs):
"""Save 3D file from URL"""
file_url = kwargs.get("3dObject", "")
filename_prefix = kwargs.get("filename_prefix", "ComfyUI")
if not file_url or not file_url.strip():
raise Exception("No 3D URL provided. Please connect the '3dObject' output from Runware 3D Inference node.")
file_url = file_url.strip()
# Get output directory
output_dir = folder_paths.get_output_directory()
threed_output_dir = os.path.join(output_dir, "3d_models")
os.makedirs(threed_output_dir, exist_ok=True)
# Get extension from URL (handle query parameters)
url_without_params = file_url.split('?')[0]
extension = url_without_params.split('.')[-1].lower()
if extension not in ["glb", "ply"]:
raise Exception(f"Unsupported 3D file extension '{extension}' from URL. Expected .glb or .ply.")
# Sanitize filename_prefix to prevent path traversal or arbitrary subdir creation
prefix_basename = os.path.basename(
filename_prefix.replace(os.sep, "_").replace(os.altsep or os.sep, "_")
)
safe_prefix = re.sub(r"[^\w\-]", "", prefix_basename) or "ComfyUI"
# Generate filename with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{safe_prefix}_{timestamp}.{extension}"
filepath = os.path.join(threed_output_dir, filename)
# Download with retry logic
max_retries = 10
retry_delays = [2, 5, 10, 15, 20]
for attempt in range(max_retries):
try:
response = requests.get(file_url, stream=True, timeout=60)
print(f"[Runware Save 3D] Download attempt {attempt + 1}: {response.status_code}")
if response.status_code == 422:
print(f"[Runware Save 3D] Server still processing, waiting...")
rwUtils.time.sleep(retry_delays[min(attempt, len(retry_delays) - 1)])
continue
if response.status_code == 502:
print(f"[Runware Save 3D] Server error (502), retrying...")
rwUtils.time.sleep(retry_delays[min(attempt, len(retry_delays) - 1)])
continue
response.raise_for_status()
with open(filepath, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"[Runware Save 3D] Successfully saved to: {filepath}")
# Send filepath to UI (like media upload mediaUUID)
rwUtils.sendSave3DFilepath(filepath, kwargs.get("node_id"))
return {"result": (filepath,), "ui": {"text": (f"Saved: {filename}",)}}
except requests.exceptions.RequestException as e:
print(f"[Runware Save 3D] Attempt {attempt + 1} failed: {e}")
if attempt < max_retries - 1:
rwUtils.time.sleep(retry_delays[min(attempt, len(retry_delays) - 1)])
continue
else:
raise Exception(f"Failed to download 3D file after {max_retries} attempts: {e}")
except Exception as e:
print(f"[Runware Save 3D] Unexpected error: {e}")
raise Exception(f"Failed to save 3D file: {e}")
raise Exception("Failed to download 3D file: max retries exceeded")
NODE_CLASS_MAPPINGS = {
"RunwareSave3D": RunwareSave3D,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"RunwareSave3D": "Runware Save 3D",
}
+178
View File
@@ -0,0 +1,178 @@
from .utils import runwareUtils as rwUtils
import comfy.model_management
class threeDInference:
"""Runware 3D Inference node for generating 3D models from images"""
# Available 3D models
THREED_MODELS = {
"Meta SAM 3D": "meta:sam@3d",
}
# Output formats
OUTPUT_FORMATS = ["GLB", "PLY"]
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"Model": (list(cls.THREED_MODELS.keys()), {
"tooltip": "Select the 3D generation model to use.",
"default": "Meta SAM 3D",
}),
"positivePrompt": ("STRING", {
"multiline": True,
"placeholder": "Describe the 3D object you want to generate...",
"tooltip": "A text prompt describing the 3D object to generate."
}),
"seed": ("INT", {
"tooltip": "Seed for reproducibility. Use the same seed to get the same result.",
"default": 1,
"min": 1,
"max": 2147483647,
}),
"outputFormat": (cls.OUTPUT_FORMATS, {
"tooltip": "Output format for the 3D model file.",
"default": "GLB",
}),
},
"optional": {
"inputs": ("RUNWARE3DINFERENCEINPUTS", {
"tooltip": "Connect a Runware 3D Inference Inputs node to provide image and mask inputs.",
}),
},
}
DESCRIPTION = "Generate 3D models from images using Runware's 3D Inference API. Connect the output to 'Runware Save 3D' node to save the file."
FUNCTION = "generate3D"
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("3dObject",)
CATEGORY = "Runware"
def generate3D(self, **kwargs):
"""Generate 3D model from inputs"""
modelName = kwargs.get("Model", "Meta SAM 3D")
positivePrompt = kwargs.get("positivePrompt", "")
seed = kwargs.get("seed", 1)
outputFormat = kwargs.get("outputFormat", "GLB")
inputs = kwargs.get("inputs", None)
# Get model AIR code
model = self.THREED_MODELS.get(modelName, "meta:sam@3d")
# Build generation config
genConfig = [
{
"taskType": "3dInference",
"taskUUID": rwUtils.genRandUUID(),
"model": model,
"numberResults": 1,
"outputType": "URL",
"includeCost": True,
"deliveryMethod": "async",
"outputFormat": outputFormat,
}
]
# Add positivePrompt if provided
if positivePrompt and positivePrompt.strip() != "":
genConfig[0]["positivePrompt"] = positivePrompt.strip()
# Add seed
genConfig[0]["seed"] = seed
# Handle inputs from 3D Inference Inputs node
if inputs is not None and isinstance(inputs, dict) and len(inputs) > 0:
genConfig[0]["inputs"] = inputs
try:
print(f"[DEBUG] Sending 3D Inference Request:")
print(f"[DEBUG] Request Payload: {rwUtils.safe_json_dumps(genConfig, indent=2)}")
# Send initial request
genResult = rwUtils.inferenecRequest(genConfig)
print(f"[DEBUG] Received 3D Inference Response:")
print(f"[DEBUG] Response: {rwUtils.safe_json_dumps(genResult, indent=2)}")
except Exception as e:
raise
# Extract task UUID for polling
taskUUID = genConfig[0]["taskUUID"]
# Poll for 3D generation completion
while True:
# Check for interrupt before each poll
comfy.model_management.throw_exception_if_processing_interrupted()
# Poll for result
pollResult = rwUtils.pollVideoResult(taskUUID) # Reuse existing poll function
print(f"[Debugging] Poll result: {rwUtils.safe_json_dumps(pollResult, indent=2) if isinstance(pollResult, (dict, list)) else pollResult}")
# Check for errors
if pollResult and "errors" in pollResult and len(pollResult["errors"]) > 0:
error_info = pollResult["errors"][0]
error_message = error_info.get("message", "Unknown error")
if "responseContent" in error_info:
response_content = error_info["responseContent"]
if isinstance(response_content, str):
detailed_message = response_content
elif isinstance(response_content, dict):
detailed_message = response_content.get("message", str(response_content))
else:
detailed_message = str(response_content)
if detailed_message:
error_message = f"{error_message}\nProvider Error: {detailed_message}"
task_uuid = error_info.get("taskUUID", "unknown")
raise Exception(f"3D generation failed (Task: {task_uuid}): {error_message}")
# Check for successful completion
if pollResult and "data" in pollResult and len(pollResult["data"]) > 0:
data = pollResult["data"][0]
# Check if outputs.files exists (3D inference response format)
outputs = data.get("outputs", {})
files = outputs.get("files", [])
if len(files) > 0:
file_info = files[0]
file_url = file_info.get("url", "")
if file_url:
print(f"[3D Inference] Generated 3D file URL: {file_url}")
return (file_url,)
# Check status if available
if "status" in data:
status = data["status"]
if status == "success":
# Try alternative response format
if "outputs" in data and "files" in data["outputs"]:
files = data["outputs"]["files"]
if len(files) > 0:
file_url = files[0].get("url", "")
if file_url:
print(f"[3D Inference] Generated 3D file URL: {file_url}")
return (file_url,)
# Check for interrupt before waiting
comfy.model_management.throw_exception_if_processing_interrupted()
# Wait before next poll
for _ in range(10): # 10 x 0.1 second = 1 second total
comfy.model_management.throw_exception_if_processing_interrupted()
rwUtils.time.sleep(0.1)
NODE_CLASS_MAPPINGS = {
"Runware3DInference": threeDInference,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Runware3DInference": "Runware 3D Inference",
}
+49
View File
@@ -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",
}
+25
View File
@@ -295,6 +295,18 @@ def sendMediaUUID(mediaUUID, nodeID):
},
)
def sendSave3DFilepath(filepath, nodeID):
PromptServer.instance.send_sync(
"runwareSave3DFilepath",
{
"success": True,
"filepath": filepath,
"nodeID": nodeID,
"widgetName": "filepath",
},
)
def sendImageCaption(captionText, nodeID):
PromptServer.instance.send_sync(
"runwareImageCaption",
@@ -317,6 +329,19 @@ def sendVideoTranscription(transcriptionText, nodeID):
},
)
def sendVideoOutputs(draftId, videoId, nodeID):
"""Send draftId and videoId to frontend for display in Video Inference Outputs node textboxes."""
PromptServer.instance.send_sync(
"runwareVideoOutputs",
{
"success": True,
"draftId": draftId or "",
"videoId": videoId or "",
"nodeID": nodeID,
},
)
def inferenecRequest(genConfig):
global RUNWARE_API_KEY, RUNWARE_API_BASE_URL, SESSION_TIMEOUT
RUNWARE_API_KEY = os.getenv("RUNWARE_API_KEY")
+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
@@ -0,0 +1 @@
{"id":"4f919008-d74f-4dcb-baaa-c9f3e0cfce69","revision":0,"last_node_id":10,"last_link_id":6,"nodes":[{"id":4,"type":"LoadImage","pos":[-102.60188293457031,287.9990234375],"size":[274.080078125,314],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[3]},{"localized_name":"MASK","name":"MASK","type":"MASK","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.3.46","Node name for S&R":"LoadImage"},"widgets_values":["hat.png","image"]},{"id":2,"type":"Runware 3D Inference Inputs","pos":[-49.6855354309082,709.256591796875],"size":[222.00253295898438,46],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"image","name":"image","shape":7,"type":"IMAGE","link":3},{"localized_name":"mask","name":"mask","shape":7,"type":"IMAGE","link":null}],"outputs":[{"localized_name":"Inputs","name":"Inputs","type":"RUNWARE3DINFERENCEINPUTS","links":[2]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"938185cd42fdc0a25095c8e72ba42fdcef10913e","Node name for S&R":"Runware 3D Inference Inputs"},"widgets_values":[],"bgcolor":"#5345bf"},{"id":7,"type":"Runware Save 3D","pos":[680.3914794921875,391.1221618652344],"size":[234.08380126953125,123.42192840576172],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"3dObject","name":"3dObject","type":"STRING","widget":{"name":"3dObject"},"link":4},{"localized_name":"filename_prefix","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null},{"localized_name":"filepath","name":"filepath","shape":7,"type":"STRING","widget":{"name":"filepath"},"link":null}],"outputs":[{"localized_name":"filepath","name":"filepath","type":"STRING","links":[]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"ff6b7d8ad4471836cfbf1637e1ee1e24fe20bc48","Node name for S&R":"Runware Save 3D"},"widgets_values":["","ComfyUI","/Users/sirshenduganguly/Downloads/Runware-codebase/ComfyUI/output/3d_models/ComfyUI_20260130_101951.glb"],"bgcolor":"#5345bf"},{"id":1,"type":"Runware 3D Inference","pos":[215.56109619140625,360.20306396484375],"size":[400,200],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"inputs","name":"inputs","shape":7,"type":"RUNWARE3DINFERENCEINPUTS","link":2},{"localized_name":"Model","name":"Model","type":"COMBO","widget":{"name":"Model"},"link":null},{"localized_name":"positivePrompt","name":"positivePrompt","type":"STRING","widget":{"name":"positivePrompt"},"link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"outputFormat","name":"outputFormat","type":"COMBO","widget":{"name":"outputFormat"},"link":null}],"outputs":[{"localized_name":"3dObject","name":"3dObject","type":"STRING","links":[4]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"938185cd42fdc0a25095c8e72ba42fdcef10913e","Node name for S&R":"Runware 3D Inference"},"widgets_values":["Meta SAM 3D","a hat",1383646106,"randomize","PLY"],"bgcolor":"#5345bf"}],"links":[[2,2,0,1,0,"RUNWARE3DINFERENCEINPUTS"],[3,4,0,2,0,"IMAGE"],[4,1,0,7,0,"STRING"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.6830134553650705,"offset":[565.2445846003944,-50.69034697604053]}},"version":0.4}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long