Refactor: Use librosa instead of torchaudio, Add dedicated handlers for all use parameters and fix widget ordering

- Use librosa instead of torchaudio for mediaStorage
- Created dedicated toggle handlers for Image Inference, Video Inference, Upscaler, Audio Inference, Accelerator Options, Bytedance Provider Settings, and OpenAI Provider Settings
- Fixed Audio Inference widget ordering (use parameters now appear before their corresponding parameters)
- All handlers use the same proven pattern from videoInferenceDimensionsHandler
- Improved widget disabling/enabling functionality across all nodes
This commit is contained in:
Sirshendu Ganguly
2025-11-05 22:31:15 -05:00
parent b75f0803ad
commit 056ade0661
76 changed files with 2856 additions and 1606 deletions
+13 -11
View File
@@ -28,25 +28,26 @@ from .modules.imageInferenceInputs import imageInferenceInputs
from .modules.videoInference import txt2vid
from .modules.videoModelSearch import videoModelSearch
from .modules.frameImages import RunwareFrameImages
from .modules.providerSettings import RunwareProviderSettings
from .modules.pixverseProviderSettings import RunwarePixverseProviderSettings
from .modules.openaiProviderSettings import RunwareOpenAIProviderSettings
from .modules.bytedanceImageProviderSettings import RunwareBytedanceProviderSettings
from .modules.briaImageProviderSettings import RunwareBriaProviderSettings
from .modules.providerSettings.providerSettings import RunwareProviderSettings
from .modules.providerSettings.pixverseProviderSettings import RunwarePixverseProviderSettings
from .modules.providerSettings.openaiProviderSettings import RunwareOpenAIProviderSettings
from .modules.providerSettings.bytedanceImageProviderSettings import RunwareBytedanceProviderSettings
from .modules.providerSettings.briaImageProviderSettings import RunwareBriaProviderSettings
from .modules.audioInference import RunwareAudioInference
from .modules.audioModelSearch import RunwareAudioModelSearch
from .modules.audioSections import RunwareAudioSections
from .modules.elevenlabsProviderSettings import RunwareElevenLabsProviderSettings
from .modules.elevenlabsProviderSettingsSections import RunwareElevenLabsProviderSettingsSections
from .modules.providerSettings.elevenlabsProviderSettings import RunwareElevenLabsProviderSettings
from .modules.providerSettings.elevenlabsProviderSettingsSections import RunwareElevenLabsProviderSettingsSections
from .modules.acceleratorOptions import acceleratorOptions
from .modules.viduProviderSettings import RunwareViduProviderSettings
from .modules.providerSettings.viduProviderSettings import RunwareViduProviderSettings
from .modules.mediaUpload import runwareMediaUpload
from .modules.inputAudios import inputAudios
from .modules.referenceVideos import referenceVideos
from .modules.videoInferenceInputs import videoInferenceInputs
from .modules.lightricksProviderSettings import RunwareLightricksProviderSettings
from .modules.minimaxProviderSettings import RunwareMiniMaxProviderSettings
from .modules.klingProviderSettings import RunwareKlingProviderSettings
from .modules.providerSettings.lightricksProviderSettings import RunwareLightricksProviderSettings
from .modules.providerSettings.minimaxProviderSettings import RunwareMiniMaxProviderSettings
from .modules.providerSettings.klingProviderSettings import RunwareKlingProviderSettings
from .modules.providerSettings.runwayProviderSettings import RunwareRunwayProviderSettings
from .modules.vectorize import vectorize
from .modules.videoBgRemoval import videoBgRemoval
from .modules.videoUpscaler import videoUpscaler
@@ -114,6 +115,7 @@ NODE_CLASS_MAPPINGS = {
"Runware Lightricks Provider Settings": RunwareLightricksProviderSettings,
"Runware MiniMax Provider Settings": RunwareMiniMaxProviderSettings,
"Runware KlingAI Provider Settings": RunwareKlingProviderSettings,
"Runware Runway Provider Settings": RunwareRunwayProviderSettings,
"Runware Vectorize": vectorize,
"Runware Video Background Removal": videoBgRemoval,
"Runware Video Upscaler": videoUpscaler,
+22 -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 } from "./utils.js";
import { promptEnhanceHandler, syncDimensionsNodeHandler, searchNodeHandler, APIKeyHandler, captionNodeHandler, mediaUUIDHandler, videoTranscriptionHandler, handleCustomErrors, videoInferenceDimensionsHandler, videoModelSearchFilterHandler, useParameterToggleHandler, imageInferenceToggleHandler, upscalerToggleHandler, audioInferenceToggleHandler, acceleratorOptionsToggleHandler, bytedanceProviderSettingsToggleHandler, openaiProviderSettingsToggleHandler } from "./utils.js";
import { RUNWARE_NODE_TYPES, RUNWARE_NODE_PROPS, SEARCH_TERMS } from "./types.js";
const nodeInitList = [];
@@ -45,6 +45,27 @@ app.registerExtension({
const nodeWidgets = node.widgets;
if(nodeWidgets.length <= 0) return;
// Handle node-specific handlers first - each node has its own dedicated handler
if(nodeClass === RUNWARE_NODE_TYPES.IMAGEINFERENCE) {
imageInferenceToggleHandler(node);
} else if(nodeClass === RUNWARE_NODE_TYPES.VIDEOINFERENCE) {
videoInferenceDimensionsHandler(node);
} else if(nodeClass === RUNWARE_NODE_TYPES.UPSCALER) {
upscalerToggleHandler(node);
} else if(nodeClass === RUNWARE_NODE_TYPES.AUDIOINFERENCE) {
audioInferenceToggleHandler(node);
} else if(nodeClass === RUNWARE_NODE_TYPES.ACCELERATOROPTIONS) {
acceleratorOptionsToggleHandler(node);
} else if(nodeClass === RUNWARE_NODE_TYPES.BYTEDANCEPROVIDERSETTINGS) {
bytedanceProviderSettingsToggleHandler(node);
} else if(nodeClass === RUNWARE_NODE_TYPES.OPENAIPROVIDERSETTINGS) {
openaiProviderSettingsToggleHandler(node);
}
if(nodeClass === RUNWARE_NODE_TYPES.VIDEOMODELSEARCH) {
videoModelSearchFilterHandler(node);
}
for(const nodeWidget of nodeWidgets) {
const widgetName = nodeWidget.name;
const widgetType = nodeWidget.type;
+5
View File
@@ -95,6 +95,7 @@ const RUNWARE_NODE_TYPES = {
VIDEOINFERENCEINPUTS: "Runware Video Inference Inputs",
LIGHTRICKSPROVIDERSETTINGS: "Runware Lightricks Provider Settings",
KLINGPROVIDERSETTINGS: "Runware KlingAI Provider Settings",
RUNWAYPROVIDERSETTINGS: "Runware Runway Provider Settings",
VECTORIZE: "Runware Vectorize",
VIDEOBGREMOVAL: "Runware Video Background Removal",
VIDEOUPSCALER: "Runware Video Upscaler",
@@ -298,6 +299,10 @@ const RUNWARE_NODE_PROPS = {
bgColor: DEFAULT_BGCOLOR,
colorModeOnly: true,
},
[RUNWARE_NODE_TYPES.RUNWAYPROVIDERSETTINGS]: {
bgColor: DEFAULT_BGCOLOR,
colorModeOnly: true,
},
[RUNWARE_NODE_TYPES.VECTORIZE]: {
bgColor: DEFAULT_BGCOLOR,
},
+1691 -629
View File
File diff suppressed because it is too large Load Diff
+55 -62
View File
@@ -1,12 +1,11 @@
from .utils import runwareUtils as rwUtils
class acceleratorOptions:
"""Accelerator Options node for configuring caching options for faster generation"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {},
"optional": {
# FBCache parameters
"fbcache": ("BOOLEAN", {
"tooltip": "Enable Frame Buffer Cache for faster generation",
"default": None,
@@ -22,8 +21,6 @@ class acceleratorOptions:
"max": 1.0,
"step": 0.01,
}),
# TeaCache parameters
"teaCache": ("BOOLEAN", {
"tooltip": "Enable TeaCache for transformer-based models (Flux, SD3). Does not work with UNet models like SDXL or SD1.5. Particularly effective for iterative editing workflows.",
"default": None,
@@ -39,8 +36,6 @@ class acceleratorOptions:
"max": 1.0,
"step": 0.01,
}),
# DeepCache parameters
"deepCache": ("BOOLEAN", {
"tooltip": "Enable DeepCache for UNet-based models (SDXL, SD1.5). Not applicable to transformer models. Provides performance improvements for high-throughput scenarios.",
"default": None,
@@ -61,8 +56,6 @@ class acceleratorOptions:
"min": 0,
"max": 10,
}),
# Cache step control parameters (step numbers)
"useCacheSteps": ("BOOLEAN", {
"tooltip": "Enable to include cache step control parameters (step numbers) in API request.",
"default": False,
@@ -79,8 +72,6 @@ class acceleratorOptions:
"min": 1,
"max": 200,
}),
# Cache step control parameters (percentages)
"useCachePercentageSteps": ("BOOLEAN", {
"tooltip": "Enable to include cache step control parameters (percentages) in API request.",
"default": False,
@@ -117,63 +108,65 @@ class acceleratorOptions:
CATEGORY = "Runware"
def acceleratorOptions(self, **kwargs):
# Get all optional parameters
fbcache = kwargs.get("fbcache")
useCacheDistance = kwargs.get("useCacheDistance", False)
cacheDistance = kwargs.get("cacheDistance", 0.0)
teaCache = kwargs.get("teaCache")
useTeaCacheDistance = kwargs.get("useTeaCacheDistance", False)
teaCacheDistance = kwargs.get("teaCacheDistance", 0.0)
deepCache = kwargs.get("deepCache")
# Get toggle and value parameters
useDeepCacheOptions = kwargs.get("useDeepCacheOptions", False)
deepCacheInterval = kwargs.get("deepCacheInterval", 1)
deepCacheBranchId = kwargs.get("deepCacheBranchId", 0)
useCacheSteps = kwargs.get("useCacheSteps", False)
cacheStartStep = kwargs.get("cacheStartStep", 0)
cacheEndStep = kwargs.get("cacheEndStep", 1)
useCachePercentageSteps = kwargs.get("useCachePercentageSteps", False)
cacheStartStepPercentage = kwargs.get("cacheStartStepPercentage", 0)
cacheEndStepPercentage = kwargs.get("cacheEndStepPercentage", 1)
useCacheMaxConsecutiveSteps = kwargs.get("useCacheMaxConsecutiveSteps", False)
cacheMaxConsecutiveSteps = kwargs.get("cacheMaxConsecutiveSteps", 1)
"""Build accelerator options dictionary"""
params = self._extractParameters(kwargs)
acceleratorOptions = self._buildAcceleratorOptions(params)
return (acceleratorOptions,)
# Build accelerator options dictionary
def _extractParameters(self, kwargs):
"""Extract all parameters from kwargs"""
return {
"fbcache": kwargs.get("fbcache"),
"useCacheDistance": kwargs.get("useCacheDistance", False),
"cacheDistance": kwargs.get("cacheDistance", 0.0),
"teaCache": kwargs.get("teaCache"),
"useTeaCacheDistance": kwargs.get("useTeaCacheDistance", False),
"teaCacheDistance": kwargs.get("teaCacheDistance", 0.0),
"deepCache": kwargs.get("deepCache"),
"useDeepCacheOptions": kwargs.get("useDeepCacheOptions", False),
"deepCacheInterval": kwargs.get("deepCacheInterval", 1),
"deepCacheBranchId": kwargs.get("deepCacheBranchId", 0),
"useCacheSteps": kwargs.get("useCacheSteps", False),
"cacheStartStep": kwargs.get("cacheStartStep", 0),
"cacheEndStep": kwargs.get("cacheEndStep", 1),
"useCachePercentageSteps": kwargs.get("useCachePercentageSteps", False),
"cacheStartStepPercentage": kwargs.get("cacheStartStepPercentage", 0),
"cacheEndStepPercentage": kwargs.get("cacheEndStepPercentage", 1),
"useCacheMaxConsecutiveSteps": kwargs.get("useCacheMaxConsecutiveSteps", False),
"cacheMaxConsecutiveSteps": kwargs.get("cacheMaxConsecutiveSteps", 1),
}
def _buildAcceleratorOptions(self, params):
"""Build accelerator options dictionary from parameters"""
acceleratorOptions = {}
# FBCache options
if fbcache is True:
acceleratorOptions["fbcache"] = fbcache
if useCacheDistance:
acceleratorOptions["cacheDistance"] = cacheDistance
if params["fbcache"] is True:
acceleratorOptions["fbcache"] = params["fbcache"]
if params["useCacheDistance"]:
acceleratorOptions["cacheDistance"] = params["cacheDistance"]
# TeaCache options
if teaCache is True:
acceleratorOptions["teaCache"] = teaCache
if useTeaCacheDistance:
acceleratorOptions["teaCacheDistance"] = teaCacheDistance
if params["teaCache"] is True:
acceleratorOptions["teaCache"] = params["teaCache"]
if params["useTeaCacheDistance"]:
acceleratorOptions["teaCacheDistance"] = params["teaCacheDistance"]
# DeepCache options
if deepCache is True:
acceleratorOptions["deepCache"] = deepCache
if useDeepCacheOptions:
acceleratorOptions["deepCacheInterval"] = deepCacheInterval
acceleratorOptions["deepCacheBranchId"] = deepCacheBranchId
if params["deepCache"] is True:
acceleratorOptions["deepCache"] = params["deepCache"]
if params["useDeepCacheOptions"]:
acceleratorOptions["deepCacheInterval"] = params["deepCacheInterval"]
acceleratorOptions["deepCacheBranchId"] = params["deepCacheBranchId"]
# Cache step control options (step numbers)
if useCacheSteps:
if cacheStartStep > 0:
acceleratorOptions["cacheStartStep"] = cacheStartStep
acceleratorOptions["cacheEndStep"] = cacheEndStep
if params["useCacheSteps"]:
if params["cacheStartStep"] > 0:
acceleratorOptions["cacheStartStep"] = params["cacheStartStep"]
acceleratorOptions["cacheEndStep"] = params["cacheEndStep"]
# Cache step control options (percentages)
if useCachePercentageSteps:
if cacheStartStepPercentage > 0:
acceleratorOptions["cacheStartStepPercentage"] = cacheStartStepPercentage
acceleratorOptions["cacheEndStepPercentage"] = cacheEndStepPercentage
if params["useCachePercentageSteps"]:
if params["cacheStartStepPercentage"] > 0:
acceleratorOptions["cacheStartStepPercentage"] = params["cacheStartStepPercentage"]
acceleratorOptions["cacheEndStepPercentage"] = params["cacheEndStepPercentage"]
if useCacheMaxConsecutiveSteps:
acceleratorOptions["cacheMaxConsecutiveSteps"] = cacheMaxConsecutiveSteps
if params["useCacheMaxConsecutiveSteps"]:
acceleratorOptions["cacheMaxConsecutiveSteps"] = params["cacheMaxConsecutiveSteps"]
return (acceleratorOptions,)
return acceleratorOptions
+22 -1
View File
@@ -1,6 +1,9 @@
from .utils import runwareUtils as rwUtils
class apiManager:
"""API Manager node for configuring Runware API settings"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -60,4 +63,22 @@ class apiManager:
DESCRIPTION = "API Managers is a Runware Utility That helps you define or change your API Key, Session Timeout, Image Output Format & Quality, and Image Caching settings From The ComfyUI Interface Without having to adjust the env file locally."
CATEGORY = "Runware"
RETURN_TYPES = ()
RETURN_TYPES = ()
def manageApiSettings(self, **kwargs):
"""Manage API settings"""
apiKey = kwargs.get("API Key")
maxTimeout = kwargs.get("Max Timeout")
imageOutputQuality = kwargs.get("Image Output Quality")
imageOutputFormat = kwargs.get("Image Output Format")
enableImagesCaching = kwargs.get("Enable Images Caching")
minImageCacheSize = kwargs.get("Min Image Cache Size")
rwUtils.setApiKey(apiKey)
rwUtils.setTimeout(maxTimeout)
rwUtils.setOutputQuality(imageOutputQuality)
rwUtils.setOutputFormat(imageOutputFormat)
rwUtils.setEnableImagesCaching(enableImagesCaching)
rwUtils.setMinImageCacheSize(minImageCacheSize)
return ()
+173 -131
View File
@@ -2,14 +2,16 @@ import json
import uuid
import requests
import torch
import torchaudio
import numpy as np
from io import BytesIO
import librosa
import tempfile
import os
from .utils import runwareUtils as rwUtils
class RunwareAudioInference:
"""Runware Audio Inference node for generating audio using Runware API"""
def __init__(self):
pass
@@ -17,22 +19,26 @@ class RunwareAudioInference:
def INPUT_TYPES(cls):
return {
"required": {
"positivePrompt": ("STRING", {
"multiline": True,
"default": "classical piano piece, gentle and melodic",
"tooltip": "Text description that guides the audio generation process"
"model": ("RUNWAREAUDIOMODEL", {
"tooltip": "AI model to use for audio generation"
}),
"usePositivePrompt": ("BOOLEAN", {
"default": True,
"tooltip": "Enable/disable positivePrompt parameter in API request"
}),
"positivePrompt": ("STRING", {
"multiline": True,
"default": "classical piano piece, gentle and melodic",
"tooltip": "Text description that guides the audio generation process"
}),
"negativePrompt": ("STRING", {
"multiline": True,
"default": "",
"tooltip": "Text description of what you don't want in the audio"
}),
"model": ("RUNWAREAUDIOMODEL", {
"tooltip": "AI model to use for audio generation"
"useDuration": ("BOOLEAN", {
"default": True,
"tooltip": "Enable/disable duration parameter in API request"
}),
"duration": ("INT", {
"default": 10,
@@ -41,10 +47,6 @@ class RunwareAudioInference:
"step": 1,
"tooltip": "Length of generated audio in seconds (10-300)"
}),
"useDuration": ("BOOLEAN", {
"default": True,
"tooltip": "Enable/disable duration parameter in API request"
}),
"sampleRate": ("INT", {
"default": 22050,
"min": 8000,
@@ -87,146 +89,186 @@ class RunwareAudioInference:
CATEGORY = "Runware/Audio"
def generateAudio(self, **kwargs):
# Get required parameters
positivePrompt = kwargs.get("positivePrompt", "")
usePositivePrompt = kwargs.get("usePositivePrompt", True)
model = kwargs.get("model", "")
duration = kwargs.get("duration", 30)
useDuration = kwargs.get("useDuration", True)
sampleRate = kwargs.get("sampleRate", 44100)
bitrate = kwargs.get("bitrate", 128)
outputType = kwargs.get("outputType", "URL")
outputFormat = kwargs.get("outputFormat", "MP3")
negativePrompt = kwargs.get("negativePrompt", "")
numberResults = kwargs.get("numberResults", 1)
providerSettings = kwargs.get("providerSettings", None)
"""Main function to generate audio using Runware API"""
params = self._extractParameters(kwargs)
genConfig = self._buildGenConfig(params)
genResult = rwUtils.inferenecRequest(genConfig)
# Create task UUID
taskUUID = str(uuid.uuid4())
self._logResponse(genResult)
self._validateResponse(genResult)
audioUrl = self._extractAudioUrl(genResult)
audioObj = self._downloadAndProcessAudio(audioUrl, params["sampleRate"])
return (audioObj,)
def _extractParameters(self, kwargs):
"""Extract and validate parameters from kwargs"""
return {
"positivePrompt": kwargs.get("positivePrompt", ""),
"usePositivePrompt": kwargs.get("usePositivePrompt", True),
"model": kwargs.get("model", ""),
"duration": kwargs.get("duration", 30),
"useDuration": kwargs.get("useDuration", True),
"sampleRate": kwargs.get("sampleRate", 44100),
"bitrate": kwargs.get("bitrate", 128),
"outputType": kwargs.get("outputType", "URL"),
"outputFormat": kwargs.get("outputFormat", "MP3"),
"negativePrompt": kwargs.get("negativePrompt", ""),
"numberResults": kwargs.get("numberResults", 1),
"providerSettings": kwargs.get("providerSettings", None),
}
def _hasSections(self, providerSettings):
"""Check if provider settings contain sections"""
if not providerSettings or not isinstance(providerSettings, dict):
return False
elevenlabsSettings = providerSettings.get("elevenlabs", {})
musicSettings = elevenlabsSettings.get("music", {})
compositionPlan = musicSettings.get("compositionPlan", {})
sections = compositionPlan.get("sections", [])
return len(sections) > 0
def _buildGenConfig(self, params):
"""Build the generation configuration for API request"""
taskUuid = str(uuid.uuid4())
# Build the generation config
genConfig = [{
"taskType": "audioInference",
"taskUUID": taskUUID,
"model": model,
"outputType": outputType,
"outputFormat": outputFormat,
"taskUUID": taskUuid,
"model": params["model"],
"outputType": params["outputType"],
"outputFormat": params["outputFormat"],
"deliveryMethod": "sync",
"includeCost": True,
"numberResults": numberResults,
"numberResults": params["numberResults"],
"audioSettings": {
"sampleRate": sampleRate,
"bitrate": bitrate
"sampleRate": params["sampleRate"],
"bitrate": params["bitrate"]
}
}]
# Add positivePrompt only if usePositivePrompt is True AND no sections are provided
has_sections = False
if providerSettings is not None:
# Check if there are sections in the provider settings
if isinstance(providerSettings, dict):
elevenlabs_settings = providerSettings.get("elevenlabs", {})
music_settings = elevenlabs_settings.get("music", {})
composition_plan = music_settings.get("compositionPlan", {})
sections = composition_plan.get("sections", [])
has_sections = len(sections) > 0
# Handle sections - disable duration if sections are provided
hasSections = self._hasSections(params["providerSettings"])
if hasSections:
params["useDuration"] = False
print(f"[DEBUG] Disabled duration because sections are provided")
# When sections are provided, disable duration but add minimal positivePrompt (ElevenLabs requires it)
if has_sections:
useDuration = False
# ElevenLabs requires positivePrompt even with composition plans
#genConfig[0]["positivePrompt"] = "Music composition"
print(f"[DEBUG] Disabled duration and added minimal positivePrompt because sections are provided")
if usePositivePrompt:
genConfig[0]["positivePrompt"] = positivePrompt
print(f"[DEBUG] Added positivePrompt: '{positivePrompt}' (usePositivePrompt={usePositivePrompt})")
# Add prompts conditionally
if params["usePositivePrompt"]:
genConfig[0]["positivePrompt"] = params["positivePrompt"]
print(f"[DEBUG] Added positivePrompt: '{params['positivePrompt']}'")
else:
print(f"[DEBUG] Skipped positivePrompt (usePositivePrompt={usePositivePrompt})")
print(f"[DEBUG] Skipped positivePrompt")
# Add duration only if useDuration is True
if useDuration:
genConfig[0]["duration"] = duration
print(f"[DEBUG] Added duration: {duration} (useDuration={useDuration})")
if params["useDuration"]:
genConfig[0]["duration"] = params["duration"]
print(f"[DEBUG] Added duration: {params['duration']}")
else:
print(f"[DEBUG] Skipped duration (useDuration={useDuration})")
print(f"[DEBUG] Skipped duration")
# Add optional parameters if provided
if negativePrompt:
genConfig[0]["negativePrompt"] = negativePrompt
if providerSettings is not None:
genConfig[0]["providerSettings"] = providerSettings
# Add optional parameters
if params["negativePrompt"]:
genConfig[0]["negativePrompt"] = params["negativePrompt"]
if params["providerSettings"] is not None:
genConfig[0]["providerSettings"] = params["providerSettings"]
# Debug: Print the request being sent
print(f"[DEBUG] Sending Audio Inference Request:")
print(f"[DEBUG] Request Payload: {json.dumps(genConfig, indent=2)}")
# Make the API request
genResult = rwUtils.inferenecRequest(genConfig)
# Debug: Print the response received
return genConfig
def _logResponse(self, genResult):
"""Log the API response for debugging"""
print(f"[DEBUG] Received Audio Inference Response:")
print(f"[DEBUG] Response: {json.dumps(genResult, indent=2)}")
# Check for errors
def _validateResponse(self, genResult):
"""Validate API response and raise errors if needed"""
if "errors" in genResult:
error_message = genResult["errors"][0]["message"]
raise Exception(f"Audio generation failed: {error_message}")
errorMessage = genResult["errors"][0]["message"]
raise Exception(f"Audio generation failed: {errorMessage}")
# Extract audio data from response
if "data" in genResult and len(genResult["data"]) > 0:
audioData = genResult["data"][0]
# Get the audio URL
audioURL = audioData.get("audioURL", "")
if not audioURL:
# Fallback to other audio data formats
audioURL = audioData.get("audioDataURI", "")
if not audioURL:
audioURL = audioData.get("audioBase64Data", "")
if audioURL:
# Download the audio and create proper audio data for ComfyUI
try:
# Download the audio file
response = requests.get(audioURL, timeout=30)
response.raise_for_status()
# Save to temporary file and load with torchaudio
with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as temp_file:
temp_file.write(response.content)
temp_file_path = temp_file.name
try:
# Load audio with torchaudio
waveform, original_sample_rate = torchaudio.load(temp_file_path)
# Resample if necessary
if original_sample_rate != sampleRate:
resampler = torchaudio.transforms.Resample(original_sample_rate, sampleRate)
waveform = resampler(waveform)
# Ensure correct format: [batch, channels, samples]
if waveform.dim() == 2:
waveform = waveform.unsqueeze(0) # Add batch dimension
# Create the audio object
audio_obj = {
"waveform": waveform,
"sample_rate": sampleRate
}
finally:
# Clean up temporary file
if os.path.exists(temp_file_path):
os.unlink(temp_file_path)
return (audio_obj,)
except Exception as e:
raise Exception(f"Failed to download audio: {e}")
else:
raise Exception("No audio URL received from API")
else:
if "data" not in genResult or len(genResult["data"]) == 0:
raise Exception("No audio data received from API")
def _extractAudioUrl(self, genResult):
"""Extract audio URL from API response"""
audioData = genResult["data"][0]
audioUrl = audioData.get("audioURL", "")
if not audioUrl:
audioUrl = audioData.get("audioDataURI", "")
if not audioUrl:
audioUrl = audioData.get("audioBase64Data", "")
if not audioUrl:
raise Exception("No audio URL received from API")
return audioUrl
def _downloadAndProcessAudio(self, audioUrl, targetSampleRate):
"""Download audio file and process it for ComfyUI"""
try:
response = requests.get(audioUrl, timeout=30)
response.raise_for_status()
with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as tempFile:
tempFile.write(response.content)
tempFilePath = tempFile.name
try:
waveformNp, originalSampleRate = self._loadAudioFile(tempFilePath)
waveformNp = self._resampleAudio(waveformNp, originalSampleRate, targetSampleRate)
waveformTensor = self._convertToTensor(waveformNp)
return {
"waveform": waveformTensor,
"sample_rate": targetSampleRate
}
finally:
if os.path.exists(tempFilePath):
os.unlink(tempFilePath)
except Exception as e:
raise Exception(f"Failed to download audio: {e}")
def _loadAudioFile(self, filePath):
"""Load audio file using librosa"""
waveformNp, originalSampleRate = librosa.load(filePath, sr=None, mono=False)
# Convert to [samples, channels] format
if waveformNp.ndim == 1:
waveformNp = waveformNp.reshape(-1, 1)
else:
waveformNp = waveformNp.T # [channels, samples] -> [samples, channels]
return waveformNp, originalSampleRate
def _resampleAudio(self, waveformNp, originalSampleRate, targetSampleRate):
"""Resample audio to target sample rate"""
if originalSampleRate == targetSampleRate:
return waveformNp
if waveformNp.ndim == 1:
return librosa.resample(waveformNp, orig_sr=originalSampleRate, target_sr=targetSampleRate)
else:
# Multi-channel: resample each channel
return librosa.resample(waveformNp.T, orig_sr=originalSampleRate, target_sr=targetSampleRate).T
def _convertToTensor(self, waveformNp):
"""Convert numpy array to PyTorch tensor with correct format"""
# Ensure 2D shape [samples, channels]
if waveformNp.ndim == 1:
waveformNp = waveformNp.reshape(-1, 1)
# Convert to [channels, samples] for ComfyUI
waveformTensor = torch.from_numpy(waveformNp.T).float()
# Add batch dimension if needed: [batch, channels, samples]
if waveformTensor.dim() == 2:
waveformTensor = waveformTensor.unsqueeze(0)
return waveformTensor
+5 -4
View File
@@ -1,4 +1,6 @@
class RunwareAudioModelSearch:
"""Audio Model Search node for searching audio models"""
def __init__(self):
pass
@@ -19,13 +21,12 @@ class RunwareAudioModelSearch:
RETURN_TYPES = ("RUNWAREAUDIOMODEL",)
RETURN_NAMES = ("model",)
FUNCTION = "search_models"
FUNCTION = "searchModels"
CATEGORY = "Runware/Audio"
def search_models(self, **kwargs):
def searchModels(self, **kwargs):
"""Search for audio models"""
searchQuery = kwargs.get("searchQuery", "")
provider = kwargs.get("provider", "elevenlabs")
return (searchQuery,)
+16 -6
View File
@@ -1,4 +1,6 @@
class RunwareAudioSections:
"""Audio Sections node for creating audio composition sections"""
def __init__(self):
pass
@@ -34,23 +36,31 @@ class RunwareAudioSections:
RETURN_TYPES = ("RUNWAREAUDIOSECTIONS",)
RETURN_NAMES = ("sections",)
FUNCTION = "create_sections"
FUNCTION = "createSections"
CATEGORY = "Runware/Audio"
def create_sections(self, **kwargs):
def createSections(self, **kwargs):
"""Create audio section object from provided parameters"""
sectionName = kwargs.get("sectionName", "")
positiveLocalStyles = kwargs.get("positiveLocalStyles", "")
negativeLocalStyles = kwargs.get("negativeLocalStyles", "")
duration = kwargs.get("duration", 0)
lines = kwargs.get("lines", "")
# Create section object
section = {
"sectionName": sectionName,
"positiveLocalStyles": [style.strip() for style in positiveLocalStyles.split(",") if style.strip()],
"negativeLocalStyles": [style.strip() for style in negativeLocalStyles.split(",") if style.strip()],
"positiveLocalStyles": self._parseCommaSeparated(positiveLocalStyles),
"negativeLocalStyles": self._parseCommaSeparated(negativeLocalStyles),
"duration": duration,
"lines": [line.strip() for line in lines.split("\n") if line.strip()]
"lines": self._parseLines(lines)
}
return (section,)
def _parseCommaSeparated(self, text):
"""Parse comma-separated string into list"""
return [item.strip() for item in text.split(",") if item.strip()]
def _parseLines(self, text):
"""Parse multiline text into list"""
return [line.strip() for line in text.split("\n") if line.strip()]
+42 -53
View File
@@ -2,75 +2,64 @@ from .utils import runwareUtils as rwUtils
class RunwareFrameImages:
"""Frame Images node for defining keyframes that constrain video timeline"""
FRAME_POSITIONS = ["auto", "first", "last", "0", "12", "24", "36", "48", "60", "72", "84", "96", "108", "120"]
MAX_FRAMES = 4
@classmethod
def INPUT_TYPES(cls):
optionalInputs = {}
for i in range(1, cls.MAX_FRAMES + 1):
optionalInputs[f"image{i}"] = ("IMAGE", {
"tooltip": "Frame image that will constrain video content at a specific timeline position. Used for keyframe control.",
})
optionalInputs[f"frame{i}_position"] = (cls.FRAME_POSITIONS, {
"default": "auto",
"tooltip": "Frame position: 'auto' (automatic distribution), 'first' (beginning), 'last' (end), or specific frame number (0-120).",
})
return {
"required": {},
"optional": {
"image1": ("IMAGE", {
"tooltip": "Frame image that will constrain video content at a specific timeline position. Used for keyframe control.",
}),
"frame1_position": (["auto", "first", "last", "0", "12", "24", "36", "48", "60", "72", "84", "96", "108", "120"], {
"default": "auto",
"tooltip": "Frame position: 'auto' (automatic distribution), 'first' (beginning), 'last' (end), or specific frame number (0-120).",
}),
"image2": ("IMAGE", {
"tooltip": "Frame image that will constrain video content at a specific timeline position. Used for keyframe control.",
}),
"frame2_position": (["auto", "first", "last", "0", "12", "24", "36", "48", "60", "72", "84", "96", "108", "120"], {
"default": "auto",
"tooltip": "Frame position: 'auto' (automatic distribution), 'first' (beginning), 'last' (end), or specific frame number (0-120).",
}),
"image3": ("IMAGE", {
"tooltip": "Frame image that will constrain video content at a specific timeline position. Used for keyframe control.",
}),
"frame3_position": (["auto", "first", "last", "0", "12", "24", "36", "48", "60", "72", "84", "96", "108", "120"], {
"default": "auto",
"tooltip": "Frame position: 'auto' (automatic distribution), 'first' (beginning), 'last' (end), or specific frame number (0-120).",
}),
"image4": ("IMAGE", {
"tooltip": "Frame image that will constrain video content at a specific timeline position. Used for keyframe control.",
}),
"frame4_position": (["auto", "first", "last", "0", "12", "24", "36", "48", "60", "72", "84", "96", "108", "120"], {
"default": "auto",
"tooltip": "Frame position: 'auto' (automatic distribution), 'first' (beginning), 'last' (end), or specific frame number (0-120).",
}),
}
"optional": optionalInputs
}
DESCRIPTION = "Define keyframes that constrain specific frames within the video timeline. Different from reference images - these control WHEN specific visual content appears, not overall style consistency."
FUNCTION = "create_frame_images"
FUNCTION = "createFrameImages"
RETURN_TYPES = ("RUNWAREFRAMEIMAGES",)
RETURN_NAMES = ("Frame Images",)
CATEGORY = "Runware"
def create_frame_images(self, **kwargs):
frame_images = []
def createFrameImages(self, **kwargs):
"""Create frame images list from provided parameters"""
frameImages = []
# Process each image input
for i in range(1, 5): # Support up to 4 images
image_key = f"image{i}"
position_key = f"frame{i}_position"
for i in range(1, self.MAX_FRAMES + 1):
imageKey = f"image{i}"
positionKey = f"frame{i}_position"
image = kwargs.get(image_key)
position = kwargs.get(position_key, "auto")
image = kwargs.get(imageKey)
position = kwargs.get(positionKey, "auto")
if image is not None:
# Convert image to data URI or UUID
image_data = rwUtils.convertTensor2IMG(image)
frame_data = {"inputImage": image_data}
# Only add frame position if not auto
if position != "auto":
if position.isdigit():
frame_data["frame"] = int(position)
else:
frame_data["frame"] = position # "first" or "last"
frame_images.append(frame_data)
frameData = self._createFrameData(image, position)
frameImages.append(frameData)
return (frame_images,)
return (frameImages,)
def _createFrameData(self, image, position):
"""Create frame data object from image and position"""
imageData = rwUtils.convertTensor2IMG(image)
frameData = {"inputImage": imageData}
if position != "auto":
if position.isdigit():
frameData["frame"] = int(position)
else:
frameData["frame"] = position # "first" or "last"
return frameData
# Node class mappings for ComfyUI registration
+3 -3
View File
@@ -87,10 +87,10 @@ class txt2img:
"default": True,
}),
"seed": ("INT", {
"tooltip": "A value used to randomize the image generation. If you want to make images reproducible (generate the same image multiple times), you can use the same seed value. Random seeds are generated as 32-bit values for platform compatibility.",
"tooltip": "A value used to randomize the image generation. If you want to make images reproducible (generate the same image multiple times), you can use the same seed value. Set to 0 to auto-generate a random seed.",
"default": 1,
"min": 1,
"max": 9223372036854776000,
"min": 0,
"max": 4294967295,
}),
"useClipSkip": ("BOOLEAN", {
"tooltip": "Enable to include clipSkip parameter in API request. Disable if your model doesn't support clipSkip.",
+16 -14
View File
@@ -1,6 +1,9 @@
from .utils import runwareUtils as rwUtils
class imageInferenceInputs:
"""Image Inference Inputs node for configuring image generation inputs"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -31,32 +34,31 @@ class imageInferenceInputs:
CATEGORY = "Runware"
def createInputs(self, **kwargs):
"""Create image inference inputs from provided parameters"""
image = kwargs.get("image", None)
refImage1 = kwargs.get("Reference Image 1", None)
refImage2 = kwargs.get("Reference Image 2", None)
refImage3 = kwargs.get("Reference Image 3", None)
refImage4 = kwargs.get("Reference Image 4", None)
# Build the inputs structure
inputs = {}
# Handle image if provided
if image is not None:
inputs["image"] = rwUtils.convertTensor2IMG(image)
# Handle references if any are provided
references = []
if refImage1 is not None:
references.append(rwUtils.convertTensor2IMG(refImage1))
if refImage2 is not None:
references.append(rwUtils.convertTensor2IMG(refImage2))
if refImage3 is not None:
references.append(rwUtils.convertTensor2IMG(refImage3))
if refImage4 is not None:
references.append(rwUtils.convertTensor2IMG(refImage4))
references = self._collectReferences(refImage1, refImage2, refImage3, refImage4)
if len(references) > 0:
inputs["references"] = references
return (inputs, )
return (inputs,)
def _collectReferences(self, refImage1, refImage2, refImage3, refImage4):
"""Collect and convert reference images to list"""
references = []
refImages = [refImage1, refImage2, refImage3, refImage4]
for refImage in refImages:
if refImage is not None:
references.append(rwUtils.convertTensor2IMG(refImage))
return references
+14 -9
View File
@@ -1,7 +1,9 @@
from .utils import runwareUtils as rwUtils
from typing import List, Dict, Any
from typing import List
class inputAudios:
"""Input Audios node for configuring multiple audio inputs"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -29,11 +31,14 @@ class inputAudios:
CATEGORY = "Runware"
def createInputAudios(self, **kwargs) -> tuple[List[str]]:
audio_list = []
"""Create list of audio UUIDs from provided parameters"""
audioList = []
for i in range(1, 5):
audio_key = f"Audio{i}"
audio_uuid = kwargs.get(audio_key)
if audio_uuid and audio_uuid.strip() != "":
audio_list.append(audio_uuid.strip())
return (audio_list,)
audioKey = f"Audio{i}"
audioUuid = kwargs.get(audioKey)
if audioUuid and audioUuid.strip() != "":
audioList.append(audioUuid.strip())
return (audioList,)
+269 -276
View File
@@ -1,10 +1,9 @@
import base64
import os
import uuid
import time
import requests
import torch
import torchaudio
import soundfile as sf
import numpy as np
from io import BytesIO
from .utils.runwareUtils import (
@@ -12,11 +11,27 @@ from .utils.runwareUtils import (
RUNWARE_API_BASE_URL,
generalRequestWrapper,
genRandUUID,
genRandSeed,
sendMediaUUID,
)
class RunwareMediaUpload:
"""Runware Media Upload node for uploading audio/video to Runware storage"""
# MIME type mapping for video files
VIDEO_MIME_TYPES = {
'.mp4': 'video/mp4',
'.avi': 'video/avi',
'.mov': 'video/quicktime',
'.webm': 'video/webm',
}
DEFAULT_SAMPLE_RATE = 22050
DEFAULT_VIDEO_MIME = 'video/mp4'
MAX_POLL_ATTEMPTS = 100
POLL_INTERVAL = 2
REQUEST_TIMEOUT = 30
def __init__(self):
pass
@@ -33,7 +48,7 @@ class RunwareMediaUpload:
"tooltip": "This field will be automatically populated with the generated media UUID after upload.",
}),
},
"hidden": { "node_id": "UNIQUE_ID" }
"hidden": {"node_id": "UNIQUE_ID"}
}
RETURN_TYPES = ("STRING",)
@@ -44,325 +59,303 @@ class RunwareMediaUpload:
OUTPUT_NODE = True
@classmethod
def IS_CHANGED(s, **kwargs):
def IS_CHANGED(cls, **kwargs):
return float("NAN")
def uploadMedia(self, media=None, mediaUUID=None, **kwargs):
print(f"[Debug] uploadMedia called with media: {type(media)}, mediaUUID: {mediaUUID}")
def uploadMedia(self, media=None, mediaUuid=None, **kwargs):
"""Main function to upload media and return mediaUUID"""
if media is None:
raise ValueError("Media input is required")
print(f"[Debug] uploadMedia called with media: {type(media)}, mediaUuid: {mediaUuid}")
try:
# Convert media tensor to base64
print(f"[Debug] Converting media to base64...")
media_base64 = self._convert_media_to_base64(media)
print(f"[Debug] Media converted to base64, length: {len(media_base64)}")
mediaBase64 = self._convertMediaToBase64(media)
print(f"[Debug] Media converted to base64, length: {len(mediaBase64)}")
# Upload to Runware
print(f"[Debug] Starting upload to Runware...")
media_uuid = self._upload_to_runware(media_base64)
print(f"[Debug] Upload completed! MediaUUID: {media_uuid}")
print(f"[Debug] ===== RESULT: {media_uuid} =====")
# Send the mediaUUID back to the UI
sendMediaUUID(media_uuid, kwargs.get("node_id"))
return (media_uuid,)
mediaUuid = self._uploadToRunware(mediaBase64)
print(f"[Debug] Upload completed! MediaUUID: {mediaUuid}")
print(f"[Debug] ===== RESULT: {mediaUuid} =====")
sendMediaUUID(mediaUuid, kwargs.get("node_id"))
return (mediaUuid,)
except Exception as e:
print(f"[Debug] Media upload failed: {str(e)}")
raise e
def _convert_media_to_base64(self, media_tensor):
def _convertMediaToBase64(self, mediaTensor):
"""Convert media tensor to base64 data URI"""
try:
# Handle ComfyUI VIDEO objects (VideoFromFile)
print(f"[Debug] Media type: {type(media_tensor)}")
print(f"[Debug] Media attributes: {dir(media_tensor)}")
# Check if this is a ComfyUI VideoFromFile object
if hasattr(media_tensor, 'video_path') or str(type(media_tensor)).find('VideoFromFile') != -1:
# This is a ComfyUI VideoFromFile object
print(f"[Debug] VideoFromFile object detected")
# Try to get the file path from the private attribute
video_file = getattr(media_tensor, '_VideoFromFile__file', None)
if video_file is None:
# Try other possible attributes
for attr in ['video_path', 'path', 'file_path', 'filename', '_file']:
if hasattr(media_tensor, attr):
video_file = getattr(media_tensor, attr)
print(f"[Debug] Found video file via {attr}: {video_file}")
break
if video_file is None:
raise ValueError("Could not find video file in VideoFromFile object")
# If it's a file object, get the path
if hasattr(video_file, 'name'):
video_path = video_file.name
elif isinstance(video_file, str):
video_path = video_file
else:
raise ValueError(f"Unexpected video file type: {type(video_file)}")
print(f"[Debug] Processing video file: {video_path}")
# Read the video file and convert to base64
with open(video_path, 'rb') as f:
video_bytes = f.read()
# Detect MIME type based on file extension
if video_path.lower().endswith('.mp4'):
mime_type = 'video/mp4'
elif video_path.lower().endswith('.avi'):
mime_type = 'video/avi'
elif video_path.lower().endswith('.mov'):
mime_type = 'video/quicktime'
elif video_path.lower().endswith('.webm'):
mime_type = 'video/webm'
else:
mime_type = 'video/mp4' # Default
# Encode to base64
video_base64 = base64.b64encode(video_bytes).decode('utf-8')
# Return as data URI
return f"data:{mime_type};base64,{video_base64}"
# ComfyUI AUDIO objects are dictionaries with 'waveform' and 'sample_rate'
elif isinstance(media_tensor, dict):
if 'waveform' in media_tensor:
waveform = media_tensor['waveform']
sample_rate = media_tensor.get('sample_rate', 22050)
# Convert tensor to proper format for torchaudio
if isinstance(waveform, torch.Tensor):
# Ensure waveform is in the correct format [channels, samples]
if waveform.dim() == 3:
waveform = waveform.squeeze(0) # Remove batch dimension
elif waveform.dim() == 1:
waveform = waveform.unsqueeze(0) # Add channel dimension
else:
waveform = torch.tensor(waveform)
if waveform.dim() == 1:
waveform = waveform.unsqueeze(0)
# Create WAV file in memory
buffer = BytesIO()
torchaudio.save(buffer, waveform, sample_rate, format="wav")
buffer.seek(0)
# Get the WAV file bytes
wav_bytes = buffer.getvalue()
# Encode to base64
wav_base64 = base64.b64encode(wav_bytes).decode('utf-8')
# Return as data URI
return f"data:audio/wav;base64,{wav_base64}"
else:
raise ValueError("Invalid audio object: missing 'waveform' key")
else:
# Fallback for direct tensor input
if isinstance(media_tensor, torch.Tensor):
waveform = media_tensor
sample_rate = 22050 # Default sample rate
# Ensure waveform is in the correct format [channels, samples]
if waveform.dim() == 3:
waveform = waveform.squeeze(0) # Remove batch dimension
elif waveform.dim() == 1:
waveform = waveform.unsqueeze(0) # Add channel dimension
else:
waveform = torch.tensor(media_tensor)
sample_rate = 22050
if waveform.dim() == 1:
waveform = waveform.unsqueeze(0)
# Create WAV file in memory
buffer = BytesIO()
torchaudio.save(buffer, waveform, sample_rate, format="wav")
buffer.seek(0)
# Get the WAV file bytes
wav_bytes = buffer.getvalue()
# Encode to base64
wav_base64 = base64.b64encode(wav_bytes).decode('utf-8')
# Return as data URI
return f"data:audio/wav;base64,{wav_base64}"
except Exception as e:
print(f"[Debug] Error converting media to base64: {str(e)}")
raise e
def _upload_to_runware(self, media_data_uri):
"""Upload media to Runware and return mediaUUID"""
global RUNWARE_API_KEY, RUNWARE_API_BASE_URL
print(f"[Debug] Media type: {type(mediaTensor)}")
task_uuid = genRandUUID()
print(f"[Debug] Generated task UUID: {task_uuid}")
# Strip data URI prefix for media storage API
if media_data_uri.startswith("data:"):
media_data = media_data_uri.split(",", 1)[1]
print(f"[Debug] Stripped data URI prefix, media data length: {len(media_data)}")
if self._isVideoFile(mediaTensor):
return self._convertVideoToBase64(mediaTensor)
elif isinstance(mediaTensor, dict) and 'waveform' in mediaTensor:
return self._convertAudioDictToBase64(mediaTensor)
elif isinstance(mediaTensor, torch.Tensor):
return self._convertTensorToBase64(mediaTensor)
else:
media_data = media_data_uri
raise ValueError(f"Unsupported media type: {type(mediaTensor)}")
def _isVideoFile(self, mediaTensor):
"""Check if media tensor is a VideoFromFile object"""
return (hasattr(mediaTensor, 'video_path') or
str(type(mediaTensor)).find('VideoFromFile') != -1)
def _convertVideoToBase64(self, videoTensor):
"""Convert video file to base64 data URI"""
print(f"[Debug] VideoFromFile object detected")
upload_config = [
{
"taskType": "mediaStorage",
"taskUUID": task_uuid,
"operation": "upload",
"media": media_data,
}
]
videoFile = self._getVideoFilePath(videoTensor)
videoPath = self._extractVideoPath(videoFile)
print(f"[Debug] Upload config: {upload_config[0]}")
print(f"[Debug] Processing video file: {videoPath}")
with open(videoPath, 'rb') as f:
videoBytes = f.read()
mimeType = self._getMimeType(videoPath)
videoBase64 = base64.b64encode(videoBytes).decode('utf-8')
return f"data:{mimeType};base64,{videoBase64}"
def _getVideoFilePath(self, videoTensor):
"""Extract video file path from VideoFromFile object"""
videoFile = getattr(videoTensor, '_VideoFromFile__file', None)
if videoFile is None:
for attr in ['video_path', 'path', 'file_path', 'filename', '_file']:
if hasattr(videoTensor, attr):
videoFile = getattr(videoTensor, attr)
print(f"[Debug] Found video file via {attr}: {videoFile}")
break
if videoFile is None:
raise ValueError("Could not find video file in VideoFromFile object")
return videoFile
def _extractVideoPath(self, videoFile):
"""Extract file path from file object or string"""
if hasattr(videoFile, 'name'):
return videoFile.name
elif isinstance(videoFile, str):
return videoFile
else:
raise ValueError(f"Unexpected video file type: {type(videoFile)}")
def _getMimeType(self, filePath):
"""Get MIME type based on file extension"""
ext = os.path.splitext(filePath.lower())[1]
return self.VIDEO_MIME_TYPES.get(ext, self.DEFAULT_VIDEO_MIME)
def _convertAudioDictToBase64(self, audioDict):
"""Convert audio dictionary to base64 WAV data URI"""
waveform = audioDict['waveform']
sampleRate = audioDict.get('sample_rate', self.DEFAULT_SAMPLE_RATE)
waveformNp = self._prepareWaveformNumpy(waveform)
wavBytes = self._convertWaveformToWav(waveformNp, sampleRate)
wavBase64 = base64.b64encode(wavBytes).decode('utf-8')
return f"data:audio/wav;base64,{wavBase64}"
def _convertTensorToBase64(self, mediaTensor):
"""Convert direct tensor input to base64 WAV data URI"""
waveformNp = self._prepareWaveformNumpy(mediaTensor)
wavBytes = self._convertWaveformToWav(waveformNp, self.DEFAULT_SAMPLE_RATE)
wavBase64 = base64.b64encode(wavBytes).decode('utf-8')
return f"data:audio/wav;base64,{wavBase64}"
def _prepareWaveformNumpy(self, waveform):
"""Prepare waveform tensor/array for soundfile (returns [channels, samples])"""
if isinstance(waveform, torch.Tensor):
# Normalize dimensions: [batch, channels, samples] -> [channels, samples]
if waveform.dim() == 3:
waveform = waveform.squeeze(0)
elif waveform.dim() == 1:
waveform = waveform.unsqueeze(0)
waveformNp = waveform.cpu().numpy()
else:
waveformNp = np.array(waveform)
if waveformNp.ndim == 1:
waveformNp = waveformNp.reshape(1, -1)
return waveformNp
def _convertWaveformToWav(self, waveformNp, sampleRate):
"""Convert numpy waveform to WAV bytes using soundfile"""
# soundfile expects [samples, channels]
waveformNp = waveformNp.T
buffer = BytesIO()
sf.write(buffer, waveformNp, sampleRate, format='WAV')
buffer.seek(0)
return buffer.getvalue()
def _uploadToRunware(self, mediaDataUri):
"""Upload media to Runware and return mediaUUID"""
taskUuid = genRandUUID()
print(f"[Debug] Generated task UUID: {taskUuid}")
mediaData = self._stripDataUriPrefix(mediaDataUri)
uploadConfig = self._buildUploadConfig(taskUuid, mediaData)
print(f"[Debug] Upload config: {uploadConfig[0]}")
print(f"[Debug] API URL: {RUNWARE_API_BASE_URL}")
headers = {
uploadResult = self._makeUploadRequest(uploadConfig)
self._validateUploadResponse(uploadResult)
uploadData = uploadResult.get("data", [])
if uploadData and len(uploadData) > 0:
mediaUuid = uploadData[0].get("mediaUUID", "")
if mediaUuid and mediaUuid != "1":
print(f"[Debug] Got immediate mediaUUID: {mediaUuid}")
return mediaUuid
print(f"[Debug] Starting polling for result...")
return self._pollForResult(taskUuid)
def _stripDataUriPrefix(self, mediaDataUri):
"""Strip data URI prefix if present"""
if mediaDataUri.startswith("data:"):
mediaData = mediaDataUri.split(",", 1)[1]
print(f"[Debug] Stripped data URI prefix, media data length: {len(mediaData)}")
return mediaData
return mediaDataUri
def _buildUploadConfig(self, taskUuid, mediaData):
"""Build upload configuration for API request"""
return [{
"taskType": "mediaStorage",
"taskUUID": taskUuid,
"operation": "upload",
"media": mediaData,
}]
def _getApiHeaders(self):
"""Get API request headers"""
return {
"Authorization": f"Bearer {RUNWARE_API_KEY}",
"Content-Type": "application/json",
"Accept-Encoding": "gzip, deflate, br, zstd",
}
def _makeUploadRequest(self, uploadConfig):
"""Make upload request to Runware API"""
def recaller():
print(f"[Debug] Making POST request to Runware API...")
return requests.post(
RUNWARE_API_BASE_URL,
headers=self._getApiHeaders(),
json=uploadConfig,
timeout=self.REQUEST_TIMEOUT,
allow_redirects=False,
stream=True,
)
response = generalRequestWrapper(recaller)
return self._parseJsonResponse(response)
def _parseJsonResponse(self, response):
"""Parse JSON response from API"""
if response.status_code != 200:
errorText = response.text[:500]
print(f"[Debug] Request failed with status {response.status_code}")
print(f"[Debug] Response text: {errorText}")
raise Exception(f"Request failed with status {response.status_code}: {response.text[:200]}")
if not response.text or not response.text.strip():
raise Exception("Request failed: Empty response from server")
try:
def recaller():
print(f"[Debug] Making POST request to Runware API...")
return requests.post(
RUNWARE_API_BASE_URL,
headers=headers,
json=upload_config,
timeout=30,
allow_redirects=False,
stream=True,
)
upload_result = generalRequestWrapper(recaller)
# Check response status before trying to parse JSON
if upload_result.status_code != 200:
print(f"[Debug] Upload failed with status {upload_result.status_code}")
print(f"[Debug] Response text: {upload_result.text[:500]}")
raise Exception(f"Upload failed with status {upload_result.status_code}: {upload_result.text[:200]}")
# Check if response is empty
if not upload_result.text or not upload_result.text.strip():
raise Exception("Upload failed: Empty response from server")
try:
upload_result = upload_result.json()
except ValueError as e:
print(f"[Debug] Failed to parse JSON response: {str(e)}")
print(f"[Debug] Response text: {upload_result.text[:500]}")
raise Exception(f"Upload failed: Invalid JSON response from server: {str(e)}")
print(f"[Debug] Upload response: {upload_result}")
if "errors" in upload_result:
print(f"[Debug] Upload error: {upload_result}")
raise Exception(f"Upload failed: {upload_result}")
# Check if we got immediate result or need to poll
if upload_result.get("data") and len(upload_result["data"]) > 0:
data = upload_result["data"][0]
print(f"[Debug] Upload data: {data}")
if "mediaUUID" in data and data["mediaUUID"] != "1":
print(f"[Debug] Got immediate mediaUUID: {data['mediaUUID']}")
return data["mediaUUID"]
# Poll for result
print(f"[Debug] Starting polling for result...")
return self._poll_for_result(task_uuid)
except Exception as e:
print(f"[Debug] Upload request failed: {str(e)}")
raise e
return response.json()
except ValueError as e:
print(f"[Debug] Failed to parse JSON response: {str(e)}")
print(f"[Debug] Response text: {response.text[:500]}")
raise Exception(f"Request failed: Invalid JSON response from server: {str(e)}")
def _poll_for_result(self, task_uuid):
def _validateUploadResponse(self, uploadResult):
"""Validate upload response"""
print(f"[Debug] Upload response: {uploadResult}")
if "errors" in uploadResult:
print(f"[Debug] Upload error: {uploadResult}")
raise Exception(f"Upload failed: {uploadResult}")
def _pollForResult(self, taskUuid):
"""Poll for media upload result"""
global RUNWARE_API_KEY, RUNWARE_API_BASE_URL
pollConfig = [{
"taskType": "getResponse",
"taskUUID": taskUuid,
}]
poll_config = [
{
"taskType": "getResponse",
"taskUUID": task_uuid,
}
]
headers = {
"Authorization": f"Bearer {RUNWARE_API_KEY}",
"Content-Type": "application/json",
"Accept-Encoding": "gzip, deflate, br, zstd",
}
max_attempts = 100
for attempt in range(max_attempts):
for attempt in range(self.MAX_POLL_ATTEMPTS):
try:
print(f"[Debug] Poll attempt {attempt + 1}/{max_attempts}")
print(f"[Debug] Poll attempt {attempt + 1}/{self.MAX_POLL_ATTEMPTS}")
def recaller():
return requests.post(
RUNWARE_API_BASE_URL,
headers=headers,
json=poll_config,
timeout=30,
headers=self._getApiHeaders(),
json=pollConfig,
timeout=self.REQUEST_TIMEOUT,
allow_redirects=False,
stream=True,
)
poll_result = generalRequestWrapper(recaller)
pollResult = generalRequestWrapper(recaller)
pollData = self._parsePollResponse(pollResult)
# Check response status
if poll_result.status_code != 200:
print(f"[Debug] Poll failed with status {poll_result.status_code}")
continue
# Check if response is empty
if not poll_result.text or not poll_result.text.strip():
print(f"[Debug] Poll returned empty response")
continue
try:
poll_result = poll_result.json()
except ValueError as e:
print(f"[Debug] Failed to parse JSON in poll: {str(e)}")
continue
print(f"[Debug] Poll response: {poll_result}")
if "errors" in poll_result:
print(f"[Debug] Poll error: {poll_result}")
continue
if "data" in poll_result and len(poll_result["data"]) > 0:
data = poll_result["data"][0]
print(f"[Debug] Poll data: {data}")
# Check if mediaUUID is still "1" (still processing)
if "mediaUUID" in data and data["mediaUUID"] == "1":
if pollData:
mediaUuid = pollData.get("mediaUUID", "")
if mediaUuid == "1":
print(f"[Debug] Still processing... attempt {attempt + 1}")
time.sleep(2)
time.sleep(self.POLL_INTERVAL)
continue
# Check for completion - look for mediaUUID
if "mediaUUID" in data and data["mediaUUID"] != "1":
print(f"[Debug] Upload completed! MediaUUID: {data['mediaUUID']}")
return data["mediaUUID"]
elif mediaUuid and mediaUuid != "1":
print(f"[Debug] Upload completed! MediaUUID: {mediaUuid}")
return mediaUuid
print(f"[Debug] Attempt {attempt + 1}: No data in response, continuing...")
time.sleep(2)
time.sleep(self.POLL_INTERVAL)
except Exception as e:
print(f"[Debug] Poll attempt {attempt + 1} failed: {str(e)}")
time.sleep(2)
time.sleep(self.POLL_INTERVAL)
continue
raise Exception("Polling timeout - upload did not complete")
def _parsePollResponse(self, pollResult):
"""Parse polling response"""
if pollResult.status_code != 200:
print(f"[Debug] Poll failed with status {pollResult.status_code}")
return None
if not pollResult.text or not pollResult.text.strip():
print(f"[Debug] Poll returned empty response")
return None
try:
pollResultJson = pollResult.json()
except ValueError as e:
print(f"[Debug] Failed to parse JSON in poll: {str(e)}")
return None
print(f"[Debug] Poll response: {pollResultJson}")
if "errors" in pollResultJson:
print(f"[Debug] Poll error: {pollResultJson}")
return None
if "data" in pollResultJson and len(pollResultJson["data"]) > 0:
print(f"[Debug] Poll data: {pollResultJson['data'][0]}")
return pollResultJson["data"][0]
return None
# Export the class directly
runwareMediaUpload = RunwareMediaUpload
+46 -37
View File
@@ -1,4 +1,42 @@
class modelSearch:
"""Model Search node for searching and selecting image models"""
MODEL_ARCHITECTURES = [
"All",
"FLUX.1-Schnell",
"FLUX.1-Dev",
"FLUX.1-Krea",
"Pony",
"SD 1.5",
"SD 1.5 Hyper",
"SD 1.5 LCM",
"SD 3",
"SDXL 1.0",
"SDXL 1.0 LCM",
"SDXL Distilled",
"SDXL Hyper",
"SDXL Lightning",
"SDXL Turbo",
]
MODEL_TYPES = [
"Base Model",
"Inpainting Model",
]
FEATURED_MODELS = [
"rundiffusion:110@101 (Juggernaut Lightning Flux by RunDiffusion)",
"rundiffusion:130@100 (Juggernaut Pro Flux by RunDiffusion)",
"runware:100@1 (Flux Schnell)",
"runware:101@1 (Flux Dev)",
"runware:107@1 (FLUX.1 Krea)",
"runware:5@1 (SD3)",
"civitai:4384@128713 (SDXL 1.5 DreamShaper)",
"civitai:43331@176425 (SD 1.5 majicMIX realistic 麦橘写实)",
"civitai:101055@128078 (SDXL v1.0 VAE fix)",
"civitai:133005@288982 (SDXL Juggernaut XL V8)",
]
@classmethod
def INPUT_TYPES(cls):
return {
@@ -6,45 +44,15 @@ class modelSearch:
"Model Search": ("STRING", {
"tooltip": "Search For Specific Model By Name Or Civit AIR Code (eg: Juggernaut).",
}),
"Model Architecture": ([
"All",
"FLUX.1-Schnell",
"FLUX.1-Dev",
"FLUX.1-Krea",
"Pony",
"SD 1.5",
"SD 1.5 Hyper",
"SD 1.5 LCM",
"SD 3",
"SDXL 1.0",
"SDXL 1.0 LCM",
"SDXL Distilled",
"SDXL Hyper",
"SDXL Lightning",
"SDXL Turbo",
], {
"Model Architecture": (cls.MODEL_ARCHITECTURES, {
"tooltip": "Choose Model Architecture To Filter Results.",
"default": "All",
}),
"ModelType": ([
"Base Model",
"Inpainting Model",
], {
"ModelType": (cls.MODEL_TYPES, {
"tooltip": "Choose Model Type To Filter Results.",
"default": "Base Model",
}),
"ModelList": ([
"rundiffusion:110@101 (Juggernaut Lightning Flux by RunDiffusion)",
"rundiffusion:130@100 (Juggernaut Pro Flux by RunDiffusion)",
"runware:100@1 (Flux Schnell)",
"runware:101@1 (Flux Dev)",
"runware:107@1 (FLUX.1 Krea)",
"runware:5@1 (SD3)",
"civitai:4384@128713 (SDXL 1.5 DreamShaper)",
"civitai:43331@176425 (SD 1.5 majicMIX realistic 麦橘写实)",
"civitai:101055@128078 (SDXL v1.0 VAE fix)",
"civitai:133005@288982 (SDXL Juggernaut XL V8)",
], {
"ModelList": (cls.FEATURED_MODELS, {
"tooltip": "Model Results Will Show UP Here So You Could Choose From. If You didn't Search For Anything this will show featured Model List.",
"default": "runware:100@1 (Flux Schnell)",
}),
@@ -68,13 +76,14 @@ class modelSearch:
return True
def modelSearch(self, **kwargs):
"""Search and return model AIR code"""
enableSearchValue = kwargs.get("Use Search Value", False)
searchInput = kwargs.get("Model Search")
if enableSearchValue:
modelAIRCode = searchInput
modelAirCode = searchInput
else:
crModel = kwargs.get("ModelList")
modelAIRCode = crModel.split(" ")[0]
currentModel = kwargs.get("ModelList")
modelAirCode = currentModel.split(" ")[0]
return (modelAIRCode,)
return (modelAirCode,)
+3 -3
View File
@@ -85,10 +85,10 @@ class photoMaker:
"max": 2,
}),
"seed": ("INT", {
"tooltip": "A value used to randomize the image generation. If you want to make images reproducible (generate the same image multiple times), you can use the same seed value.",
"tooltip": "A value used to randomize the image generation. If you want to make images reproducible (generate the same image multiple times), you can use the same seed value. Set to 0 to auto-generate a random seed.",
"default": 0,
"min": 1,
"max": 9223372036854776000,
"min": 0,
"max": 4294967295,
}),
"batchSize": ("INT", {
"tooltip": "The number of images to generate in a single request.",
+5
View File
@@ -0,0 +1,5 @@
"""
Runware Provider Settings Module
Contains all provider-specific settings nodes for Runware ComfyUI integration.
"""
@@ -1,4 +1,4 @@
from .utils import runwareUtils as rwUtils
from ..utils import runwareUtils as rwUtils
class RunwareOpenAIProviderSettings:
@@ -1,4 +1,4 @@
from .utils import runwareUtils as rwUtils
from ..utils import runwareUtils as rwUtils
class RunwarePixverseProviderSettings:
@@ -1,4 +1,4 @@
from .utils import runwareUtils as rwUtils
from ..utils import runwareUtils as rwUtils
class RunwareProviderSettings:
@@ -0,0 +1,57 @@
"""
Runware Runway Provider Settings Node
Provides Runway-specific settings for video generation
"""
from typing import Optional, Dict, Any
class RunwareRunwayProviderSettings:
"""Runware Runway Provider Settings Node"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {},
"optional": {
"publicFigureThreshold": ("STRING", {
"tooltip": "Public figure threshold for content moderation (provider key: contentModeration.publicFigureThreshold).",
"default": None,
}),
}
}
RETURN_TYPES = ("RUNWAREPROVIDERSETTINGS",)
RETURN_NAMES = ("Provider Settings",)
FUNCTION = "createProviderSettings"
CATEGORY = "Runware"
DESCRIPTION = "Configure Runway-specific provider settings for video generation including content moderation options."
def createProviderSettings(self, **kwargs) -> tuple[Dict[str, Any]]:
"""Create Runway provider settings"""
publicFigureThreshold = kwargs.get("publicFigureThreshold", None)
runwaySettings: Dict[str, Any] = {}
# Build contentModeration if publicFigureThreshold is provided
if publicFigureThreshold is not None and publicFigureThreshold.strip():
contentModeration: Dict[str, Any] = {}
contentModeration["publicFigureThreshold"] = publicFigureThreshold.strip()
runwaySettings["contentModeration"] = contentModeration
# Remove unset values
runwaySettings = {k: v for k, v in runwaySettings.items() if v is not None}
return (runwaySettings,)
# Node class mappings
NODE_CLASS_MAPPINGS = {
"RunwareRunwayProviderSettings": RunwareRunwayProviderSettings,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"RunwareRunwayProviderSettings": "Runware Runway Provider Settings",
}
@@ -1,4 +1,4 @@
from .utils import runwareUtils as rwUtils
from ..utils import runwareUtils as rwUtils
class RunwareViduProviderSettings:
@classmethod
+13 -10
View File
@@ -1,13 +1,16 @@
from .utils import runwareUtils as rwUtils
class referenceImages:
"""Reference Images node for providing reference images for identity consistency"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"Image 1": ("IMAGE", {
"tooltip": "Specifies Reference Image 1 that will be used as reference for the subject. These reference images help the AI to maintain identity consistency during the generation process."
}),
"Image 1": ("IMAGE", {
"tooltip": "Specifies Reference Image 1 that will be used as reference for the subject. These reference images help the AI to maintain identity consistency during the generation process."
}),
},
"optional": {
"Image 2": ("IMAGE", {
@@ -29,17 +32,17 @@ class referenceImages:
CATEGORY = "Runware"
def referenceImages(self, **kwargs):
"""Collect and convert reference images to list"""
image1 = kwargs.get("Image 1")
image2 = kwargs.get("Image 2", None)
image3 = kwargs.get("Image 3", None)
image4 = kwargs.get("Image 4", None)
imageList = [rwUtils.convertTensor2IMG(image1)]
if (image2 is not None):
imageList.append(rwUtils.convertTensor2IMG(image2))
if (image3 is not None):
imageList.append(rwUtils.convertTensor2IMG(image3))
if (image4 is not None):
imageList.append(rwUtils.convertTensor2IMG(image4))
optionalImages = [image2, image3, image4]
for image in optionalImages:
if image is not None:
imageList.append(rwUtils.convertTensor2IMG(image))
return (imageList, )
return (imageList,)
+26 -23
View File
@@ -1,25 +1,25 @@
from typing import List
from .utils import runwareUtils as rwUtils
from typing import List, Dict, Any
class referenceVideos:
"""Reference Videos node for configuring reference video inputs"""
MAX_VIDEOS = 4
@classmethod
def INPUT_TYPES(cls):
optionalInputs = {}
for i in range(1, cls.MAX_VIDEOS + 1):
ordinal = rwUtils.getOrdinal(i)
optionalInputs[f"Video{i}"] = ("STRING", {
"tooltip": f"MediaUUID for the {ordinal} reference video from Runware Media Upload node.",
})
return {
"required": {},
"optional": {
"Video1": ("STRING", {
"tooltip": "MediaUUID for the first reference video from Runware Media Upload node.",
}),
"Video2": ("STRING", {
"tooltip": "MediaUUID for the second reference video from Runware Media Upload node.",
}),
"Video3": ("STRING", {
"tooltip": "MediaUUID for the third reference video from Runware Media Upload node.",
}),
"Video4": ("STRING", {
"tooltip": "MediaUUID for the fourth reference video from Runware Media Upload node.",
}),
}
"optional": optionalInputs
}
DESCRIPTION = "Configure multiple reference video inputs (mediaUUIDs) for video generation."
@@ -29,11 +29,14 @@ class referenceVideos:
CATEGORY = "Runware"
def createReferenceVideos(self, **kwargs) -> tuple[List[str]]:
video_list = []
for i in range(1, 5):
video_key = f"Video{i}"
video_uuid = kwargs.get(video_key)
if video_uuid and video_uuid.strip() != "":
video_list.append(video_uuid.strip())
return (video_list,)
"""Create list of reference video UUIDs"""
videoList = []
for i in range(1, self.MAX_VIDEOS + 1):
videoKey = f"Video{i}"
videoUuid = kwargs.get(videoKey)
if videoUuid and videoUuid.strip() != "":
videoList.append(videoUuid.strip())
return (videoList,)
+11 -4
View File
@@ -1,4 +1,6 @@
class refiner:
"""Refiner node for enhancing image quality using refiner models"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -28,22 +30,27 @@ class refiner:
@classmethod
def VALIDATE_INPUTS(cls, startStep, startStepPercentage):
if(startStep != 0 and startStepPercentage != 0):
"""Validate that exactly one of startStep or startStepPercentage is provided"""
if startStep != 0 and startStepPercentage != 0:
raise Exception("Please provide either startStep or startStepPercentage, not both.")
elif(startStep == 0 and startStepPercentage == 0):
elif startStep == 0 and startStepPercentage == 0:
raise Exception("Please provide either startStep or startStepPercentage.")
else:
return True
def refiner(self, **kwargs):
"""Create refiner configuration"""
runwareModel = kwargs.get("Model")
startStep = kwargs.get("startStep")
startStepPercentage = kwargs.get("startStepPercentage")
refinerResult = {
"model": runwareModel,
}
if(startStep != 0):
if startStep != 0:
refinerResult["startStep"] = startStep
else:
refinerResult["startStepPercentage"] = startStepPercentage
return (refinerResult,)
return (refinerResult,)
+3 -3
View File
@@ -38,10 +38,10 @@ class runwareKontext:
"tooltip": "Adjust the dimensions of the generated image. This setting allows you to choose from various aspect ratios, such as 16:9 for wide images or 1:1 for square images. The width and height will be automatically adjusted based on the selected aspect ratio.",
}),
"seed": ("INT", {
"tooltip": "A value used to randomize the image generation. If you want to make images reproducible (generate the same image multiple times), you can use the same seed value.",
"tooltip": "A value used to randomize the image generation. If you want to make images reproducible (generate the same image multiple times), you can use the same seed value. Set to 0 to auto-generate a random seed.",
"default": 0,
"min": 1,
"max": 9223372036854776000,
"min": 0,
"max": 4294967295,
}),
"Multi Inference Mode": ("BOOLEAN", {
"tooltip": "If Enabled the node will skip the image generation process and will only return the Runware Task Object to be used in the Multi Inference Node.",
+11 -13
View File
@@ -1,7 +1,8 @@
from typing import Optional, Dict, Any
class safetyInputs:
"""Runware Safety Inputs Node"""
"""Runware Safety Inputs Node for configuring safety and content moderation settings"""
@classmethod
def INPUT_TYPES(cls):
@@ -35,32 +36,30 @@ class safetyInputs:
RETURN_TYPES = ("RUNWARESAFETYINPUTS",)
RETURN_NAMES = ("Safety Inputs",)
FUNCTION = "create_safety_inputs"
FUNCTION = "createSafetyInputs"
CATEGORY = "Runware"
DESCRIPTION = "Configure safety and content moderation settings for Runware tasks."
def create_safety_inputs(self, **kwargs) -> tuple[Dict[str, Any]]:
def createSafetyInputs(self, **kwargs) -> tuple[Dict[str, Any]]:
"""Create safety inputs dictionary"""
# Get parameters
mode = kwargs.get("mode", None)
tolerance = kwargs.get("tolerance", None)
checkInputs = kwargs.get("checkInputs", None)
checkContent = kwargs.get("checkContent", None)
# Build safety inputs dictionary - only include non-None values
safety_inputs = {}
safetyInputs = {}
if mode is not None:
safety_inputs["mode"] = mode
safetyInputs["mode"] = mode
if tolerance is not None:
safety_inputs["tolerance"] = tolerance
safetyInputs["tolerance"] = tolerance
if checkInputs is not None:
safety_inputs["checkInputs"] = checkInputs
safetyInputs["checkInputs"] = checkInputs
if checkContent is not None:
safety_inputs["checkContent"] = checkContent
safetyInputs["checkContent"] = checkContent
return (safety_inputs,)
return (safetyInputs,)
# Node class mappings
NODE_CLASS_MAPPINGS = {
@@ -70,4 +69,3 @@ NODE_CLASS_MAPPINGS = {
NODE_DISPLAY_NAME_MAPPINGS = {
"RunwareSafetyInputs": "Runware Safety Inputs",
}
+7 -1
View File
@@ -192,12 +192,18 @@ def setMinImageCacheSize(size: int):
os.environ["RUNWARE_MIN_IMAGE_CACHE_SIZE"] = str(size)
return True
def genRandSeed(minSeed=1000, maxSeed=9223372036854776000):
def genRandSeed(minSeed=1, maxSeed=4294967295):
"""Generate a random seed value (32-bit unsigned integer range for compatibility)"""
return random.randint(minSeed, maxSeed)
def genRandUUID():
return str(uuid.uuid4())
def getOrdinal(num):
"""Get ordinal string for number (e.g., 1 -> 'first', 2 -> 'second')"""
ordinals = {1: "first", 2: "second", 3: "third", 4: "fourth"}
return ordinals.get(num, f"{num}th")
def checkAPIKey(apiKey):
headers = {
"Accept": "application/json",
+59 -61
View File
@@ -3,14 +3,18 @@ import json
import base64
import io
class SVGData:
"""Wrapper class to match RecraftIO.SVG format for SaveSVGNode"""
def __init__(self, svg_content):
# Convert SVG string to BytesIO
svg_bytes = io.BytesIO(svg_content.encode('utf-8'))
self.data = [svg_bytes]
def __init__(self, svgContent):
svgBytes = io.BytesIO(svgContent.encode('utf-8'))
self.data = [svgBytes]
class vectorize:
"""Vectorize node for converting images to vector format"""
RUNWARE_VECTORIZE_MODELS = {
"recraft:1@1": "recraft:1@1",
"picsart:1@1": "picsart:1@1",
@@ -37,69 +41,64 @@ class vectorize:
CATEGORY = "Runware"
def vectorizeImage(self, **kwargs):
"""Vectorize image and return SVG data"""
image = kwargs.get("Image")
modelName = kwargs.get("Model", "recraft:1@1")
# Get the model AIR code
model = self.RUNWARE_VECTORIZE_MODELS.get(modelName, "recraft:1@1")
imageUuid = rwUtils.convertTensor2IMG(image)
# Convert image to UUID
image_uuid = rwUtils.convertTensor2IMG(image)
genConfig = self._buildGenConfig(model, imageUuid)
# Build the API request
genConfig = [
{
"taskType": "vectorize",
"taskUUID": rwUtils.genRandUUID(),
"model": model,
"inputs": {
"image": image_uuid
},
"outputType": "base64Data",
"outputFormat": "svg",
}
]
print(f"[DEBUG] Sending Vectorize Request:")
print(f"[DEBUG] Request Payload: {json.dumps(genConfig, indent=2)}")
try:
# Debug: Print the request being sent
print(f"[DEBUG] Sending Vectorize Request:")
print(f"[DEBUG] Request Payload: {json.dumps(genConfig, indent=2)}")
genResult = rwUtils.inferenecRequest(genConfig)
# Debug: Print the response received
print(f"[DEBUG] Received Vectorize Response:")
print(f"[DEBUG] Response: {json.dumps(genResult, indent=2)}")
# Check for errors
if "errors" in genResult:
error_message = genResult["errors"][0]["message"]
raise Exception(f"Vectorization failed: {error_message}")
# Extract the result
if "data" in genResult and len(genResult["data"]) > 0:
result_data = genResult["data"][0]
# Check for base64 SVG data
if "imageBase64Data" in result_data:
base64_svg = result_data["imageBase64Data"]
# Decode base64 SVG to get the actual SVG content for SaveSVGNode
svg_content = base64.b64decode(base64_svg).decode('utf-8')
# Wrap SVG content in SVGData for SaveSVGNode compatibility
svg_data = SVGData(svg_content)
# Return only SVG data
return (svg_data,)
else:
raise Exception("imageBase64Data not found in response")
else:
raise Exception("No data returned from vectorization API")
except Exception as e:
print(f"[Error] Vectorization failed: {str(e)}")
raise e
genResult = rwUtils.inferenecRequest(genConfig)
print(f"[DEBUG] Received Vectorize Response:")
print(f"[DEBUG] Response: {json.dumps(genResult, indent=2)}")
self._validateResponse(genResult)
svgData = self._extractSvgData(genResult)
return (svgData,)
def _buildGenConfig(self, model, imageUuid):
"""Build generation configuration for API request"""
return [{
"taskType": "vectorize",
"taskUUID": rwUtils.genRandUUID(),
"model": model,
"inputs": {
"image": imageUuid
},
"outputType": "base64Data",
"outputFormat": "svg",
}]
def _validateResponse(self, genResult):
"""Validate API response"""
if "errors" in genResult:
errorMessage = genResult["errors"][0]["message"]
raise Exception(f"Vectorization failed: {errorMessage}")
if "data" not in genResult or len(genResult["data"]) == 0:
raise Exception("No data returned from vectorization API")
def _extractSvgData(self, genResult):
"""Extract SVG data from API response"""
resultData = genResult["data"][0]
if "imageBase64Data" not in resultData:
raise Exception("imageBase64Data not found in response")
base64Svg = resultData["imageBase64Data"]
svgContent = base64.b64decode(base64Svg).decode('utf-8')
svgData = SVGData(svgContent)
return svgData
# Node class mappings
NODE_CLASS_MAPPINGS = {
@@ -109,4 +108,3 @@ NODE_CLASS_MAPPINGS = {
NODE_DISPLAY_NAME_MAPPINGS = {
"RunwareVectorize": "Runware Vectorize",
}
+68 -77
View File
@@ -2,7 +2,10 @@ from .utils import runwareUtils as rwUtils
import json
import comfy.model_management
class videoBgRemoval:
"""Video Background Removal node for removing video backgrounds"""
RUNWARE_VRMBG_MODELS = {
"bria:51@1": "bria:51@1",
}
@@ -20,7 +23,6 @@ class videoBgRemoval:
"Orange [255, 165, 0, 0]": [255, 165, 0, 0],
}
@classmethod
def INPUT_TYPES(cls):
return {
@@ -52,96 +54,86 @@ class videoBgRemoval:
CATEGORY = "Runware"
def removeBackground(self, **kwargs):
video_uuid = kwargs.get("Video")
"""Remove background from video"""
videoUuid = kwargs.get("Video")
modelName = kwargs.get("Model", "Bria Video Background Removal")
backgroundColor = kwargs.get("Background Color", "Transparent")
outputFormat = kwargs.get("Output Format", "mp4")
# Validate mediaUUID
if not video_uuid or video_uuid.strip() == "":
if not videoUuid or videoUuid.strip() == "":
raise ValueError("Video mediaUUID is required")
# Get the model AIR code
modelAIR = self.RUNWARE_VRMBG_MODELS.get(modelName, "bria:51@1")
modelAir = self.RUNWARE_VRMBG_MODELS.get(modelName, "bria:51@1")
rgbaColor = self.BACKGROUND_COLORS.get(backgroundColor, [255, 255, 255, 0])
# Get background color RGBA
rgba_color = self.BACKGROUND_COLORS.get(backgroundColor, [255, 255, 255, 0])
genConfig = self._buildGenConfig(videoUuid, modelAir, outputFormat, rgbaColor)
# Build the API request
genConfig = [
{
"taskType": "removeBackground",
"taskUUID": rwUtils.genRandUUID(),
"inputs": {
"video": video_uuid
},
"model": modelAIR,
"outputFormat": outputFormat,
"settings": {
"rgba": rgba_color
}
print(f"[DEBUG] Sending Video Background Removal Request:")
print(f"[DEBUG] Request Payload: {json.dumps(genConfig, indent=2)}")
genResult = rwUtils.inferenecRequest(genConfig)
print(f"[DEBUG] Received Video Background Removal Response:")
print(f"[DEBUG] Response: {json.dumps(genResult, indent=2)}")
self._validateResponse(genResult)
taskUuid = genConfig[0]["taskUUID"]
videos = self._pollForVideoResult(taskUuid)
return videos
def _buildGenConfig(self, videoUuid, modelAir, outputFormat, rgbaColor):
"""Build generation configuration for API request"""
return [{
"taskType": "removeBackground",
"taskUUID": rwUtils.genRandUUID(),
"inputs": {
"video": videoUuid
},
"model": modelAir,
"outputFormat": outputFormat,
"settings": {
"rgba": rgbaColor
}
]
try:
# Debug: Print the request being sent
print(f"[DEBUG] Sending Video Background Removal Request:")
print(f"[DEBUG] Request Payload: {json.dumps(genConfig, indent=2)}")
}]
def _validateResponse(self, genResult):
"""Validate API response"""
if "errors" in genResult:
errorMessage = genResult["errors"][0]["message"]
raise Exception(f"Video background removal failed: {errorMessage}")
def _pollForVideoResult(self, taskUuid):
"""Poll for video result"""
while True:
comfy.model_management.throw_exception_if_processing_interrupted()
genResult = rwUtils.inferenecRequest(genConfig)
pollResult = rwUtils.pollVideoResult(taskUuid)
print(f"[Debugging] Poll result: {pollResult}")
# Debug: Print the response received
print(f"[DEBUG] Received Video Background Removal Response:")
print(f"[DEBUG] Response: {json.dumps(genResult, indent=2)}")
if pollResult and "errors" in pollResult and len(pollResult["errors"]) > 0:
errorInfo = pollResult["errors"][0]
errorMessage = errorInfo.get("message", "Unknown error")
raise Exception(f"Video background removal failed: {errorMessage}")
# Check for errors
if "errors" in genResult:
error_message = genResult["errors"][0]["message"]
raise Exception(f"Video background removal failed: {error_message}")
# Extract task UUID for polling
taskUUID = genConfig[0]["taskUUID"]
# Poll for video completion
while True:
# Check for interrupt before each poll
comfy.model_management.throw_exception_if_processing_interrupted()
if pollResult and "data" in pollResult and len(pollResult["data"]) > 0:
videoData = pollResult["data"][0]
# Poll for video result
pollResult = rwUtils.pollVideoResult(taskUUID)
print(f"[Debugging] Poll result: {pollResult}")
# Check for errors first
if pollResult and "errors" in pollResult and len(pollResult["errors"]) > 0:
error_info = pollResult["errors"][0]
error_message = error_info.get("message", "Unknown error")
raise Exception(f"Video background removal failed: {error_message}")
if pollResult and "data" in pollResult and len(pollResult["data"]) > 0:
video_data = pollResult["data"][0]
if "status" in videoData:
status = videoData["status"]
# Check status directly
if "status" in video_data:
status = video_data["status"]
if status == "success":
if "videoURL" in video_data or "videoBase64Data" in video_data:
videos = rwUtils.convertVideoB64List(pollResult, 1920, 1080)
return videos
# If status is "processing", continue polling
# Check for interrupt before waiting
if status == "success":
if "videoURL" in videoData or "videoBase64Data" in videoData:
videos = rwUtils.convertVideoB64List(pollResult, 1920, 1080)
return videos
comfy.model_management.throw_exception_if_processing_interrupted()
for _ in range(10):
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)
except Exception as e:
print(f"[Error] Video background removal failed: {str(e)}")
raise e
rwUtils.time.sleep(0.1)
# Node class mappings
NODE_CLASS_MAPPINGS = {
@@ -151,4 +143,3 @@ NODE_CLASS_MAPPINGS = {
NODE_DISPLAY_NAME_MAPPINGS = {
"RunwareVideoBgRemoval": "Runware Video Background Removal",
}
+11 -13
View File
@@ -71,10 +71,10 @@ class txt2vid:
"default": True,
}),
"seed": ("INT", {
"tooltip": "A value used to randomize the video generation. If you want to make videos reproducible (generate the same video multiple times), you can use the same seed value. Leave empty or 0 to auto-generate.",
"tooltip": "A value used to randomize the video generation. If you want to make videos reproducible (generate the same video multiple times), you can use the same seed value. Set to 0 to auto-generate a random seed.",
"default": 1,
"min": 1,
"max": 9223372036854776000,
"min": 0,
"max": 4294967295,
}),
"useSteps": ("BOOLEAN", {
"tooltip": "Enable to include steps parameter in API request. Disable if your model doesn't support steps.",
@@ -205,11 +205,9 @@ class txt2vid:
width = customWidth
height = customHeight
else:
model_dimensions = videoModelSearch.MODEL_DIMENSIONS.get(model, {"width": 1024, "height": 576})
width = model_dimensions["width"]
height = model_dimensions["height"]
modelDimensions = videoModelSearch.MODEL_DIMENSIONS.get(model, {"width": 1024, "height": 576})
width = modelDimensions["width"]
height = modelDimensions["height"]
genConfig = [
{
@@ -221,7 +219,7 @@ class txt2vid:
"includeCost": True,
}
]
if height!=0 and width!=0:
if height != 0 and width != 0:
genConfig[0]['height'] = height
genConfig[0]['width'] = width
@@ -359,11 +357,11 @@ class txt2vid:
"invalid height" in error_msg.lower() or
"dimension not supported" in error_msg.lower()):
# Get model default dimensions for comparison
model_dimensions = videoModelSearch.MODEL_DIMENSIONS.get(model, {"width": 1024, "height": 576})
expected_width = model_dimensions["width"]
expected_height = model_dimensions["height"]
modelDimensions = videoModelSearch.MODEL_DIMENSIONS.get(model, {"width": 1024, "height": 576})
expectedWidth = modelDimensions["width"]
expectedHeight = modelDimensions["height"]
raise Exception(f"Error: Unsupported width/height combination for this model architecture. You used {width}x{height}, but {model} expects {expected_width}x{expected_height}. Please use 'Model Default' or set custom dimensions to {expected_width}x{expected_height}.")
raise Exception(f"Error: Unsupported width/height combination for this model architecture. You used {width}x{height}, but {model} expects {expectedWidth}x{expectedHeight}. Please use 'Model Default' or set custom dimensions to {expectedWidth}x{expectedHeight}.")
else:
# Re-raise the original error
raise e
+8 -7
View File
@@ -1,6 +1,10 @@
from .utils import runwareUtils as rwUtils
from typing import List
class videoInferenceInputs:
"""Video Inference Inputs node for configuring video generation inputs"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -34,6 +38,7 @@ class videoInferenceInputs:
CATEGORY = "Runware"
def createInputs(self, **kwargs):
"""Create video inference inputs from provided parameters"""
image = kwargs.get("Image", None)
audio = kwargs.get("Audio", None)
video = kwargs.get("Video", None)
@@ -44,8 +49,7 @@ class videoInferenceInputs:
inputs = {}
if image is not None:
image_uuid = rwUtils.convertTensor2IMG(image)
inputs["image"] = image_uuid
inputs["image"] = rwUtils.convertTensor2IMG(image)
if audio is not None and audio.strip() != "":
inputs["audio"] = audio.strip()
@@ -54,15 +58,12 @@ class videoInferenceInputs:
inputs["video"] = video.strip()
if mask is not None:
mask_uuid = rwUtils.convertTensor2IMG(mask)
inputs["mask"] = mask_uuid
inputs["mask"] = rwUtils.convertTensor2IMG(mask)
if frame is not None:
frame_uuid = rwUtils.convertTensor2IMG(frame)
inputs["frame"] = frame_uuid
inputs["frame"] = rwUtils.convertTensor2IMG(frame)
if references is not None and len(references) > 0:
inputs["references"] = references
return (inputs,)
+36 -45
View File
@@ -1,41 +1,29 @@
from .utils import runwareUtils as rwUtils
from typing import List, Dict, Any
class videoInputsReferences:
"""Video Inputs References node for configuring reference images with types"""
MAX_REFERENCES = 4
@classmethod
def INPUT_TYPES(cls):
optionalInputs = {}
for i in range(1, cls.MAX_REFERENCES + 1):
ordinal = rwUtils.getOrdinal(i)
optionalInputs[f"Image{i}"] = ("IMAGE", {
"tooltip": f"{ordinal.capitalize()} reference image.",
})
optionalInputs[f"Type{i}"] = ("STRING", {
"tooltip": f"Type for the {ordinal} reference (e.g., 'asset'). Leave empty to omit.",
"default": "",
})
return {
"required": {},
"optional": {
"Image1": ("IMAGE", {
"tooltip": "First reference image.",
}),
"Type1": ("STRING", {
"tooltip": "Type for the first reference (e.g., 'asset'). Leave empty to omit.",
"default": "",
}),
"Image2": ("IMAGE", {
"tooltip": "Second reference image.",
}),
"Type2": ("STRING", {
"tooltip": "Type for the second reference (e.g., 'asset'). Leave empty to omit.",
"default": "",
}),
"Image3": ("IMAGE", {
"tooltip": "Third reference image.",
}),
"Type3": ("STRING", {
"tooltip": "Type for the third reference (e.g., 'asset'). Leave empty to omit.",
"default": "",
}),
"Image4": ("IMAGE", {
"tooltip": "Fourth reference image.",
}),
"Type4": ("STRING", {
"tooltip": "Type for the fourth reference (e.g., 'asset'). Leave empty to omit.",
"default": "",
}),
}
"optional": optionalInputs
}
DESCRIPTION = "Configure multiple reference images with optional types for video inference inputs."
@@ -45,29 +33,33 @@ class videoInputsReferences:
CATEGORY = "Runware"
def createReferences(self, **kwargs) -> tuple[List[Dict[str, Any]]]:
"""Create list of reference objects from provided parameters"""
references = []
for i in range(1, 5):
image_key = f"Image{i}"
type_key = f"Type{i}"
for i in range(1, self.MAX_REFERENCES + 1):
imageKey = f"Image{i}"
typeKey = f"Type{i}"
image = kwargs.get(image_key)
ref_type = kwargs.get(type_key, "")
image = kwargs.get(imageKey)
refType = kwargs.get(typeKey, "")
if image is not None:
# Convert image tensor to URL
image_url = rwUtils.convertTensor2IMG(image)
reference = {"image": image_url}
# Only add type if it's not empty
if ref_type and ref_type.strip() != "":
reference["type"] = ref_type.strip()
reference = self._createReference(image, refType)
references.append(reference)
return (references,)
def _createReference(self, image, refType):
"""Create reference object from image and type"""
imageUrl = rwUtils.convertTensor2IMG(image)
reference = {"image": imageUrl}
if refType and refType.strip() != "":
reference["type"] = refType.strip()
return reference
# Node class mappings
NODE_CLASS_MAPPINGS = {
"RunwareVideoInputsReferences": videoInputsReferences,
@@ -76,4 +68,3 @@ NODE_CLASS_MAPPINGS = {
NODE_DISPLAY_NAME_MAPPINGS = {
"RunwareVideoInputsReferences": "Runware Video Inputs References",
}
+106 -71
View File
@@ -1,4 +1,6 @@
class videoModelSearch:
"""Video Model Search node for searching and selecting video models"""
# Define the video models by architecture with their default dimensions
VIDEO_MODELS = {
"KlingAI": [
@@ -21,7 +23,7 @@ class videoModelSearch:
"google:3@2 (Veo 3.1)",
"google:3@3 (Veo 3.1 Fast)",
],
"Seedance": [
"Bytedance": [
"bytedance:2@1 (Seedance 1.0 Pro)",
"bytedance:1@1 (Seedance 1.0 Lite)",
"bytedance:5@1 (OmniHuman 1)",
@@ -68,100 +70,103 @@ class videoModelSearch:
# Model dimensions mapping
MODEL_DIMENSIONS = {
# KlingAI Models
"klingai:1@2": {"width": 1280, "height": 720}, # KlingAI V1.0 Pro
"klingai:1@1": {"width": 1280, "height": 720}, # KlingAI V1 Standard
"klingai:2@2": {"width": 1920, "height": 1080}, # KlingAI V1.5 Pro
"klingai:2@1": {"width": 1280, "height": 720}, # KlingAI V1.5 Standard
"klingai:3@1": {"width": 1280, "height": 720}, # KlingAI V1.6 Standard
"klingai:3@2": {"width": 1920, "height": 1080}, # KlingAI V1.6 Pro
"klingai:4@3": {"width": 1280, "height": 720}, # KlingAI V2.1 Master
"klingai:5@1": {"width": 1280, "height": 720}, # KlingAI V2.1 Standard (I2V)
"klingai:5@2": {"width": 1920, "height": 1080}, # KlingAI V2.1 Pro (I2V)
"klingai:5@3": {"width": 1920, "height": 1080}, # KlingAI V2.0 Master
"klingai:7@1": {"width": 0, "height": 0}, # KlingAI V3.0 Standard
"klingai:1@2": {"width": 1280, "height": 720},
"klingai:1@1": {"width": 1280, "height": 720},
"klingai:2@2": {"width": 1920, "height": 1080},
"klingai:2@1": {"width": 1280, "height": 720},
"klingai:3@1": {"width": 1280, "height": 720},
"klingai:3@2": {"width": 1920, "height": 1080},
"klingai:4@3": {"width": 1280, "height": 720},
"klingai:5@1": {"width": 1280, "height": 720},
"klingai:5@2": {"width": 1920, "height": 1080},
"klingai:5@3": {"width": 1920, "height": 1080},
"klingai:7@1": {"width": 0, "height": 0},
# Veo Models
"google:2@0": {"width": 1280, "height": 720}, # Veo 2.0
"google:3@0": {"width": 1280, "height": 720}, # Veo 3.0
"google:3@1": {"width": 1280, "height": 720}, # Veo 3.0 Fast
"google:3@2": {"width": 1280, "height": 720}, # Veo 3.1
"google:3@3": {"width": 1280, "height": 720}, # Veo 3.1 Fast
"google:2@0": {"width": 1280, "height": 720},
"google:3@0": {"width": 1280, "height": 720},
"google:3@1": {"width": 1280, "height": 720},
"google:3@2": {"width": 1280, "height": 720},
"google:3@3": {"width": 1280, "height": 720},
# Seedance Models
"bytedance:2@1": {"width": 864, "height": 480}, # Seedance 1.0 Pro
"bytedance:1@1": {"width": 864, "height": 480}, # Seedance 1.0 Lite
"bytedance:5@1": {"width": 1024, "height": 1024}, # OmniHuman 1
"bytedance:5@2": {"width": 1024, "height": 1024}, # OmniHuman 1.5
# Bytedance Models
"bytedance:2@1": {"width": 864, "height": 480},
"bytedance:1@1": {"width": 864, "height": 480},
"bytedance:5@1": {"width": 1024, "height": 1024},
"bytedance:5@2": {"width": 1024, "height": 1024},
# MiniMax Models
"minimax:1@1": {"width": 1366, "height": 768}, # MiniMax 01 Base
"minimax:2@1": {"width": 1366, "height": 768}, # MiniMax 01 Director
"minimax:2@3": {"width": 1366, "height": 768}, # MiniMax I2V 01 Live
"minimax:3@1": {"width": 1366, "height": 768}, # MiniMax 02 Hailuo
"minimax:4@1": {"width": 1366, "height": 768}, # MiniMax Hailuo 2.3
"minimax:4@2": {"width": 1366, "height": 768}, # MiniMax Hailuo 2.3 Fast
"minimax:1@1": {"width": 1366, "height": 768},
"minimax:2@1": {"width": 1366, "height": 768},
"minimax:2@3": {"width": 1366, "height": 768},
"minimax:3@1": {"width": 1366, "height": 768},
"minimax:4@1": {"width": 1366, "height": 768},
"minimax:4@2": {"width": 1366, "height": 768},
# PixVerse Models
"pixverse:1@1": {"width": 640, "height": 360}, # PixVerse v3.5
"pixverse:1@2": {"width": 640, "height": 360}, # PixVerse v4
"pixverse:1@3": {"width": 640, "height": 360}, # PixVerse v4.5
"pixverse:lipsync@1": {"width": 640, "height": 360}, # PixVerse LipSync
"pixverse:1@1": {"width": 640, "height": 360},
"pixverse:1@2": {"width": 640, "height": 360},
"pixverse:1@3": {"width": 640, "height": 360},
"pixverse:lipsync@1": {"width": 640, "height": 360},
# Vidu Models
"vidu:1@0": {"width": 1920, "height": 1080}, # Vidu Q1 Classic
"vidu:1@1": {"width": 1920, "height": 1080}, # Vidu Q1
"vidu:1@5": {"width": 1920, "height": 1080}, # Vidu 1.5
"vidu:2@0": {"width": 1920, "height": 1080}, # Vidu 2.0
"vidu:1@0": {"width": 1920, "height": 1080},
"vidu:1@1": {"width": 1920, "height": 1080},
"vidu:1@5": {"width": 1920, "height": 1080},
"vidu:2@0": {"width": 1920, "height": 1080},
# Wan Models
"runware:200@1": {"width": 853, "height": 480}, # Wan 2.1 1.3B
"runware:200@2": {"width": 853, "height": 480}, # Wan 2.1 14B
"runware:200@6": {"width": 1280, "height": 720}, # Wan 2.2
"runware:200@1": {"width": 853, "height": 480},
"runware:200@2": {"width": 853, "height": 480},
"runware:200@6": {"width": 1280, "height": 720},
# OpenAI Models
"openai:3@1": {"width": 1280, "height": 720}, # OpenAI Sora 3.1
"openai:3@0": {"width": 1280, "height": 720}, # OpenAI Sora 3.0
"openai:3@1": {"width": 1280, "height": 720},
"openai:3@0": {"width": 1280, "height": 720},
# Lightricks Models
"lightricks:2@0": {"width": 1920, "height": 1080}, # Lightricks v2.0 Fast
"lightricks:2@1": {"width": 1920, "height": 1080}, # Lightricks v2.1 Pro
"lightricks:2@0": {"width": 1920, "height": 1080},
"lightricks:2@1": {"width": 1920, "height": 1080},
# Ovi Models
"runware:190@1": {"width": 0, "height": 0}, # Ovi
"runware:190@1": {"width": 0, "height": 0},
}
MODEL_ARCHITECTURES = [
"All",
"KlingAI",
"Veo",
"Bytedance",
"MiniMax",
"PixVerse",
"Vidu",
"Wan",
"OpenAI",
"Lightricks",
"Ovi"
]
DEFAULT_DIMENSIONS = {"width": 1024, "height": 576}
@classmethod
def INPUT_TYPES(cls):
# Get all models for "All" option
all_models = []
for models in cls.VIDEO_MODELS.values():
all_models.extend(models)
allModels = cls._getAllModels()
defaultModel = "klingai:5@3 (KlingAI V2.0 Master)"
defaultModelCode = defaultModel.split(" (")[0]
defaultDims = cls.MODEL_DIMENSIONS.get(defaultModelCode, cls.DEFAULT_DIMENSIONS)
return {
"required": {
"Model Search": ("STRING", {
"tooltip": "Search For Specific Video Model By Name Or Civit AIR Code (eg: KlingAI, Veo).",
}),
"Model Architecture": ([
"All",
"KlingAI",
"Veo",
"Seedance",
"MiniMax",
"PixVerse",
"Vidu",
"Wan",
"OpenAI",
"Lightricks",
"Ovi"
], {
"Model Architecture": (cls.MODEL_ARCHITECTURES, {
"tooltip": "Choose Video Model Architecture To Filter Results.",
"default": "KlingAI",
}),
"VideoList": (all_models, {
"VideoList": (allModels, {
"tooltip": "Video Model Results Will Show UP Here So You Could Choose From.",
"default": "klingai:5@3 (KlingAI V2.0 Master)",
"default": defaultModel,
}),
"Use Search Value": ("BOOLEAN", {
"tooltip": "When Enabled, the value you've set in the search input will be used instead.\n\nThis is useful in case the model search API is down or you prefer to set the model manually.",
@@ -169,9 +174,31 @@ class videoModelSearch:
"label_on": "Enabled",
"label_off": "Disabled",
}),
"Width": ("INT", {
"default": defaultDims["width"],
"min": 0,
"max": 4096,
"step": 1,
"tooltip": "Custom Width For The Selected Model. Defaults To Model's Recommended Width.",
}),
"Height": ("INT", {
"default": defaultDims["height"],
"min": 0,
"max": 4096,
"step": 1,
"tooltip": "Custom Height For The Selected Model. Defaults To Model's Recommended Height.",
}),
},
}
@classmethod
def _getAllModels(cls):
"""Get all models from all architectures"""
allModels = []
for models in cls.VIDEO_MODELS.values():
allModels.extend(models)
return allModels
DESCRIPTION = "Directly Search and Connect Video Models to Runware Video Inference Nodes In ComfyUI."
FUNCTION = "videoModelSearch"
RETURN_TYPES = ("RUNWAREVIDEOMODEL", "INT", "INT")
@@ -183,19 +210,27 @@ class videoModelSearch:
return True
def videoModelSearch(self, **kwargs):
"""Search and return video model with dimensions"""
enableSearchValue = kwargs.get("Use Search Value", False)
searchInput = kwargs.get("Model Search")
customWidth = kwargs.get("Width", 0)
customHeight = kwargs.get("Height", 0)
if enableSearchValue:
modelAIRCode = searchInput
modelAirCode = searchInput
else:
crModel = kwargs.get("VideoList")
# Extract the full AIR identifier (everything before the first space)
modelAIRCode = crModel.split(" (")[0]
currentModel = kwargs.get("VideoList")
modelAirCode = currentModel.split(" (")[0]
# Get dimensions for the selected model
dimensions = self.MODEL_DIMENSIONS.get(modelAIRCode, {"width": 1024, "height": 576})
dimensions = self.MODEL_DIMENSIONS.get(modelAirCode, self.DEFAULT_DIMENSIONS)
width = customWidth if customWidth > 0 else dimensions["width"]
height = customHeight if customHeight > 0 else dimensions["height"]
if width == 0 and height == 0:
width = None
height = None
return ({
"model": modelAIRCode,
}, dimensions["width"], dimensions["height"])
"model": modelAirCode,
}, width, height)
+2 -1
View File
@@ -1,3 +1,4 @@
python-dotenv
brotli
zstd
zstd
librosa
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
{"id":"5549ed01-3c63-4157-a500-1c576048515a","revision":0,"last_node_id":21,"last_link_id":18,"nodes":[{"id":16,"type":"SaveAudioMP3","pos":[1199.99267578125,-78.82829284667969],"size":[270,136],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"audio","name":"audio","type":"AUDIO","link":18},{"localized_name":"filename_prefix","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null},{"localized_name":"quality","name":"quality","type":"COMBO","widget":{"name":"quality"},"link":null},{"localized_name":"audioUI","name":"audioUI","type":"AUDIO_UI","widget":{"name":"audioUI"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.46","Node name for S&R":"SaveAudioMP3"},"widgets_values":["audio/ComfyUI","V0"]},{"id":18,"type":"Runware Audio Model Search","pos":[222.23391723632812,-129.12527465820312],"size":[270,82],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"searchQuery","name":"searchQuery","type":"STRING","widget":{"name":"searchQuery"},"link":null},{"localized_name":"provider","name":"provider","type":"COMBO","widget":{"name":"provider"},"link":null}],"outputs":[{"localized_name":"model","name":"model","type":"RUNWAREAUDIOMODEL","links":[17]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"5e5e2b3fac2c984692b057e4e607d778a8c73e40","Node name for S&R":"Runware Audio Model Search"},"widgets_values":["elevenlabs:1@1","elevenlabs"]},{"id":21,"type":"Runware Audio Inference","pos":[590.1148681640625,-162.9167938232422],"size":[400,354],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"RUNWAREAUDIOMODEL","link":17},{"localized_name":"providerSettings","name":"providerSettings","shape":7,"type":"RUNWAREPROVIDERSETTINGS","link":null},{"localized_name":"positivePrompt","name":"positivePrompt","type":"STRING","widget":{"name":"positivePrompt"},"link":null},{"localized_name":"usePositivePrompt","name":"usePositivePrompt","type":"BOOLEAN","widget":{"name":"usePositivePrompt"},"link":null},{"localized_name":"negativePrompt","name":"negativePrompt","type":"STRING","widget":{"name":"negativePrompt"},"link":null},{"localized_name":"duration","name":"duration","type":"INT","widget":{"name":"duration"},"link":null},{"localized_name":"useDuration","name":"useDuration","type":"BOOLEAN","widget":{"name":"useDuration"},"link":null},{"localized_name":"sampleRate","name":"sampleRate","type":"INT","widget":{"name":"sampleRate"},"link":null},{"localized_name":"bitrate","name":"bitrate","type":"INT","widget":{"name":"bitrate"},"link":null},{"localized_name":"outputType","name":"outputType","type":"COMBO","widget":{"name":"outputType"},"link":null},{"localized_name":"outputFormat","name":"outputFormat","type":"COMBO","widget":{"name":"outputFormat"},"link":null},{"localized_name":"numberResults","name":"numberResults","type":"INT","widget":{"name":"numberResults"},"link":null}],"outputs":[{"localized_name":"audio","name":"audio","type":"AUDIO","links":[18]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"5583f01fabae66b237ccd315242dbc74f1d110d6","Node name for S&R":"Runware Audio Inference"},"widgets_values":["classical piano piece, gentle and melodic",true,"",10,true,22050,32,"URL","MP3",1]}],"links":[[17,18,0,21,0,"RUNWAREAUDIOMODEL"],[18,21,0,16,0,"AUDIO"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.6830134553650705,"offset":[857.6627752576644,624.9404459140089]}},"version":0.4}
{"id":"5549ed01-3c63-4157-a500-1c576048515a","revision":0,"last_node_id":22,"last_link_id":20,"nodes":[{"id":16,"type":"SaveAudioMP3","pos":[1199.99267578125,-78.82829284667969],"size":[270,136],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"audio","name":"audio","type":"AUDIO","link":19},{"localized_name":"filename_prefix","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null},{"localized_name":"quality","name":"quality","type":"COMBO","widget":{"name":"quality"},"link":null},{"localized_name":"audioUI","name":"audioUI","type":"AUDIO_UI","widget":{"name":"audioUI"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.46","Node name for S&R":"SaveAudioMP3"},"widgets_values":["audio/ComfyUI","V0"]},{"id":22,"type":"Runware Audio Inference","pos":[626.7266845703125,-157.00523376464844],"size":[400,354],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"RUNWAREAUDIOMODEL","link":20},{"localized_name":"providerSettings","name":"providerSettings","shape":7,"type":"RUNWAREPROVIDERSETTINGS","link":null},{"localized_name":"usePositivePrompt","name":"usePositivePrompt","type":"BOOLEAN","widget":{"name":"usePositivePrompt"},"link":null},{"localized_name":"positivePrompt","name":"positivePrompt","type":"STRING","widget":{"name":"positivePrompt"},"link":null},{"localized_name":"negativePrompt","name":"negativePrompt","type":"STRING","widget":{"name":"negativePrompt"},"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":"sampleRate","name":"sampleRate","type":"INT","widget":{"name":"sampleRate"},"link":null},{"localized_name":"bitrate","name":"bitrate","type":"INT","widget":{"name":"bitrate"},"link":null},{"localized_name":"outputType","name":"outputType","type":"COMBO","widget":{"name":"outputType"},"link":null},{"localized_name":"outputFormat","name":"outputFormat","type":"COMBO","widget":{"name":"outputFormat"},"link":null},{"localized_name":"numberResults","name":"numberResults","type":"INT","widget":{"name":"numberResults"},"link":null}],"outputs":[{"localized_name":"audio","name":"audio","type":"AUDIO","links":[19]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"b75f0803ad2a5b5624f97d4025927744329e0c31","Node name for S&R":"Runware Audio Inference"},"widgets_values":[true,"classical piano piece, gentle and melodic","",true,10,22050,32,"URL","MP3",1],"bgcolor":"#5345bf"},{"id":18,"type":"Runware Audio Model Search","pos":[222.23391723632812,-129.12527465820312],"size":[270,82],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"searchQuery","name":"searchQuery","type":"STRING","widget":{"name":"searchQuery"},"link":null},{"localized_name":"provider","name":"provider","type":"COMBO","widget":{"name":"provider"},"link":null}],"outputs":[{"localized_name":"model","name":"model","type":"RUNWAREAUDIOMODEL","links":[20]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"5e5e2b3fac2c984692b057e4e607d778a8c73e40","Node name for S&R":"Runware Audio Model Search"},"widgets_values":["elevenlabs:1@1","elevenlabs"],"bgcolor":"#5345bf"}],"links":[[19,22,0,16,0,"AUDIO"],[20,18,0,22,0,"RUNWAREAUDIOMODEL"]],"groups":[],"config":{},"extra":{"ds":{"scale":1.2401314071374674,"offset":[-0.940790812174555,389.1820880323489]}},"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
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
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
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
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
{"id":"7493d64d-0c0c-40ca-a1df-816552f0526b","revision":0,"last_node_id":12,"last_link_id":9,"nodes":[{"id":11,"type":"Runware Media Upload","pos":[448.4990234375,623.826416015625],"size":[270,58],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"media","name":"media","shape":7,"type":"VIDEO,AUDIO","link":9},{"localized_name":"mediaUUID","name":"mediaUUID","shape":7,"type":"STRING","widget":{"name":"mediaUUID"},"link":null}],"outputs":[{"localized_name":"mediaUUID","name":"mediaUUID","type":"STRING","links":null}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"1c7da18f11d51514140944e0f9f90e62ae99572c","Node name for S&R":"Runware Media Upload"},"widgets_values":["f61620ac-27aa-4bee-9795-0df76fb2b0ce"],"bgcolor":"#5345bf"},{"id":12,"type":"LoadAudio","pos":[133.54676818847656,597.8778686523438],"size":[274.080078125,136],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"audio","name":"audio","type":"COMBO","widget":{"name":"audio"},"link":null},{"localized_name":"audioUI","name":"audioUI","type":"AUDIO_UI","widget":{"name":"audioUI"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"AUDIOUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"AUDIO","name":"AUDIO","type":"AUDIO","links":[9]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.46","Node name for S&R":"LoadAudio"},"widgets_values":["president_speech_6sec.mp3",null,""]}],"links":[[9,12,0,11,0,"VIDEO,AUDIO"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.8264462809917354,"offset":[453.6411562800821,-175.4364195346344]}},"version":0.4}
{"id":"7493d64d-0c0c-40ca-a1df-816552f0526b","revision":0,"last_node_id":12,"last_link_id":9,"nodes":[{"id":11,"type":"Runware Media Upload","pos":[448.4990234375,623.826416015625],"size":[270,58],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"media","name":"media","shape":7,"type":"VIDEO,AUDIO","link":9},{"localized_name":"mediaUUID","name":"mediaUUID","shape":7,"type":"STRING","widget":{"name":"mediaUUID"},"link":null}],"outputs":[{"localized_name":"mediaUUID","name":"mediaUUID","type":"STRING","links":null}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"1c7da18f11d51514140944e0f9f90e62ae99572c","Node name for S&R":"Runware Media Upload"},"widgets_values":["f61620ac-27aa-4bee-9795-0df76fb2b0ce"],"bgcolor":"#5345bf"},{"id":12,"type":"LoadAudio","pos":[133.54676818847656,597.8778686523438],"size":[274.080078125,136],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"audio","name":"audio","type":"COMBO","widget":{"name":"audio"},"link":null},{"localized_name":"audioUI","name":"audioUI","type":"AUDIO_UI","widget":{"name":"audioUI"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"AUDIOUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"AUDIO","name":"AUDIO","type":"AUDIO","links":[9]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.46","Node name for S&R":"LoadAudio"},"widgets_values":["president_speech_6sec.mp3",null,null]}],"links":[[9,12,0,11,0,"VIDEO,AUDIO"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.8264462809917354,"offset":[453.6411562800821,-175.4364195346344]}},"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
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"id":"df246f14-f1cb-4379-91af-5185b0c663d8","revision":0,"last_node_id":4,"last_link_id":2,"nodes":[{"id":4,"type":"Runware Media Upload","pos":[-151.37985229492188,276.1902160644531],"size":[270,58],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"media","name":"media","shape":7,"type":"VIDEO,AUDIO","link":1},{"localized_name":"mediaUUID","name":"mediaUUID","shape":7,"type":"STRING","widget":{"name":"mediaUUID"},"link":null}],"outputs":[{"localized_name":"mediaUUID","name":"mediaUUID","type":"STRING","links":[2]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"056c15a1b72b1555184d0447562c095797825d6e","Node name for S&R":"Runware Media Upload"},"widgets_values":["f4d54ced-fe68-45d5-9cb8-ba7b29729fe9"],"bgcolor":"#5345bf"},{"id":3,"type":"LoadVideo","pos":[-436.6539001464844,430.0327453613281],"size":[274.080078125,442.0882263183594],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"file","name":"file","type":"COMBO","widget":{"name":"file"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"VIDEO","name":"VIDEO","type":"VIDEO","links":[1]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.46","Node name for S&R":"LoadVideo"},"widgets_values":["b395d4bb-364c-44b5-91ba-f768e5ff3e98.mp4","image"]},{"id":1,"type":"Runware Video Transcription","pos":[-75.60359191894531,496.2238464355469],"size":[400,200],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"Always Retranscribe","name":"Always Retranscribe","type":"BOOLEAN","widget":{"name":"Always Retranscribe"},"link":null},{"localized_name":"prompt","name":"prompt","type":"STRING","widget":{"name":"prompt"},"link":null},{"localized_name":"video","name":"video","type":"STRING","widget":{"name":"video"},"link":2},{"localized_name":"model","name":"model","type":"STRING","widget":{"name":"model"},"link":null}],"outputs":[{"localized_name":"Video Transcription","name":"Video Transcription","type":"STRING","links":null}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"056c15a1b72b1555184d0447562c095797825d6e","Node name for S&R":"Runware Video Transcription"},"widgets_values":[false,"[0] (0s-1s): A man with curly brown hair and closed eyes is shown against a plain gray background. He is wearing a dark blue t-shirt.\n[1] (1s-3s): The man opens his eyes and begins to speak, his mouth slightly open.\n[2] (3s-5s): He continues to speak, his expression animated.\n[3] (5s-7s): The man purses his lips as if about to speak or make a sound.\n[4] (7s-9s): He speaks again, his mouth open and teeth visible.\n[5] (9s-10s): The man's expression is neutral as he looks forward.\n[6] (10s-11s): He blinks slowly, his eyes partially closed.\n[7] (11s-13s): The man sticks out his tongue slightly and speaks.\n[8] (13s-14s): He speaks with his mouth open, showing his teeth.","","memories:2@1"]}],"links":[[1,3,0,4,0,"VIDEO,AUDIO"],[2,4,0,1,2,"STRING"]],"groups":[],"config":{},"extra":{"ds":{"scale":1.118153876706717,"offset":[993.4996947608385,-128.5350901285807]}},"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
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
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