Merge pull request #17 from Runware/feature-update

Adding audio inference, image captioning improvements
This commit is contained in:
Sirshendu Ganguly
2025-09-22 12:24:05 -04:00
committed by GitHub
15 changed files with 941 additions and 351 deletions
Vendored
BIN
View File
Binary file not shown.
+12 -1
View File
@@ -32,8 +32,13 @@ from .modules.providerSettings import RunwareProviderSettings
from .modules.pixverseProviderSettings import RunwarePixverseProviderSettings
from .modules.openaiProviderSettings import RunwareOpenAIProviderSettings
from .modules.bytedanceImageProviderSettings import RunwareBytedanceProviderSettings
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
RUNWARE_COMFYUI_VERSION = "1.3.0 Beta"
RUNWARE_COMFYUI_VERSION = "1.4.0 Beta"
RESET_COLOR = "\033[0m"
BLUE_COLOR = "\033[94m"
@@ -78,6 +83,11 @@ NODE_CLASS_MAPPINGS = {
"Runware Pixverse Provider Settings": RunwarePixverseProviderSettings,
"Runware OpenAI Provider Settings": RunwareOpenAIProviderSettings,
"Runware Bytedance Provider Settings": RunwareBytedanceProviderSettings,
"Runware Audio Inference": RunwareAudioInference,
"Runware Audio Model Search": RunwareAudioModelSearch,
"Runware Audio Sections": RunwareAudioSections,
"Runware ElevenLabs Provider Settings": RunwareElevenLabsProviderSettings,
"Runware ElevenLabs Provider Settings Sections": RunwareElevenLabsProviderSettingsSections,
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -87,6 +97,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"Runware VAE Search": "Runware VAE",
"Runware Multi Inference": "Runware Multi Inference [BETA]",
"Runware Video Model Search": "Runware Video Model",
"Runware Audio Model Search": "Runware Audio Model",
}
WEB_DIRECTORY = "./clientlibs"
+50
View File
@@ -76,6 +76,16 @@ const RUNWARE_NODE_TYPES = {
REFERENCEIMAGES: "Runware Reference Images",
VIDEOINFERENCE: "Runware Video Inference",
VIDEOMODELSEARCH: "Runware Video Model Search",
FRAMEIMAGES: "Runware Frame Images",
AUDIOINFERENCE: "Runware Audio Inference",
AUDIOMODELSEARCH: "Runware Audio Model Search",
AUDIOSECTIONS: "Runware Audio Sections",
PROVIDERSETTINGS: "Runware Provider Settings",
PIXVERSEPROVIDERSETTINGS: "Runware Pixverse Provider Settings",
OPENAIPROVIDERSETTINGS: "Runware OpenAI Provider Settings",
BYTEDANCEPROVIDERSETTINGS: "Runware Bytedance Provider Settings",
ELEVENLABSPROVIDERSETTINGS: "Runware ElevenLabs Provider Settings",
ELEVENLABSPROVIDERSETTINGSSECTIONS: "Runware ElevenLabs Provider Settings Sections",
};
const RUNWARE_NODE_PROPS = {
@@ -198,6 +208,46 @@ const RUNWARE_NODE_PROPS = {
bgColor: DEFAULT_BGCOLOR,
liveSearch: true,
},
[RUNWARE_NODE_TYPES.FRAMEIMAGES]: {
bgColor: DEFAULT_BGCOLOR,
colorModeOnly: true,
},
[RUNWARE_NODE_TYPES.AUDIOINFERENCE]: {
bgColor: DEFAULT_BGCOLOR,
promptEnhancer: true,
},
[RUNWARE_NODE_TYPES.AUDIOMODELSEARCH]: {
bgColor: DEFAULT_BGCOLOR,
liveSearch: true,
},
[RUNWARE_NODE_TYPES.AUDIOSECTIONS]: {
bgColor: DEFAULT_BGCOLOR,
colorModeOnly: true,
},
[RUNWARE_NODE_TYPES.PROVIDERSETTINGS]: {
bgColor: DEFAULT_BGCOLOR,
colorModeOnly: true,
},
[RUNWARE_NODE_TYPES.PIXVERSEPROVIDERSETTINGS]: {
bgColor: DEFAULT_BGCOLOR,
colorModeOnly: true,
},
[RUNWARE_NODE_TYPES.OPENAIPROVIDERSETTINGS]: {
bgColor: DEFAULT_BGCOLOR,
colorModeOnly: true,
},
[RUNWARE_NODE_TYPES.BYTEDANCEPROVIDERSETTINGS]: {
bgColor: DEFAULT_BGCOLOR,
colorModeOnly: true,
},
[RUNWARE_NODE_TYPES.ELEVENLABSPROVIDERSETTINGS]: {
bgColor: DEFAULT_BGCOLOR,
colorModeOnly: true,
},
[RUNWARE_NODE_TYPES.ELEVENLABSPROVIDERSETTINGSSECTIONS]: {
bgColor: DEFAULT_BGCOLOR,
colorModeOnly: true,
},
};
export {
+7 -3
View File
@@ -44,11 +44,15 @@ function captionNodeHandler(msgEvent) {
const captionData = msgEvent.detail;
const captionText = captionData.captionText;
const captionNodeID = parseInt(captionData.nodeID);
if(captionData.success) {
const captionNode = app.graph.getNodeById(captionNodeID);
if(captionNode !== null || captionNode !== undefined) {
const captionInput = captionNode.widgets[1].inputEl;
captionInput.value = captionText;
if(captionNode !== null && captionNode !== undefined) {
// Find the imageCaption widget specifically
const imageCaptionWidget = captionNode.widgets.find(widget => widget.name === "imageCaption");
if(imageCaptionWidget && imageCaptionWidget.inputEl) {
imageCaptionWidget.inputEl.value = captionText;
}
}
}
return false;
+232
View File
@@ -0,0 +1,232 @@
import json
import uuid
import requests
import torch
import torchaudio
import numpy as np
from io import BytesIO
import tempfile
import os
from .utils import runwareUtils as rwUtils
class RunwareAudioInference:
def __init__(self):
pass
@classmethod
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"
}),
"usePositivePrompt": ("BOOLEAN", {
"default": True,
"tooltip": "Enable/disable positivePrompt parameter in API request"
}),
"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"
}),
"duration": ("INT", {
"default": 10,
"min": 10,
"max": 300,
"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,
"max": 48000,
"step": 1,
"tooltip": "Sample rate of the generated audio in Hz (8000-48000)"
}),
"bitrate": ("INT", {
"default": 32,
"min": 32,
"max": 320,
"step": 32,
"tooltip": "Bitrate of the generated audio in kbps (32-320)"
}),
"outputType": (["URL", "dataURI", "base64Data"], {
"default": "URL",
"tooltip": "Output type for the generated audio"
}),
"outputFormat": (["MP3"], {
"default": "MP3",
"tooltip": "Format of the output audio"
}),
"numberResults": ("INT", {
"default": 1,
"min": 1,
"max": 3,
"tooltip": "Number of audio files to generate"
}),
},
"optional": {
"providerSettings": ("RUNWAREPROVIDERSETTINGS", {
"tooltip": "Provider-specific configuration settings"
}),
}
}
RETURN_TYPES = ("AUDIO",)
RETURN_NAMES = ("audio",)
FUNCTION = "generateAudio"
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)
# Create task UUID
taskUUID = str(uuid.uuid4())
# Build the generation config
genConfig = [{
"taskType": "audioInference",
"taskUUID": taskUUID,
"model": model,
"outputType": outputType,
"outputFormat": outputFormat,
"deliveryMethod": "sync",
"includeCost": True,
"numberResults": numberResults,
"audioSettings": {
"sampleRate": sampleRate,
"bitrate": 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
# 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})")
else:
print(f"[DEBUG] Skipped positivePrompt (usePositivePrompt={usePositivePrompt})")
# Add duration only if useDuration is True
if useDuration:
genConfig[0]["duration"] = duration
print(f"[DEBUG] Added duration: {duration} (useDuration={useDuration})")
else:
print(f"[DEBUG] Skipped duration (useDuration={useDuration})")
# Add optional parameters if provided
if negativePrompt:
genConfig[0]["negativePrompt"] = negativePrompt
if providerSettings is not None:
genConfig[0]["providerSettings"] = 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
print(f"[DEBUG] Received Audio Inference 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"Audio generation failed: {error_message}")
# 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:
raise Exception("No audio data received from API")
+31
View File
@@ -0,0 +1,31 @@
class RunwareAudioModelSearch:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"searchQuery": ("STRING", {
"default": "elevenlabs:1@1",
"tooltip": "Search for audio models by name, provider, or capabilities"
}),
"provider": (["elevenlabs"], {
"default": "elevenlabs",
"tooltip": "Currently only ElevenLabs is supported"
}),
}
}
RETURN_TYPES = ("RUNWAREAUDIOMODEL",)
RETURN_NAMES = ("model",)
FUNCTION = "search_models"
CATEGORY = "Runware/Audio"
def search_models(self, **kwargs):
searchQuery = kwargs.get("searchQuery", "")
provider = kwargs.get("provider", "elevenlabs")
return (searchQuery,)
+56
View File
@@ -0,0 +1,56 @@
class RunwareAudioSections:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"sectionName": ("STRING", {
"default": "intro",
"tooltip": "Name of the section"
}),
"positiveLocalStyles": ("STRING", {
"default": "guitar, buildup",
"tooltip": "Styles that should be present in this section (comma-separated)"
}),
"negativeLocalStyles": ("STRING", {
"default": "vocals",
"tooltip": "Styles that should not be present in this section (comma-separated)"
}),
"duration": ("INT", {
"default": 15,
"min": 3,
"max": 120,
"tooltip": "Duration of the section in seconds (3-120)"
}),
"lines": ("STRING", {
"multiline": True,
"default": "",
"tooltip": "Lyrics for the section (one line per line)"
}),
}
}
RETURN_TYPES = ("RUNWAREAUDIOSECTIONS",)
RETURN_NAMES = ("sections",)
FUNCTION = "create_sections"
CATEGORY = "Runware/Audio"
def create_sections(self, **kwargs):
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()],
"duration": duration,
"lines": [line.strip() for line in lines.split("\n") if line.strip()]
}
return (section,)
+67
View File
@@ -0,0 +1,67 @@
class RunwareElevenLabsProviderSettings:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"positiveGlobalStyles": ("STRING", {
"multiline": True,
"default": "rock, energetic",
"tooltip": "Global positive styles for the entire song (comma-separated): positiveGlobalStyles"
}),
"negativeGlobalStyles": ("STRING", {
"multiline": True,
"default": "slow, quiet",
"tooltip": "Global negative styles for the entire song (comma-separated): negativeGlobalStyles"
}),
},
"optional": {
"sections": ("RUNWAREAUDIOSECTIONS", {
"tooltip": "Connect multiple Runware Audio Sections nodes to define song structure"
}),
}
}
RETURN_TYPES = ("RUNWAREPROVIDERSETTINGS",)
RETURN_NAMES = ("providerSettings",)
FUNCTION = "create_provider_settings"
CATEGORY = "Runware/Audio"
def create_provider_settings(self, **kwargs):
positiveGlobalStyles = kwargs.get("positiveGlobalStyles", "")
negativeGlobalStyles = kwargs.get("negativeGlobalStyles", "")
sections = kwargs.get("sections", [])
# Parse styles from comma-separated strings
positiveStyles = [style.strip() for style in positiveGlobalStyles.split(",") if style.strip()]
negativeStyles = [style.strip() for style in negativeGlobalStyles.split(",") if style.strip()]
# Handle sections - can be single section or list of sections
sections_list = []
if sections:
if isinstance(sections, list):
sections_list = sections
else:
sections_list = [sections]
# Build the music composition plan
music = {
"compositionPlan": {
"positiveGlobalStyles": positiveStyles,
"negativeGlobalStyles": negativeStyles,
"sections": sections_list
}
}
# Create provider settings
providerSettings = {
"elevenlabs": {
"music": music
}
}
return (providerSettings,)
@@ -0,0 +1,56 @@
class RunwareElevenLabsProviderSettingsSections:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"sectionName": ("STRING", {
"default": "intro",
"tooltip": "Name of the section"
}),
"positiveLocalStyles": ("STRING", {
"default": "guitar, buildup",
"tooltip": "Local positive styles for this section (comma-separated): positiveLocalStyles"
}),
"negativeLocalStyles": ("STRING", {
"default": "vocals",
"tooltip": "Local negative styles for this section (comma-separated): negativeLocalStyles"
}),
"duration": ("INT", {
"default": 15,
"min": 3,
"max": 120,
"tooltip": "Duration of the section in seconds (3-120)"
}),
"lines": ("STRING", {
"multiline": True,
"default": "",
"tooltip": "Lyrics for the section (one line per line)"
}),
}
}
RETURN_TYPES = ("RUNWAREAUDIOSECTIONS",)
RETURN_NAMES = ("sections",)
FUNCTION = "create_sections"
CATEGORY = "Runware/Audio"
def create_sections(self, **kwargs):
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()],
"duration": duration,
"lines": [line.strip() for line in lines.split("\n") if line.strip()]
}
return (section,)
+93 -14
View File
@@ -11,20 +11,40 @@ class imageCaptioning:
"label_on": "Enabled",
"label_off": "Disabled",
}),
"Image": ("IMAGE", {
"tooltip": "Specify The Image to be Captioned."
"prompt": ("STRING", {
"multiline": True,
"default": "Describe this image in detail",
"placeholder": "Enter the prompt for image captioning. You can provide multiple prompts separated by newlines.",
"tooltip": "The prompt(s) to guide the image captioning. Multiple prompts can be provided, each on a new line."
}),
},
"optional": {
"Image Description": ("STRING", {
"inputImages": ("IMAGE", {
"tooltip": "Connect multiple Load Image nodes here to caption multiple images at once."
}),
"inputImage": ("IMAGE", {
"tooltip": "Connect a single Load Image node here for convenience. If inputImages is provided, this will be ignored."
}),
"model": ("STRING", {
"default": "",
"placeholder": "Leave empty for default model (runware:150@1)",
"tooltip": "Optional: Specify the AIR model ID (e.g., runware:150@1, runware:150@2). Leave empty to use the default model."
}),
"template": ("STRING", {
"default": "",
"placeholder": "Optional template for structured output",
"tooltip": "Optional template to structure the caption output."
}),
"imageCaption": ("STRING", {
"multiline": True,
"placeholder": "You Don't Have to write Anything here.\nThe Image Description Will Be Generated Automatically.",
"placeholder": "Generated image caption will appear here automatically.",
"tooltip": "This field will be automatically populated with the generated image caption."
}),
},
"hidden": { "node_id": "UNIQUE_ID" }
}
DESCRIPTION = "Image to text, also known as image captioning, allows you to obtain descriptive text prompts based on uploaded or previously generated images. This process is instrumental in generating textual descriptions that can be used to create additional images or provide detailed insights into visual content."
DESCRIPTION = "Advanced image captioning with support for multiple images, custom prompts, and model selection. Generate descriptive text from single or multiple images using customizable prompts and optional AIR models. Perfect for batch processing and structured output generation."
FUNCTION = "imageCaptioning"
RETURN_TYPES = ("STRING",)
@@ -40,16 +60,75 @@ class imageCaptioning:
return True
def imageCaptioning(self, **kwargs):
image = kwargs.get("Image")
genConfig = [
{
"taskType": "imageCaption",
"taskUUID": rwUtils.genRandUUID(),
"inputImage": rwUtils.convertTensor2IMG(image)
}
]
inputImages = kwargs.get("inputImages", None)
inputImage = kwargs.get("inputImage", None)
prompt = kwargs.get("prompt", "Describe this image in detail")
model = kwargs.get("model", "")
template = kwargs.get("template", "")
# Handle multiple prompts - split by newlines
prompt_list = [p.strip() for p in prompt.split('\n') if p.strip()]
if not prompt_list:
prompt_list = ["Describe this image in detail"]
# Determine which images to use (following SDK logic)
if inputImages is not None:
images_to_process = inputImages
elif inputImage is not None:
# Single image provided via inputImage - convert to array
images_to_process = [inputImage]
else:
raise Exception("Either inputImages or inputImage must be provided")
# Set inputImage to inputImages[0] if not already provided
actual_input_image = inputImage
if actual_input_image is None and len(images_to_process) > 0:
actual_input_image = images_to_process[0]
# Convert images to base64 format
converted_images = []
for img in images_to_process:
converted_images.append(rwUtils.convertTensor2IMG(img))
# Create a dictionary with mandatory parameters
task_params = {
"taskType": "imageCaption",
"taskUUID": rwUtils.genRandUUID(),
}
# Add either inputImage or inputImages, but not both (API requirement)
if len(converted_images) == 1:
# Single image - use inputImage parameter
task_params["inputImage"] = converted_images[0]
else:
# Multiple images - use inputImages parameter
task_params["inputImages"] = converted_images
# Add model parameter only if specified - backend handles default
if model and model.strip():
task_params["model"] = model.strip()
# Add template parameter if specified
if template and template.strip():
task_params["template"] = template.strip()
# When using template, do NOT include prompt parameter
else:
# Use the provided prompt when no template
task_params["prompt"] = prompt_list
# Send the task with all applicable parameters
genConfig = [task_params]
genResult = rwUtils.inferenecRequest(genConfig)
genText = genResult["data"][0]["text"]
print(genResult)
# Handle multiple results if multiple images were processed
if len(genResult["data"]) > 1:
# Multiple images - combine results
combined_text = "\n\n".join([item["text"] for item in genResult["data"]])
genText = combined_text
else:
# Single image
genText = genResult["data"][0]["text"]
rwUtils.sendImageCaption(genText, kwargs.get("node_id"))
return (genText, )
+332 -332
View File
@@ -1,332 +1,332 @@
from .utils import runwareUtils as rwUtils
import json
class txt2img:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"Model": ("RUNWAREMODEL", {
"tooltip": "Connect a Runware Model From Runware Model Node.",
}),
"positivePrompt": ("STRING", {
"multiline": True,
"placeholder": "Positive Prompt: a text instruction to guide the model on generating the image. It is usually a sentence or a paragraph that provides positive guidance for the task. This parameter is essential to shape the desired results.\n\nYou Can Press (Ctrl + Alt + E) To Enhance The Prompt!",
"tooltip": "Positive Prompt: a text instruction to guide the model on generating the image. You Can Also Press (Ctrl + Alt + E) To Enhance The Prompt!"
}),
"negativePrompt": ("STRING", {
"multiline": True,
"placeholder": "Negative Prompt: a text instruction to guide the model on generating the image. It is usually a sentence or a paragraph that provides negative guidance for the task. This parameter helps to avoid certain undesired results.",
"tooltip": "Negative Prompt: a text instruction to guide the model on generating the image."
}),
"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.",
"default": False,
"label_on": "Enabled",
"label_off": "Disabled",
}),
"Prompt Weighting": (["Disabled", "sdEmbeds", "Compel"], {
"default": "Disabled",
"tooltip": "Prompt weighting allows you to adjust how strongly different parts of your prompt influence the generated image.\n\nChoose between \"compel\" notation with advanced weighting operations or \"sdEmbeds\" for simple emphasis adjustments.\n\nCompel Example: \"small+ dog, pixar style\"\n\nsdEmbeds Example: \"(small:2.5) dog, pixar style\"",
}),
"dimensions": ([
"None", "Square (512x512)", "Square HD (1024x1024)", "Portrait 3:4 (768x1024)",
"Portrait 9:16 (576x1024)", "Landscape 4:3 (1024x768)",
"Landscape 16:9 (1024x576)", "Custom"
], {
"default": "Square (512x512)",
"tooltip": "Adjust the dimensions of the generated image by specifying its width and height in pixels, or select from the predefined options. Image dimensions must be multiples of 64 (e.g., 512x512, 1024x768). Select 'None' to let the model determine dimensions automatically.",
}),
"width": ("INT", {
"tooltip": "The Width of the image in pixels.",
"default": 512,
"min": 128,
"max": 2048,
"step": 64,
}),
"height": ("INT", {
"tooltip": "The Height of the image in pixels.",
"default": 512,
"min": 128,
"max": 2048,
"step": 64,
}),
"useSteps": ("BOOLEAN", {
"tooltip": "Enable to include steps parameter in API request. Disable if your model doesn't support steps (like nano banana).",
"default": True,
}),
"steps": ("INT", {
"tooltip": "The number of steps is the number of iterations the model will perform to generate the image. Only used when 'Use Steps' is enabled.",
"default": 4,
"min": 1,
"max": 100,
}),
"useScheduler": ("BOOLEAN", {
"tooltip": "Enable to include scheduler parameter in API request. Disable if your model doesn't support scheduler.",
"default": True,
}),
"scheduler": (['Default', 'DDIM', 'DDIMScheduler', 'DDPMScheduler', 'DEISMultistepScheduler', 'DPMSolverSinglestepScheduler', 'DPMSolverMultistepScheduler', 'DPMSolverMultistepInverse', 'DPM++', 'DPM++ Karras', 'DPM++ 2M', 'DPM++ 2M Karras', 'DPM++ 2M SDE Karras', 'DPM++ 2M SDE', 'DPM++ 3M', 'DPM++ 3M Karras', 'DPM++ SDE Karras', 'DPM++ SDE', 'EDMEulerScheduler', 'EDMDPMSolverMultistepScheduler', 'Euler', 'EulerDiscreteScheduler', 'Euler Karras', 'Euler a', 'EulerAncestralDiscreteScheduler', 'FlowMatchEulerDiscreteScheduler', 'Heun', 'HeunDiscreteScheduler', 'Heun Karras', 'IPNDMScheduler', 'KDPM2DiscreteScheduler', 'KDPM2AncestralDiscreteScheduler', 'LCM', 'LCMScheduler', 'LMS', 'LMSDiscreteScheduler', 'LMS Karras', 'PNDMScheduler', 'TCDScheduler', 'UniPC', 'UniPCMultistepScheduler', 'UniPC Karras', 'UniPC 2M', 'UniPC 2M Karras', 'UniPC 3M', 'UniPC 3M Karras'], {
"tooltip": "An scheduler is a component that manages the inference process. Different schedulers can be used to achieve different results like more detailed images, faster inference, or more accurate results.",
"default": "Default",
}),
"useCFGScale": ("BOOLEAN", {
"tooltip": "Enable to include CFG scale parameter in API request. Disable if your model doesn't support CFG scale (like nano banana).",
"default": True,
}),
"cfgScale": ("FLOAT", {
"tooltip": "Guidance scale represents how closely the images will resemble the prompt or how much freedom the AI model has. Only used when 'Use CFG Scale' is enabled.",
"default": 6.5,
"min": 1.0,
"max": 50.0,
"step": 0.5,
}),
"useSeed": ("BOOLEAN", {
"tooltip": "Enable to include seed parameter in API request. Disable if your model doesn't support seed.",
"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.",
"default": rwUtils.genRandSeed(),
"min": 1,
"max": 9223372036854776000,
}),
"useClipSkip": ("BOOLEAN", {
"tooltip": "Enable to include clipSkip parameter in API request. Disable if your model doesn't support clipSkip.",
"default": True,
}),
"clipSkip": ("INT", {
"tooltip": "Enables skipping layers of the CLIP embedding process, leading to quicker and more varied image generation. Only used when 'Use Clip Skip' is enabled.",
"default": 0,
"min": 0,
"max": 2,
}),
"strength": ("FLOAT", {
"tooltip": "When doing Image-to-Image or Inpainting, this parameter is used to determine the influence of the seedImage image in the generated output. A lower value results in more influence from the original image, while a higher value allows more creative deviation.",
"default": 0.8,
"min": 0.0,
"max": 1.0,
"step": 0.01,
}),
"Mask Margin": ("BOOLEAN", {
"tooltip": "Enables Or Disables Mask Margin Feature.",
"default": False,
"label_on": "Enabled",
"label_off": "Disabled",
}),
"maskMargin": ("INT", {
"tooltip": "Adds extra context pixels around the masked region during inpainting. When this parameter is present, the model will zoom into the masked area, considering these additional pixels to create more coherent and well-integrated details.",
"default": 32,
"min": 32,
"max": 128,
}),
"batchSize": ("INT", {
"tooltip": "The number of images to generate in a single request.",
"default": 1,
"min": 1,
"max": 10,
}),
},
"optional": {
"Accelerator": ("RUNWAREACCELERATOR", {
"tooltip": "Connect a Runware Accelerator Node (TeaCache, DeepCache) to speed up the image generation process.",
}),
"Lora": ("RUNWARELORA", {
"tooltip": "Connect a Runware Lora From Lora Search Node Or Lora Combine For Multiple Lora's Together.",
}),
"Outpainting": ("RUNWAREOUTPAINT", {
"tooltip": "Connect a Runware Outpainting Node to extend the image boundaries in different directions.",
}),
"IPAdapters": ("RUNWAREIPAdapter", {
"tooltip": "Connect a Runware IP Adapter node or IP Adapter Combine node to use reference images for guiding the generation.",
}),
"ControlNet": ("RUNWARECONTROLNET", {
"tooltip": "Connect a Runware ControlNet Guidance Node to help the model generate images that align with the desired structure.",
}),
"Refiner": ("RUNWAREREFINER", {
"tooltip": "Connect a Runware Refiner Node to help create higher quality image outputs by incorporating specialized models designed to enhance image details and overall coherence.",
}),
"seedImage": ("IMAGE", {
"tooltip": "Specifies the seed image to be used for the diffusion process, when doing Image-to-Image, Inpainting or Outpainting, this parameter is required.",
}),
"maskImage": ("MASK", {
"tooltip": "Specifies the mask image to be used for the inpainting process, when doing Inpainting, this parameter is required.",
}),
"Embeddings": ("RUNWAREEMBEDDING", {
"tooltip": "Connect a Runware Embedding Node to help the model generate images that align with the desired structure.",
}),
"VAE": ("RUNWAREVAE", {
"tooltip": "Connect a Runware VAE Node to help the model generate images that align with the desired structure.",
}),
"referenceImages": ("RUNWAREREFERENCEIMAGES", {
"tooltip": "Connect a Runware Reference Images Node to provide visual guidance for image generation.",
}),
"providerSettings": ("RUNWAREPROVIDERSETTINGS", {
"tooltip": "Connect a Runware Provider Settings Node to configure provider-specific parameters.",
}),
}
}
@classmethod
def VALIDATE_INPUTS(cls, positivePrompt, negativePrompt):
if (positivePrompt is not None and (positivePrompt == "" or len(positivePrompt) < 3 or len(positivePrompt) > 2000)):
raise Exception(
"Positive Prompt Must Be Between 3 And 2000 characters!")
if (negativePrompt is not None and negativePrompt != "" and (len(negativePrompt) < 3 or len(negativePrompt) > 2000)):
raise Exception(
"Negative Prompt Must Be Between 3 And 2000 characters!")
return True
DESCRIPTION = "Generates Images Lightning Fast With Runware Image Inference Sonic Engine."
FUNCTION = "generateImage"
RETURN_TYPES = ("IMAGE", "RUNWARETASK")
RETURN_NAMES = ("IMAGE", "RW-Task")
CATEGORY = "Runware"
def generateImage(self, **kwargs):
runwareModel = kwargs.get("Model")
positivePrompt = kwargs.get("positivePrompt")
negativePrompt = kwargs.get("negativePrompt", None)
multiInferenceMode = kwargs.get("Multi Inference Mode", False)
promptWeighting = kwargs.get("Prompt Weighting", "Disabled")
runwareControlNet = kwargs.get("ControlNet", None)
runwareAccelerator = kwargs.get("Accelerator", None)
runwareLora = kwargs.get("Lora", None)
runwareOutpainting = kwargs.get("Outpainting", None)
runwareIPAdapters = kwargs.get("IPAdapters", None)
runwareRefiner = kwargs.get("Refiner", None)
runwareEmbedding = kwargs.get("Embeddings", None)
runwareVAE = kwargs.get("VAE", None)
referenceImages = kwargs.get("referenceImages", None)
providerSettings = kwargs.get("providerSettings", None)
seedImage = kwargs.get("seedImage", None)
seedImageStrength = kwargs.get("strength", 0.8)
maskImage = kwargs.get("maskImage", None)
enableMaskMargin = kwargs.get("Mask Margin", False)
maskImageMargin = kwargs.get("maskMargin", 32)
clipSkip = kwargs.get("clipSkip", 0)
height = kwargs.get("height", 512)
width = kwargs.get("width", 512)
steps = kwargs.get("steps", 4)
useSteps = kwargs.get("useSteps", True)
scheduler = kwargs.get("scheduler", "Default")
useScheduler = kwargs.get("useScheduler", True)
cfgScale = kwargs.get("cfgScale", 6.5)
useCFGScale = kwargs.get("useCFGScale", True)
seed = kwargs.get("seed")
useSeed = kwargs.get("useSeed", True)
useClipSkip = kwargs.get("useClipSkip", True)
dimensions = kwargs.get("dimensions", "Square (512x512)")
batchSize = kwargs.get("batchSize", 1)
if (maskImage is not None and seedImage is None):
raise Exception("Mask Image Requires Seed Image To Be Provided!")
genConfig = [
{
"taskType": "imageInference",
"taskUUID": rwUtils.genRandUUID(),
"positivePrompt": positivePrompt,
"model": runwareModel,
"outputType": "base64Data",
"outputFormat": rwUtils.OUTPUT_FORMAT,
"outputQuality": rwUtils.OUTPUT_QUALITY,
"numberResults": batchSize,
}
]
# For Debugging Purposes Only
print(f"[Debugging] Task UUID: {genConfig[0]['taskUUID']}")
# Add steps, CFGScale, seed, scheduler, clipSkip, and dimensions only if enabled
if useSteps:
genConfig[0]["steps"] = steps
if useCFGScale:
genConfig[0]["CFGScale"] = cfgScale
if useSeed:
genConfig[0]["seed"] = seed
if useScheduler:
genConfig[0]["scheduler"] = scheduler
if useClipSkip:
genConfig[0]["clipSkip"] = clipSkip
if dimensions != "None":
genConfig[0]["width"] = width
genConfig[0]["height"] = height
if (negativePrompt is not None and negativePrompt != ""):
genConfig[0]["negativePrompt"] = negativePrompt
if (promptWeighting != "Disabled"):
if (promptWeighting == "sdEmbeds"):
genConfig[0]["promptWeighting"] = "sdEmbeds"
else:
genConfig[0]["promptWeighting"] = "compel"
if(runwareAccelerator is not None):
genConfig[0]["acceleratorOptions"] = runwareAccelerator
if (runwareLora is not None):
if (isinstance(runwareLora, list)):
genConfig[0]["lora"] = runwareLora
elif (isinstance(runwareLora, dict)):
genConfig[0]["lora"] = [runwareLora]
if (runwareIPAdapters is not None):
if (isinstance(runwareIPAdapters, list)):
genConfig[0]["ipAdapters"] = runwareIPAdapters
elif (isinstance(runwareIPAdapters, dict)):
genConfig[0]["ipAdapters"] = [runwareIPAdapters]
if (runwareEmbedding is not None):
if (isinstance(runwareEmbedding, list)):
genConfig[0]["embeddings"] = runwareEmbedding
elif (isinstance(runwareEmbedding, dict)):
genConfig[0]["embeddings"] = [runwareEmbedding]
if (runwareOutpainting is not None):
genConfig[0]["outpaint"] = runwareOutpainting
if (runwareVAE is not None):
genConfig[0]["vae"] = runwareVAE
if (runwareControlNet is not None):
genConfig[0]["controlNet"] = runwareControlNet
if (runwareRefiner is not None):
genConfig[0]["refiner"] = runwareRefiner
if (seedImage is not None):
seedImage = rwUtils.convertTensor2IMG(seedImage)
genConfig[0]["seedImage"] = seedImage
genConfig[0]["strength"] = seedImageStrength
if (maskImage is not None):
maskImage = rwUtils.convertTensor2IMG(maskImage)
genConfig[0]["maskImage"] = maskImage
if (enableMaskMargin):
genConfig[0]["maskMargin"] = maskImageMargin
# Handle referenceImages
if referenceImages is not None:
genConfig[0]["referenceImages"] = referenceImages
# Handle providerSettings - extract provider name from model and merge with custom settings
if providerSettings is not None:
# Extract provider name from model (e.g., "bytedance:1@1" -> "bytedance")
provider_name = runwareModel.split(":")[0] if ":" in runwareModel else runwareModel
# If providerSettings is a dictionary, create the correct API format
if isinstance(providerSettings, dict):
# Create the providerSettings object with provider name as key
final_provider_settings = {
provider_name: providerSettings
}
genConfig[0]["providerSettings"] = final_provider_settings
else:
# If it's already in the correct format, use it directly
genConfig[0]["providerSettings"] = providerSettings
if (multiInferenceMode):
return (None, genConfig)
else:
# Debug: Print the request being sent
print(f"[DEBUG] Sending Image Inference Request:")
print(f"[DEBUG] Request Payload: {json.dumps(genConfig, indent=2)}")
genResult = rwUtils.inferenecRequest(genConfig)
# Debug: Print the response received
print(f"[DEBUG] Received Image Inference Response:")
print(f"[DEBUG] Response: {json.dumps(genResult, indent=2)}")
images = rwUtils.convertImageB64List(genResult)
return (images, None)
from .utils import runwareUtils as rwUtils
import json
class txt2img:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"Model": ("RUNWAREMODEL", {
"tooltip": "Connect a Runware Model From Runware Model Node.",
}),
"positivePrompt": ("STRING", {
"multiline": True,
"placeholder": "Positive Prompt: a text instruction to guide the model on generating the image. It is usually a sentence or a paragraph that provides positive guidance for the task. This parameter is essential to shape the desired results.\n\nYou Can Press (Ctrl + Alt + E) To Enhance The Prompt!",
"tooltip": "Positive Prompt: a text instruction to guide the model on generating the image. You Can Also Press (Ctrl + Alt + E) To Enhance The Prompt!"
}),
"negativePrompt": ("STRING", {
"multiline": True,
"placeholder": "Negative Prompt: a text instruction to guide the model on generating the image. It is usually a sentence or a paragraph that provides negative guidance for the task. This parameter helps to avoid certain undesired results.",
"tooltip": "Negative Prompt: a text instruction to guide the model on generating the image."
}),
"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.",
"default": False,
"label_on": "Enabled",
"label_off": "Disabled",
}),
"Prompt Weighting": (["Disabled", "sdEmbeds", "Compel"], {
"default": "Disabled",
"tooltip": "Prompt weighting allows you to adjust how strongly different parts of your prompt influence the generated image.\n\nChoose between \"compel\" notation with advanced weighting operations or \"sdEmbeds\" for simple emphasis adjustments.\n\nCompel Example: \"small+ dog, pixar style\"\n\nsdEmbeds Example: \"(small:2.5) dog, pixar style\"",
}),
"dimensions": ([
"None", "Square (512x512)", "Square HD (1024x1024)", "Portrait 3:4 (768x1024)",
"Portrait 9:16 (576x1024)", "Landscape 4:3 (1024x768)",
"Landscape 16:9 (1024x576)", "Custom"
], {
"default": "Square (512x512)",
"tooltip": "Adjust the dimensions of the generated image by specifying its width and height in pixels, or select from the predefined options. Image dimensions must be multiples of 64 (e.g., 512x512, 1024x768). Select 'None' to let the model determine dimensions automatically.",
}),
"width": ("INT", {
"tooltip": "The Width of the image in pixels.",
"default": 512,
"min": 128,
"max": 2048,
"step": 64,
}),
"height": ("INT", {
"tooltip": "The Height of the image in pixels.",
"default": 512,
"min": 128,
"max": 2048,
"step": 64,
}),
"useSteps": ("BOOLEAN", {
"tooltip": "Enable to include steps parameter in API request. Disable if your model doesn't support steps (like nano banana).",
"default": True,
}),
"steps": ("INT", {
"tooltip": "The number of steps is the number of iterations the model will perform to generate the image. Only used when 'Use Steps' is enabled.",
"default": 4,
"min": 1,
"max": 100,
}),
"useScheduler": ("BOOLEAN", {
"tooltip": "Enable to include scheduler parameter in API request. Disable if your model doesn't support scheduler.",
"default": True,
}),
"scheduler": (['Default', 'DDIM', 'DDIMScheduler', 'DDPMScheduler', 'DEISMultistepScheduler', 'DPMSolverSinglestepScheduler', 'DPMSolverMultistepScheduler', 'DPMSolverMultistepInverse', 'DPM++', 'DPM++ Karras', 'DPM++ 2M', 'DPM++ 2M Karras', 'DPM++ 2M SDE Karras', 'DPM++ 2M SDE', 'DPM++ 3M', 'DPM++ 3M Karras', 'DPM++ SDE Karras', 'DPM++ SDE', 'EDMEulerScheduler', 'EDMDPMSolverMultistepScheduler', 'Euler', 'EulerDiscreteScheduler', 'Euler Karras', 'Euler a', 'EulerAncestralDiscreteScheduler', 'FlowMatchEulerDiscreteScheduler', 'Heun', 'HeunDiscreteScheduler', 'Heun Karras', 'IPNDMScheduler', 'KDPM2DiscreteScheduler', 'KDPM2AncestralDiscreteScheduler', 'LCM', 'LCMScheduler', 'LMS', 'LMSDiscreteScheduler', 'LMS Karras', 'PNDMScheduler', 'TCDScheduler', 'UniPC', 'UniPCMultistepScheduler', 'UniPC Karras', 'UniPC 2M', 'UniPC 2M Karras', 'UniPC 3M', 'UniPC 3M Karras'], {
"tooltip": "An scheduler is a component that manages the inference process. Different schedulers can be used to achieve different results like more detailed images, faster inference, or more accurate results.",
"default": "Default",
}),
"useCFGScale": ("BOOLEAN", {
"tooltip": "Enable to include CFG scale parameter in API request. Disable if your model doesn't support CFG scale (like nano banana).",
"default": True,
}),
"cfgScale": ("FLOAT", {
"tooltip": "Guidance scale represents how closely the images will resemble the prompt or how much freedom the AI model has. Only used when 'Use CFG Scale' is enabled.",
"default": 6.5,
"min": 1.0,
"max": 50.0,
"step": 0.5,
}),
"useSeed": ("BOOLEAN", {
"tooltip": "Enable to include seed parameter in API request. Disable if your model doesn't support seed.",
"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.",
"default": rwUtils.genRandSeed(),
"min": 1,
"max": 9223372036854776000,
}),
"useClipSkip": ("BOOLEAN", {
"tooltip": "Enable to include clipSkip parameter in API request. Disable if your model doesn't support clipSkip.",
"default": True,
}),
"clipSkip": ("INT", {
"tooltip": "Enables skipping layers of the CLIP embedding process, leading to quicker and more varied image generation. Only used when 'Use Clip Skip' is enabled.",
"default": 0,
"min": 0,
"max": 2,
}),
"strength": ("FLOAT", {
"tooltip": "When doing Image-to-Image or Inpainting, this parameter is used to determine the influence of the seedImage image in the generated output. A lower value results in more influence from the original image, while a higher value allows more creative deviation.",
"default": 0.8,
"min": 0.0,
"max": 1.0,
"step": 0.01,
}),
"Mask Margin": ("BOOLEAN", {
"tooltip": "Enables Or Disables Mask Margin Feature.",
"default": False,
"label_on": "Enabled",
"label_off": "Disabled",
}),
"maskMargin": ("INT", {
"tooltip": "Adds extra context pixels around the masked region during inpainting. When this parameter is present, the model will zoom into the masked area, considering these additional pixels to create more coherent and well-integrated details.",
"default": 32,
"min": 32,
"max": 128,
}),
"batchSize": ("INT", {
"tooltip": "The number of images to generate in a single request.",
"default": 1,
"min": 1,
"max": 10,
}),
},
"optional": {
"Accelerator": ("RUNWAREACCELERATOR", {
"tooltip": "Connect a Runware Accelerator Node (TeaCache, DeepCache) to speed up the image generation process.",
}),
"Lora": ("RUNWARELORA", {
"tooltip": "Connect a Runware Lora From Lora Search Node Or Lora Combine For Multiple Lora's Together.",
}),
"Outpainting": ("RUNWAREOUTPAINT", {
"tooltip": "Connect a Runware Outpainting Node to extend the image boundaries in different directions.",
}),
"IPAdapters": ("RUNWAREIPAdapter", {
"tooltip": "Connect a Runware IP Adapter node or IP Adapter Combine node to use reference images for guiding the generation.",
}),
"ControlNet": ("RUNWARECONTROLNET", {
"tooltip": "Connect a Runware ControlNet Guidance Node to help the model generate images that align with the desired structure.",
}),
"Refiner": ("RUNWAREREFINER", {
"tooltip": "Connect a Runware Refiner Node to help create higher quality image outputs by incorporating specialized models designed to enhance image details and overall coherence.",
}),
"seedImage": ("IMAGE", {
"tooltip": "Specifies the seed image to be used for the diffusion process, when doing Image-to-Image, Inpainting or Outpainting, this parameter is required.",
}),
"maskImage": ("MASK", {
"tooltip": "Specifies the mask image to be used for the inpainting process, when doing Inpainting, this parameter is required.",
}),
"Embeddings": ("RUNWAREEMBEDDING", {
"tooltip": "Connect a Runware Embedding Node to help the model generate images that align with the desired structure.",
}),
"VAE": ("RUNWAREVAE", {
"tooltip": "Connect a Runware VAE Node to help the model generate images that align with the desired structure.",
}),
"referenceImages": ("RUNWAREREFERENCEIMAGES", {
"tooltip": "Connect a Runware Reference Images Node to provide visual guidance for image generation.",
}),
"providerSettings": ("RUNWAREPROVIDERSETTINGS", {
"tooltip": "Connect a Runware Provider Settings Node to configure provider-specific parameters.",
}),
}
}
@classmethod
def VALIDATE_INPUTS(cls, positivePrompt, negativePrompt):
if (positivePrompt is not None and (positivePrompt == "" or len(positivePrompt) < 3 or len(positivePrompt) > 2000)):
raise Exception(
"Positive Prompt Must Be Between 3 And 2000 characters!")
if (negativePrompt is not None and negativePrompt != "" and (len(negativePrompt) < 3 or len(negativePrompt) > 2000)):
raise Exception(
"Negative Prompt Must Be Between 3 And 2000 characters!")
return True
DESCRIPTION = "Generates Images Lightning Fast With Runware Image Inference Sonic Engine."
FUNCTION = "generateImage"
RETURN_TYPES = ("IMAGE", "RUNWARETASK")
RETURN_NAMES = ("IMAGE", "RW-Task")
CATEGORY = "Runware"
def generateImage(self, **kwargs):
runwareModel = kwargs.get("Model")
positivePrompt = kwargs.get("positivePrompt")
negativePrompt = kwargs.get("negativePrompt", None)
multiInferenceMode = kwargs.get("Multi Inference Mode", False)
promptWeighting = kwargs.get("Prompt Weighting", "Disabled")
runwareControlNet = kwargs.get("ControlNet", None)
runwareAccelerator = kwargs.get("Accelerator", None)
runwareLora = kwargs.get("Lora", None)
runwareOutpainting = kwargs.get("Outpainting", None)
runwareIPAdapters = kwargs.get("IPAdapters", None)
runwareRefiner = kwargs.get("Refiner", None)
runwareEmbedding = kwargs.get("Embeddings", None)
runwareVAE = kwargs.get("VAE", None)
referenceImages = kwargs.get("referenceImages", None)
providerSettings = kwargs.get("providerSettings", None)
seedImage = kwargs.get("seedImage", None)
seedImageStrength = kwargs.get("strength", 0.8)
maskImage = kwargs.get("maskImage", None)
enableMaskMargin = kwargs.get("Mask Margin", False)
maskImageMargin = kwargs.get("maskMargin", 32)
clipSkip = kwargs.get("clipSkip", 0)
height = kwargs.get("height", 512)
width = kwargs.get("width", 512)
steps = kwargs.get("steps", 4)
useSteps = kwargs.get("useSteps", True)
scheduler = kwargs.get("scheduler", "Default")
useScheduler = kwargs.get("useScheduler", True)
cfgScale = kwargs.get("cfgScale", 6.5)
useCFGScale = kwargs.get("useCFGScale", True)
seed = kwargs.get("seed")
useSeed = kwargs.get("useSeed", True)
useClipSkip = kwargs.get("useClipSkip", True)
dimensions = kwargs.get("dimensions", "Square (512x512)")
batchSize = kwargs.get("batchSize", 1)
if (maskImage is not None and seedImage is None):
raise Exception("Mask Image Requires Seed Image To Be Provided!")
genConfig = [
{
"taskType": "imageInference",
"taskUUID": rwUtils.genRandUUID(),
"positivePrompt": positivePrompt,
"model": runwareModel,
"outputType": "base64Data",
"outputFormat": rwUtils.OUTPUT_FORMAT,
"outputQuality": rwUtils.OUTPUT_QUALITY,
"numberResults": batchSize,
}
]
# For Debugging Purposes Only
print(f"[Debugging] Task UUID: {genConfig[0]['taskUUID']}")
# Add steps, CFGScale, seed, scheduler, clipSkip, and dimensions only if enabled
if useSteps:
genConfig[0]["steps"] = steps
if useCFGScale:
genConfig[0]["CFGScale"] = cfgScale
if useSeed:
genConfig[0]["seed"] = seed
if useScheduler:
genConfig[0]["scheduler"] = scheduler
if useClipSkip:
genConfig[0]["clipSkip"] = clipSkip
if dimensions != "None":
genConfig[0]["width"] = width
genConfig[0]["height"] = height
if (negativePrompt is not None and negativePrompt != ""):
genConfig[0]["negativePrompt"] = negativePrompt
if (promptWeighting != "Disabled"):
if (promptWeighting == "sdEmbeds"):
genConfig[0]["promptWeighting"] = "sdEmbeds"
else:
genConfig[0]["promptWeighting"] = "compel"
if(runwareAccelerator is not None):
genConfig[0]["acceleratorOptions"] = runwareAccelerator
if (runwareLora is not None):
if (isinstance(runwareLora, list)):
genConfig[0]["lora"] = runwareLora
elif (isinstance(runwareLora, dict)):
genConfig[0]["lora"] = [runwareLora]
if (runwareIPAdapters is not None):
if (isinstance(runwareIPAdapters, list)):
genConfig[0]["ipAdapters"] = runwareIPAdapters
elif (isinstance(runwareIPAdapters, dict)):
genConfig[0]["ipAdapters"] = [runwareIPAdapters]
if (runwareEmbedding is not None):
if (isinstance(runwareEmbedding, list)):
genConfig[0]["embeddings"] = runwareEmbedding
elif (isinstance(runwareEmbedding, dict)):
genConfig[0]["embeddings"] = [runwareEmbedding]
if (runwareOutpainting is not None):
genConfig[0]["outpaint"] = runwareOutpainting
if (runwareVAE is not None):
genConfig[0]["vae"] = runwareVAE
if (runwareControlNet is not None):
genConfig[0]["controlNet"] = runwareControlNet
if (runwareRefiner is not None):
genConfig[0]["refiner"] = runwareRefiner
if (seedImage is not None):
seedImage = rwUtils.convertTensor2IMG(seedImage)
genConfig[0]["seedImage"] = seedImage
genConfig[0]["strength"] = seedImageStrength
if (maskImage is not None):
maskImage = rwUtils.convertTensor2IMG(maskImage)
genConfig[0]["maskImage"] = maskImage
if (enableMaskMargin):
genConfig[0]["maskMargin"] = maskImageMargin
# Handle referenceImages
if referenceImages is not None:
genConfig[0]["referenceImages"] = referenceImages
# Handle providerSettings - extract provider name from model and merge with custom settings
if providerSettings is not None:
# Extract provider name from model (e.g., "bytedance:1@1" -> "bytedance")
provider_name = runwareModel.split(":")[0] if ":" in runwareModel else runwareModel
# If providerSettings is a dictionary, create the correct API format
if isinstance(providerSettings, dict):
# Create the providerSettings object with provider name as key
final_provider_settings = {
provider_name: providerSettings
}
genConfig[0]["providerSettings"] = final_provider_settings
else:
# If it's already in the correct format, use it directly
genConfig[0]["providerSettings"] = providerSettings
if (multiInferenceMode):
return (None, genConfig)
else:
# Debug: Print the request being sent
print(f"[DEBUG] Sending Image Inference Request:")
print(f"[DEBUG] Request Payload: {json.dumps(genConfig, indent=2)}")
genResult = rwUtils.inferenecRequest(genConfig)
# Debug: Print the response received
print(f"[DEBUG] Received Image Inference Response:")
print(f"[DEBUG] Response: {json.dumps(genResult, indent=2)}")
images = rwUtils.convertImageB64List(genResult)
return (images, None)
+2
View File
@@ -240,6 +240,7 @@ def sendImageCaption(captionText, nodeID):
"success": True,
"captionText": captionText,
"nodeID": nodeID,
"widgetName": "imageCaption", # Target the new imageCaption widget
},
)
@@ -274,6 +275,7 @@ def inferenecRequest(genConfig):
print(f"[Debugging] Runware Raw Response Content: {genResult.content}")
raise Exception("Error: Invalid JSON response from API!")
if "errors" in genResult:
print(f"[DEBUG] API Error Response: {genResult}")
raise Exception(genResult["errors"][0]["message"])
else:
return genResult
+1
View File
@@ -0,0 +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}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long