Merge pull request #13 from Sirsho1997/fix-workflow/feature-videoInference

Add video inference; fix missing workflows
This commit is contained in:
Sirshendu Ganguly
2025-09-03 14:26:29 -04:00
committed by GitHub
32 changed files with 1306 additions and 500 deletions
Vendored
BIN
View File
Binary file not shown.
+4 -3
View File
@@ -4,7 +4,7 @@ Welcome to **ComfyUI-Runware**! 🌟 This is the official integration for **[Run
## 🌟 What is ComfyUI-Runware?
Imagine creating stunning images, refining details, or even removing backgrounds—all without needing a powerful GPU. With **Runware**, all the heavy lifting happens on our servers. No GPU? No problem! 🖥️💨
Imagine creating stunning images or videos, refining details, or even removing backgrounds—all without needing a powerful GPU. With **Runware**, all the heavy lifting happens on our servers. No GPU? No problem! 🖥️💨
This integration adds a suite of custom nodes to ComfyUI, allowing you to:
- Perform advanced image inference tasks, including image generation and editing with powerful models.
@@ -100,10 +100,10 @@ Inside the `ComfyUI-Runware` custom node folder, you’ll find a `workflows` fol
## 🧩 Node Features
Here’s a breakdown of the amazing nodes included in this integration:
Here's a breakdown of the amazing nodes included in this integration:
- **Runware API Manager**: Set or change your API keys, adjust the max connection timeout, adjust the image output quality and format, and enable or disable image caching directly in ComfyUI—no need to edit config files manually! 🔑
- **Runware Image Inference**: Perform advanced tasks like inpainting, outpainting, and more. 🎨
- **Runware Image Inference**: Perform advanced tasks like inpainting, outpainting, and more 🎨
- **Runware PhotoMakerV2**: Create consistent identities with our photomaker pipeline. 🖼️
- **Runware Image Upscale**: Upscale your images up to 4x. 🔍
- **Runware Background Removal**: Effortlessly remove image backgrounds. 🧹
@@ -121,6 +121,7 @@ Here’s a breakdown of the amazing nodes included in this integration:
- **Runware Image Caption**: Generate descriptive text from images for further workflow integration. 🖼️
- **Runware IPAdapter**: Use reference images to guide the style and content of generated images. 🖌️
- **Runware IPAdapters Combine**: Combine multiple IP-Adapter inputs for sophisticated image conditioning. 🔗
- **Runware Video Inference**: Generate videos from text prompts using advanced AI models with support for frame images and reference images. 🎬
---
+8 -1
View File
@@ -25,8 +25,11 @@ from .modules.ipAdapter import ipAdapter
from .modules.ipAdapterCombine import ipAdapterCombine
from .modules.vaeSearch import vaeSearch
from .modules.referenceImages import referenceImages
from .modules.videoInference import txt2vid
from .modules.videoModelSearch import videoModelSearch
from .modules.frameImages import RunwareFrameImages
RUNWARE_COMFYUI_VERSION = "1.2.0 Beta"
RUNWARE_COMFYUI_VERSION = "1.3.0 Beta"
RESET_COLOR = "\033[0m"
BLUE_COLOR = "\033[94m"
@@ -64,6 +67,9 @@ NODE_CLASS_MAPPINGS = {
"Runware IPAdapter": ipAdapter,
"Runware IPAdapters Combine": ipAdapterCombine,
"Runware Reference Images": referenceImages,
"Runware Video Inference": txt2vid,
"Runware Video Model Search": videoModelSearch,
"Runware Frame Images": RunwareFrameImages,
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -72,6 +78,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"Runware Embedding Search": "Runware Embedding",
"Runware VAE Search": "Runware VAE",
"Runware Multi Inference": "Runware Multi Inference [BETA]",
"Runware Video Model Search": "Runware Video Model",
}
WEB_DIRECTORY = "./clientlibs"
+12
View File
@@ -13,6 +13,7 @@ const DEFAULT_MODELS_ARCH_LIST = {
"All": "all",
"FLUX.1-Schnell": "flux1s",
"FLUX.1-Dev": "flux1d",
"FLUX.1-Krea": "flux1d",
"Pony": "pony",
"SD 1.5": "sd1x",
"SD 1.5 Hyper": "sdhyper",
@@ -73,6 +74,8 @@ const RUNWARE_NODE_TYPES = {
EMBEDDINGCOMBINE: "Runware Embedding Combine",
VAE: "Runware VAE Search",
REFERENCEIMAGES: "Runware Reference Images",
VIDEOINFERENCE: "Runware Video Inference",
VIDEOMODELSEARCH: "Runware Video Model Search",
};
const RUNWARE_NODE_PROPS = {
@@ -186,6 +189,15 @@ const RUNWARE_NODE_PROPS = {
bgColor: DEFAULT_BGCOLOR,
colorModeOnly: true,
},
[RUNWARE_NODE_TYPES.VIDEOINFERENCE]: {
bgColor: DEFAULT_BGCOLOR,
liveDimensions: true,
promptEnhancer: true,
},
[RUNWARE_NODE_TYPES.VIDEOMODELSEARCH]: {
bgColor: DEFAULT_BGCOLOR,
liveSearch: true,
},
};
export {
+62 -62
View File
@@ -1,63 +1,63 @@
from .utils import runwareUtils as rwUtils
class apiManager:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"API Key": (
"STRING",
{
"tooltip": "Define Or Change Your Runware API Key To Start Using Runware Image Inference Services.\n\n(e.g; BcVr1JVQM8FGzrRwb3GsbCq5ww1QwabV)\n\nIf You Don't Have One, You Can Get It From: https://my.runware.ai/keys",
},
),
"Max Timeout": (
"INT",
{
"tooltip": "Change Runware API Request Timeout In Seconds.\n\n(e.g; 90 Seconds).",
"min": 5,
"max": 99,
"default": int(rwUtils.getTimeout()),
},
),
"Image Output Quality": (
"INT",
{
"tooltip": "Sets the compression quality of the output image. Higher values preserve more quality but increase file size, lower values reduce file size but decrease quality.",
"min": 20,
"max": 99,
"default": int(rwUtils.getOutputQuality()),
},
),
"Image Output Format": (
["WEBP", "PNG", "JPEG"],
{
"tooltip": "Change the Default Image Output Format.",
"default": rwUtils.getOutputFormat(),
},
),
"Enable Images Caching": (
"BOOLEAN",
{
"label_on": "Enabled",
"label_off": "Disabled",
"tooltip": "Enable or disable image caching functionality.\n\nWhen enabled, images will be cached to improve performance and reduce redundant processing.",
"default": rwUtils.getEnableImagesCaching(),
},
),
"Min Image Cache Size": (
"INT",
{
"tooltip": "Set the minimum size (in KB) for images to be cached.",
"min": 30,
"max": 4096,
"step": 1,
"default": int(rwUtils.getMinImageCacheSize()),
},
),
},
}
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"
from .utils import runwareUtils as rwUtils
class apiManager:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"API Key": (
"STRING",
{
"tooltip": "Define Or Change Your Runware API Key To Start Using Runware Image Inference Services.\n\n(e.g; BcVr1JVQM8FGzrRwb3GsbCq5ww1QwabV)\n\nIf You Don't Have One, You Can Get It From: https://my.runware.ai/keys",
},
),
"Max Timeout": (
"INT",
{
"tooltip": "Change Runware API Request Timeout In Seconds.\n\n(e.g; 90 Seconds).",
"min": 5,
"max": 99,
"default": int(rwUtils.getTimeout()),
},
),
"Image Output Quality": (
"INT",
{
"tooltip": "Sets the compression quality of the output image. Higher values preserve more quality but increase file size, lower values reduce file size but decrease quality.",
"min": 20,
"max": 99,
"default": int(rwUtils.getOutputQuality()),
},
),
"Image Output Format": (
["WEBP", "PNG", "JPEG"],
{
"tooltip": "Change the Default Image Output Format.",
"default": rwUtils.getOutputFormat(),
},
),
"Enable Images Caching": (
"BOOLEAN",
{
"label_on": "Enabled",
"label_off": "Disabled",
"tooltip": "Enable or disable image caching functionality.\n\nWhen enabled, images will be cached to improve performance and reduce redundant processing.",
"default": rwUtils.getEnableImagesCaching(),
},
),
"Min Image Cache Size": (
"INT",
{
"tooltip": "Set the minimum size (in KB) for images to be cached.",
"min": 30,
"max": 4096,
"step": 1,
"default": int(rwUtils.getMinImageCacheSize()),
},
),
},
}
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 = ()
+1
View File
@@ -15,6 +15,7 @@ class controlNet:
"All",
"FLUX.1-Schnell",
"FLUX.1-Dev",
"FLUX.1 Krea",
"Pony",
"SD 1.5",
"SD 1.5 Hyper",
+1
View File
@@ -10,6 +10,7 @@ class embeddingSearch:
"All",
"FLUX.1-Schnell",
"FLUX.1-Dev",
"FLUX.1 Krea",
"Pony",
"SD 1.5",
"SD 1.5 Hyper",
+87
View File
@@ -0,0 +1,87 @@
from .utils import runwareUtils as rwUtils
class RunwareFrameImages:
@classmethod
def INPUT_TYPES(cls):
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).",
}),
}
}
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"
RETURN_TYPES = ("RUNWAREFRAMEIMAGES",)
RETURN_NAMES = ("Frame Images",)
CATEGORY = "Runware"
def create_frame_images(self, **kwargs):
frame_images = []
# 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"
image = kwargs.get(image_key)
position = kwargs.get(position_key, "auto")
if image is not None:
# Upload image and get UUID
image_uuid = rwUtils.convertTensor2IMGForVideo(image)
image_url = f"https://im.runware.ai/image/ii/{image_uuid}.webp"
frame_data = {
"inputImage": image_url
}
# Only add frame position if not auto
if position != "auto":
# Convert string numbers to integers for API
if position.isdigit():
frame_data["frame"] = int(position)
else:
frame_data["frame"] = position # "first" or "last"
frame_images.append(frame_data)
return (frame_images,)
# Node class mappings for ComfyUI registration
NODE_CLASS_MAPPINGS = {
"RunwareFrameImages": RunwareFrameImages,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"RunwareFrameImages": "Runware Frame Images",
}
+20 -6
View File
@@ -52,21 +52,29 @@ class txt2img:
"step": 64,
}),
"steps": ("INT", {
"tooltip": "The number of steps is the number of iterations the model will perform to generate the image. The higher the number of steps, the more detailed the image will be. However, increasing the number of steps will also increase the time it takes to generate the image and may not always result in a better image.",
"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,
}),
"useSteps": (["enable", "disabled"], {
"tooltip": "Enable to include steps parameter in API request. Disable if your model doesn't support steps (like nano banana).",
"default": "enable",
}),
"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",
}),
"cfgScale": ("FLOAT", {
"tooltip": "Guidance scale represents how closely the images will resemble the prompt or how much freedom the AI model has. Higher values are closer to the prompt. Low values may reduce the quality of the results.",
"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": 30.0,
"step": 0.1,
"max": 50.0,
"step": 0.5,
}),
"useCFGScale": (["enable", "disabled"], {
"tooltip": "Enable to include CFG scale parameter in API request. Disable if your model doesn't support CFG scale (like nano banana).",
"default": "enable",
}),
"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.",
@@ -179,8 +187,10 @@ class txt2img:
height = kwargs.get("height", 512)
width = kwargs.get("width", 512)
steps = kwargs.get("steps", 4)
useSteps = kwargs.get("useSteps", "enable")
scheduler = kwargs.get("scheduler", "Default")
cfgScale = kwargs.get("cfgScale", 6.5)
useCFGScale = kwargs.get("useCFGScale", "enable")
seed = kwargs.get("seed")
batchSize = kwargs.get("batchSize", 1)
@@ -195,8 +205,6 @@ class txt2img:
"height": height,
"width": width,
"model": runwareModel,
"steps": steps,
"CFGScale": cfgScale,
"scheduler": scheduler,
"clipSkip": clipSkip,
"seed": seed,
@@ -210,6 +218,12 @@ class txt2img:
# For Debugging Purposes Only
print(f"[Debugging] Task UUID: {genConfig[0]['taskUUID']}")
# Add steps and CFGScale only if enabled
if useSteps == "enable":
genConfig[0]["steps"] = steps
if useCFGScale == "enable":
genConfig[0]["CFGScale"] = cfgScale
if (negativePrompt is not None and negativePrompt != ""):
genConfig[0]["negativePrompt"] = negativePrompt
if (promptWeighting != "Disabled"):
+1
View File
@@ -10,6 +10,7 @@ class loraSearch:
"All",
"FLUX.1-Schnell",
"FLUX.1-Dev",
"FLUX.1 Krea",
"Pony",
"SD 1.5",
"SD 1.5 Hyper",
+2
View File
@@ -10,6 +10,7 @@ class modelSearch:
"All",
"FLUX.1-Schnell",
"FLUX.1-Dev",
"FLUX.1-Krea",
"Pony",
"SD 1.5",
"SD 1.5 Hyper",
@@ -37,6 +38,7 @@ class modelSearch:
"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 麦橘写实)",
+2 -1
View File
@@ -46,7 +46,8 @@ class multiInference:
def multiInference(self, **kwargs):
tasksData = [kwargs.get(f"Task {i}", None) for i in range(1, 5)]
validTasks = [task for task in tasksData if task is not None]
if not validTasks:
raise Exception("Error: No valid tasks provided for Multi Inference!")
+585 -414
View File
@@ -1,414 +1,585 @@
from comfy.model_management import InterruptProcessingException
from requests.adapters import HTTPAdapter
from datetime import datetime, timedelta
from server import PromptServer
from dotenv import load_dotenv
from pathlib import Path
from PIL import Image
import numpy as np
import requests
import hashlib
import asyncio
import random
import base64
import torch
import uuid
import time
import json
import os
import io
import threading
load_dotenv()
BASEFOLDER = Path(__file__).parent.parent.parent
IMAGE_CACHE_FILE = BASEFOLDER / "imagesCache.json"
if not IMAGE_CACHE_FILE.exists():
with open(IMAGE_CACHE_FILE, "w") as f:
print("[Runware] Initializing images cache...")
json.dump({}, f)
RUNWARE_REMBG_OUTPUT_FORMATS = {
"outputFormat": (
["WEBP", "PNG"],
{"default": "WEBP", "tooltip": "Choose the output image format."},
)
}
MAX_RETRIES = 4
RETRY_COOLDOWNS = [1, 2, 5, 10]
session = requests.Session()
adapter = HTTPAdapter(pool_connections=10, pool_maxsize=10)
session.mount("http://", adapter)
session.mount("https://", adapter)
def generalRequestWrapper(recaller, *args, **kwargs):
for attempt in range(MAX_RETRIES + 1):
try:
return recaller(*args, **kwargs)
except (requests.exceptions.ConnectionError, requests.exceptions.SSLError) as e:
if attempt == MAX_RETRIES:
raise
else:
cooldown = RETRY_COOLDOWNS[attempt]
print(f"[Runware] Error API Request Failed! Retrying in {cooldown} seconds... (Attempt {attempt+1}/{MAX_RETRIES})")
time.sleep(cooldown)
continue
return False
def getAPIKey():
apiKey = os.getenv("RUNWARE_API_KEY")
if apiKey and isinstance(apiKey, str) and len(apiKey) > 24:
return apiKey
return False
def getTimeout():
timeout = os.getenv("RUNWARE_TIMEOUT")
if timeout and timeout.isdigit():
return int(timeout)
else:
timeout = 90
os.environ["RUNWARE_TIMEOUT"] = str(timeout)
return timeout
def getOutputQuality():
output_quality = os.getenv("RUNWARE_OUTPUT_QUALITY")
if output_quality and isinstance(output_quality, str) and output_quality.isdigit():
return int(output_quality)
else:
output_quality = 95
os.environ["RUNWARE_OUTPUT_QUALITY"] = str(output_quality)
return output_quality
def getOutputFormat():
output_format = os.getenv("RUNWARE_OUTPUT_FORMAT")
if output_format and isinstance(output_format, str):
return output_format
else:
output_format = "WEBP"
os.environ["RUNWARE_OUTPUT_FORMAT"] = output_format
return output_format
def getEnableImagesCaching():
enable_images_caching = os.getenv("RUNWARE_ENABLE_IMAGES_CACHING")
if enable_images_caching and enable_images_caching.lower() in ["true", "false"]:
return enable_images_caching.lower() == "true"
else:
enable_images_caching = True
os.environ["RUNWARE_ENABLE_IMAGES_CACHING"] = str(enable_images_caching)
return enable_images_caching
def getMinImageCacheSize():
min_image_cache_size = os.getenv("RUNWARE_MIN_IMAGE_CACHE_SIZE")
if min_image_cache_size and min_image_cache_size.isdigit():
return int(min_image_cache_size)
else:
min_image_cache_size = 150
os.environ["RUNWARE_MIN_IMAGE_CACHE_SIZE"] = str(min_image_cache_size)
return min_image_cache_size
def getCustomEndpoint():
custom_endpoint = os.getenv("RUNWARE_CUSTOM_ENDPOINT")
if custom_endpoint and isinstance(custom_endpoint, str):
return custom_endpoint
return "https://api.runware.ai/v1"
SESSION_TIMEOUT = getTimeout()
RUNWARE_API_KEY = getAPIKey()
OUTPUT_FORMAT = getOutputFormat()
OUTPUT_QUALITY = getOutputQuality()
ENABLE_IMAGES_CACHING = getEnableImagesCaching()
MIN_IMAGE_CACHE_SIZE = getMinImageCacheSize()
RUNWARE_API_BASE_URL = getCustomEndpoint()
def setEnvKey(keyName, keyValue):
comfyNodeRoot = Path(__file__).parent.parent.parent
envFilePath = comfyNodeRoot / ".env"
if not envFilePath.exists():
envFilePath.touch()
with open(envFilePath, "r") as f:
lines = f.readlines()
key_exists = False
new_lines = []
for line in lines:
if line.startswith(f"{keyName}="):
key_exists = True
new_lines.append(f"{keyName}={keyValue}\n")
else:
new_lines.append(line)
if not key_exists:
new_lines.append(f"{keyName}={keyValue}\n")
with open(envFilePath, "w") as f:
f.writelines(new_lines)
return True
def setAPIKey(apiKey: str):
global RUNWARE_API_KEY
envSetRes = setEnvKey("RUNWARE_API_KEY", apiKey)
if envSetRes:
RUNWARE_API_KEY = apiKey
os.environ["RUNWARE_API_KEY"] = apiKey
return True
def setTimeout(timeout: int):
envSetRes = setEnvKey("RUNWARE_TIMEOUT", str(timeout))
if envSetRes:
global SESSION_TIMEOUT
SESSION_TIMEOUT = timeout
os.environ["RUNWARE_TIMEOUT"] = str(timeout)
return True
def setOutputFormat(format: str):
envSetRes = setEnvKey("RUNWARE_OUTPUT_FORMAT", format)
if envSetRes:
global OUTPUT_FORMAT
OUTPUT_FORMAT = format
os.environ["RUNWARE_OUTPUT_FORMAT"] = format
return True
def setOutputQuality(quality: int):
envSetRes = setEnvKey("RUNWARE_OUTPUT_QUALITY", str(quality))
if envSetRes:
global OUTPUT_QUALITY
OUTPUT_QUALITY = quality
os.environ["RUNWARE_OUTPUT_QUALITY"] = str(quality)
return True
def setEnableImagesCaching(enabled: bool):
envSetRes = setEnvKey("RUNWARE_ENABLE_IMAGES_CACHING", str(enabled))
if envSetRes:
global ENABLE_IMAGES_CACHING
ENABLE_IMAGES_CACHING = enabled
os.environ["RUNWARE_ENABLE_IMAGES_CACHING"] = str(enabled)
return True
def setMinImageCacheSize(size: int):
envSetRes = setEnvKey("RUNWARE_MIN_IMAGE_CACHE_SIZE", str(size))
if envSetRes:
global MIN_IMAGE_CACHE_SIZE
MIN_IMAGE_CACHE_SIZE = size
os.environ["RUNWARE_MIN_IMAGE_CACHE_SIZE"] = str(size)
return True
def genRandSeed(minSeed=1000, maxSeed=9223372036854776000):
return random.randint(minSeed, maxSeed)
def genRandUUID():
return str(uuid.uuid4())
def checkAPIKey(apiKey):
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Accept-Encoding": "gzip, deflate, br, zstd",
}
genConfig = [
{
"taskType": "authentication",
"apiKey": apiKey,
}
]
try:
def recaller():
return session.post(
RUNWARE_API_BASE_URL,
headers=headers,
json=genConfig,
timeout=10,
allow_redirects=False,
stream=True,
)
genResult = generalRequestWrapper(recaller)
genResult = genResult.json()
if "errors" in genResult:
return str(genResult["errors"][0]["message"])
else:
return True
except Exception as e:
return False
def sendImageCaption(captionText, nodeID):
PromptServer.instance.send_sync(
"runwareImageCaption",
{
"success": True,
"captionText": captionText,
"nodeID": nodeID,
},
)
def inferenecRequest(genConfig):
global RUNWARE_API_KEY, RUNWARE_API_BASE_URL, SESSION_TIMEOUT
RUNWARE_API_KEY = os.getenv("RUNWARE_API_KEY")
SESSION_TIMEOUT = int(os.getenv("RUNWARE_TIMEOUT"))
headers = {
"Authorization": f"Bearer {RUNWARE_API_KEY}",
"Content-Type": "application/json",
"Accept-Encoding": "gzip, deflate, br, zstd",
}
try:
def recaller():
return session.post(
RUNWARE_API_BASE_URL,
headers=headers,
json=genConfig,
timeout=SESSION_TIMEOUT,
allow_redirects=False,
stream=True,
)
genResult = generalRequestWrapper(recaller)
try:
genResult = genResult.json()
except json.JSONDecodeError as e:
print(f"[Debugging] Runware JSON Decode Error: {str(e)}")
print(f"[Debugging] Runware Response Status Code: {genResult.status_code}")
print(f"[Debugging] Runware Response Headers: {genResult.headers}")
print(f"[Debugging] Runware Raw Response Content: {genResult.content}")
raise Exception("Error: Invalid JSON response from API!")
if "errors" in genResult:
raise Exception(genResult["errors"][0]["message"])
else:
return genResult
except requests.exceptions.Timeout:
raise Exception(
f"Error: Request Timed Out After {SESSION_TIMEOUT} Seconds - Please Try Again!"
)
except requests.exceptions.RequestException:
raise Exception("Error: Runware Request Failed!")
except Exception as e:
if "invalid api key" in str(e).lower():
PromptServer.instance.send_sync(
"runwareError",
{
"success": False,
"errorMessage": str(e),
"errorCode": 401,
},
)
raise InterruptProcessingException()
else:
raise Exception(f"Error: {e}")
return False
async def uploadImage(imageDataUri):
uploadTaskConfig = [
{"taskType": "imageUpload", "taskUUID": genRandUUID(), "image": imageDataUri}
]
try:
uploadResult = inferenecRequest(uploadTaskConfig)
if (
uploadResult
and "data" in uploadResult
and "imageUUID" in uploadResult["data"][0]
):
imageUUID = uploadResult["data"][0]["imageUUID"]
return imageUUID
except Exception as e:
return False
return False
async def uploadAndCacheImage(imgSig: str, imgDataUri: str):
try:
imageUUID = await uploadImage(imgDataUri)
if imageUUID:
await imageStoreSet(imgSig, imageUUID)
except Exception as e:
return False
async def imageStoreSet(imgHash: str, imgUUID: str) -> bool:
try:
with open(IMAGE_CACHE_FILE, "r") as f:
cache = json.load(f)
expires = (datetime.now() + timedelta(days=30)).isoformat()
cache[imgHash] = {"uuid": imgUUID, "expires": expires}
with open(IMAGE_CACHE_FILE, "w") as f:
json.dump(cache, f, indent=2)
return True
except Exception as e:
return False
def imageStoreGet(imgHash: str) -> str | bool:
try:
with open(IMAGE_CACHE_FILE, "r") as f:
cache = json.load(f)
if imgHash not in cache:
return False
entry = cache[imgHash]
expires = datetime.fromisoformat(entry["expires"])
if datetime.now() > expires:
del cache[imgHash]
with open(IMAGE_CACHE_FILE, "w") as f:
json.dump(cache, f, indent=2)
return False
return entry["uuid"]
except Exception as e:
return False
def convertTensor2IMG(tensorImage):
global ENABLE_IMAGES_CACHING, MIN_IMAGE_CACHE_SIZE
ENABLE_IMAGES_CACHING = getEnableImagesCaching()
MIN_IMAGE_CACHE_SIZE = int(os.getenv("RUNWARE_MIN_IMAGE_CACHE_SIZE"))
imageNP = (tensorImage.squeeze().numpy() * 255).astype(np.uint8)
imgSig = hashlib.sha256(imageNP.tobytes()).hexdigest()
if ENABLE_IMAGES_CACHING:
imgUUID = imageStoreGet(imgSig)
if imgUUID:
return imgUUID
image = Image.fromarray(imageNP)
with io.BytesIO() as buffer:
image.save(
buffer, format="webp", quality=100, subsampling=0, method=6, exact=True
)
imageRawData = buffer.getvalue()
imgBytes = len(imageRawData)
imgSize = int(imgBytes / 1024)
imgb64 = base64.b64encode(imageRawData).decode("utf-8")
imgDataUri = f"data:image/webp;base64,{imgb64}"
if ENABLE_IMAGES_CACHING and imgSize >= MIN_IMAGE_CACHE_SIZE:
try:
loop = asyncio.get_running_loop()
loop.call_soon(lambda: asyncio.ensure_future(uploadAndCacheImage(imgSig, imgDataUri)))
except RuntimeError:
def run_coro():
new_loop = asyncio.new_event_loop()
asyncio.set_event_loop(new_loop)
new_loop.run_until_complete(uploadAndCacheImage(imgSig, imgDataUri))
new_loop.close()
threading.Thread(target=run_coro).start()
return imgDataUri
def convertIMG2Tensor(b64img):
imgbytes = base64.b64decode(b64img)
image = Image.open(io.BytesIO(imgbytes))
imageNP = np.array(image).astype(np.float32) / 255.0
tensorImage = torch.from_numpy(imageNP).squeeze()
return tensorImage
def convertImageB64List(imageDataObject):
images = ()
for result in imageDataObject["data"]:
generatedImage = result.get(
"imageBase64Data",
result.get(
"maskImageBase64Data", result.get("guideImageBase64Data", False)
),
)
generatedImage = convertIMG2Tensor(generatedImage)
images += (generatedImage,)
images = torch.stack(images, dim=0)
return images
from comfy.model_management import InterruptProcessingException
from requests.adapters import HTTPAdapter
from datetime import datetime, timedelta
from server import PromptServer
from dotenv import load_dotenv
from pathlib import Path
from PIL import Image
import numpy as np
import requests
import hashlib
import asyncio
import random
import base64
import torch
import uuid
import time
import json
import os
import io
import threading
load_dotenv()
BASEFOLDER = Path(__file__).parent.parent.parent
IMAGE_CACHE_FILE = BASEFOLDER / "imagesCache.json"
if not IMAGE_CACHE_FILE.exists():
with open(IMAGE_CACHE_FILE, "w") as f:
print("[Runware] Initializing images cache...")
json.dump({}, f)
RUNWARE_REMBG_OUTPUT_FORMATS = {
"outputFormat": (
["WEBP", "PNG"],
{"default": "WEBP", "tooltip": "Choose the output image format."},
)
}
MAX_RETRIES = 4
RETRY_COOLDOWNS = [1, 2, 5, 10]
session = requests.Session()
adapter = HTTPAdapter(pool_connections=10, pool_maxsize=10)
session.mount("http://", adapter)
session.mount("https://", adapter)
def generalRequestWrapper(recaller, *args, **kwargs):
for attempt in range(MAX_RETRIES + 1):
try:
return recaller(*args, **kwargs)
except (requests.exceptions.ConnectionError, requests.exceptions.SSLError) as e:
if attempt == MAX_RETRIES:
raise
else:
cooldown = RETRY_COOLDOWNS[attempt]
print(f"[Runware] Error API Request Failed! Retrying in {cooldown} seconds... (Attempt {attempt+1}/{MAX_RETRIES})")
time.sleep(cooldown)
continue
return False
def getAPIKey():
apiKey = os.getenv("RUNWARE_API_KEY")
if apiKey and isinstance(apiKey, str) and len(apiKey) > 24:
return apiKey
return False
def getTimeout():
timeout = os.getenv("RUNWARE_TIMEOUT")
if timeout and timeout.isdigit():
return int(timeout)
else:
timeout = 90
os.environ["RUNWARE_TIMEOUT"] = str(timeout)
return timeout
def getOutputQuality():
output_quality = os.getenv("RUNWARE_OUTPUT_QUALITY")
if output_quality and isinstance(output_quality, str) and output_quality.isdigit():
return int(output_quality)
else:
output_quality = 95
os.environ["RUNWARE_OUTPUT_QUALITY"] = str(output_quality)
return output_quality
def getOutputFormat():
output_format = os.getenv("RUNWARE_OUTPUT_FORMAT")
if output_format and isinstance(output_format, str):
return output_format
else:
output_format = "WEBP"
os.environ["RUNWARE_OUTPUT_FORMAT"] = output_format
return output_format
def getEnableImagesCaching():
enable_images_caching = os.getenv("RUNWARE_ENABLE_IMAGES_CACHING")
if enable_images_caching and enable_images_caching.lower() in ["true", "false"]:
return enable_images_caching.lower() == "true"
else:
enable_images_caching = True
os.environ["RUNWARE_ENABLE_IMAGES_CACHING"] = str(enable_images_caching)
return enable_images_caching
def getMinImageCacheSize():
min_image_cache_size = os.getenv("RUNWARE_MIN_IMAGE_CACHE_SIZE")
if min_image_cache_size and min_image_cache_size.isdigit():
return int(min_image_cache_size)
else:
min_image_cache_size = 150
os.environ["RUNWARE_MIN_IMAGE_CACHE_SIZE"] = str(min_image_cache_size)
return min_image_cache_size
def getCustomEndpoint():
custom_endpoint = os.getenv("RUNWARE_CUSTOM_ENDPOINT")
if custom_endpoint and isinstance(custom_endpoint, str):
return custom_endpoint
return "https://api.runware.ai/v1"
SESSION_TIMEOUT = getTimeout()
RUNWARE_API_KEY = getAPIKey()
OUTPUT_FORMAT = getOutputFormat()
OUTPUT_QUALITY = getOutputQuality()
ENABLE_IMAGES_CACHING = getEnableImagesCaching()
MIN_IMAGE_CACHE_SIZE = getMinImageCacheSize()
RUNWARE_API_BASE_URL = getCustomEndpoint()
def setEnvKey(keyName, keyValue):
comfyNodeRoot = Path(__file__).parent.parent.parent
envFilePath = comfyNodeRoot / ".env"
if not envFilePath.exists():
envFilePath.touch()
with open(envFilePath, "r") as f:
lines = f.readlines()
key_exists = False
new_lines = []
for line in lines:
if line.startswith(f"{keyName}="):
key_exists = True
new_lines.append(f"{keyName}={keyValue}\n")
else:
new_lines.append(line)
if not key_exists:
new_lines.append(f"{keyName}={keyValue}\n")
with open(envFilePath, "w") as f:
f.writelines(new_lines)
return True
def setAPIKey(apiKey: str):
global RUNWARE_API_KEY
envSetRes = setEnvKey("RUNWARE_API_KEY", apiKey)
if envSetRes:
RUNWARE_API_KEY = apiKey
os.environ["RUNWARE_API_KEY"] = apiKey
return True
def setTimeout(timeout: int):
envSetRes = setEnvKey("RUNWARE_TIMEOUT", str(timeout))
if envSetRes:
global SESSION_TIMEOUT
SESSION_TIMEOUT = timeout
os.environ["RUNWARE_TIMEOUT"] = str(timeout)
return True
def setOutputFormat(format: str):
envSetRes = setEnvKey("RUNWARE_OUTPUT_FORMAT", format)
if envSetRes:
global OUTPUT_FORMAT
OUTPUT_FORMAT = format
os.environ["RUNWARE_OUTPUT_FORMAT"] = format
return True
def setOutputQuality(quality: int):
envSetRes = setEnvKey("RUNWARE_OUTPUT_QUALITY", str(quality))
if envSetRes:
global OUTPUT_QUALITY
OUTPUT_QUALITY = quality
os.environ["RUNWARE_OUTPUT_QUALITY"] = str(quality)
return True
def setEnableImagesCaching(enabled: bool):
envSetRes = setEnvKey("RUNWARE_ENABLE_IMAGES_CACHING", str(enabled))
if envSetRes:
global ENABLE_IMAGES_CACHING
ENABLE_IMAGES_CACHING = enabled
os.environ["RUNWARE_ENABLE_IMAGES_CACHING"] = str(enabled)
return True
def setMinImageCacheSize(size: int):
envSetRes = setEnvKey("RUNWARE_MIN_IMAGE_CACHE_SIZE", str(size))
if envSetRes:
global MIN_IMAGE_CACHE_SIZE
MIN_IMAGE_CACHE_SIZE = size
os.environ["RUNWARE_MIN_IMAGE_CACHE_SIZE"] = str(size)
return True
def genRandSeed(minSeed=1000, maxSeed=9223372036854776000):
return random.randint(minSeed, maxSeed)
def genRandUUID():
return str(uuid.uuid4())
def checkAPIKey(apiKey):
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Accept-Encoding": "gzip, deflate, br, zstd",
}
genConfig = [
{
"taskType": "authentication",
"apiKey": apiKey,
}
]
try:
def recaller():
return session.post(
RUNWARE_API_BASE_URL,
headers=headers,
json=genConfig,
timeout=10,
allow_redirects=False,
stream=True,
)
genResult = generalRequestWrapper(recaller)
genResult = genResult.json()
if "errors" in genResult:
return str(genResult["errors"][0]["message"])
else:
return True
except Exception as e:
return False
def sendImageCaption(captionText, nodeID):
PromptServer.instance.send_sync(
"runwareImageCaption",
{
"success": True,
"captionText": captionText,
"nodeID": nodeID,
},
)
def inferenecRequest(genConfig):
global RUNWARE_API_KEY, RUNWARE_API_BASE_URL, SESSION_TIMEOUT
RUNWARE_API_KEY = os.getenv("RUNWARE_API_KEY")
SESSION_TIMEOUT = int(os.getenv("RUNWARE_TIMEOUT"))
headers = {
"Authorization": f"Bearer {RUNWARE_API_KEY}",
"Content-Type": "application/json",
"Accept-Encoding": "gzip, deflate, br, zstd",
}
try:
def recaller():
return session.post(
RUNWARE_API_BASE_URL,
headers=headers,
json=genConfig,
timeout=SESSION_TIMEOUT,
allow_redirects=False,
stream=True,
)
genResult = generalRequestWrapper(recaller)
try:
genResult = genResult.json()
except json.JSONDecodeError as e:
print(f"[Debugging] Runware JSON Decode Error: {str(e)}")
print(f"[Debugging] Runware Response Status Code: {genResult.status_code}")
print(f"[Debugging] Runware Response Headers: {genResult.headers}")
print(f"[Debugging] Runware Raw Response Content: {genResult.content}")
raise Exception("Error: Invalid JSON response from API!")
if "errors" in genResult:
raise Exception(genResult["errors"][0]["message"])
else:
return genResult
except requests.exceptions.Timeout:
raise Exception(
f"Error: Request Timed Out After {SESSION_TIMEOUT} Seconds - Please Try Again!"
)
except requests.exceptions.RequestException:
raise Exception("Error: Runware Request Failed!")
except Exception as e:
if "invalid api key" in str(e).lower():
PromptServer.instance.send_sync(
"runwareError",
{
"success": False,
"errorMessage": str(e),
"errorCode": 401,
},
)
raise InterruptProcessingException()
else:
raise Exception(f"Error: {e}")
return False
async def uploadImage(imageDataUri):
uploadTaskConfig = [
{"taskType": "imageUpload", "taskUUID": genRandUUID(), "image": imageDataUri}
]
try:
uploadResult = inferenecRequest(uploadTaskConfig)
if (
uploadResult
and "data" in uploadResult
and "imageUUID" in uploadResult["data"][0]
):
imageUUID = uploadResult["data"][0]["imageUUID"]
return imageUUID
except Exception as e:
return False
return False
async def uploadAndCacheImage(imgSig: str, imgDataUri: str):
try:
imageUUID = await uploadImage(imgDataUri)
if imageUUID:
await imageStoreSet(imgSig, imageUUID)
except Exception as e:
return False
async def imageStoreSet(imgHash: str, imgUUID: str) -> bool:
try:
with open(IMAGE_CACHE_FILE, "r") as f:
cache = json.load(f)
expires = (datetime.now() + timedelta(days=30)).isoformat()
cache[imgHash] = {"uuid": imgUUID, "expires": expires}
with open(IMAGE_CACHE_FILE, "w") as f:
json.dump(cache, f, indent=2)
return True
except Exception as e:
return False
def imageStoreGet(imgHash: str) -> str | bool:
try:
with open(IMAGE_CACHE_FILE, "r") as f:
cache = json.load(f)
if imgHash not in cache:
return False
entry = cache[imgHash]
expires = datetime.fromisoformat(entry["expires"])
if datetime.now() > expires:
del cache[imgHash]
with open(IMAGE_CACHE_FILE, "w") as f:
json.dump(cache, f, indent=2)
return False
return entry["uuid"]
except Exception as e:
return False
def convertTensor2IMG(tensorImage):
global ENABLE_IMAGES_CACHING, MIN_IMAGE_CACHE_SIZE
ENABLE_IMAGES_CACHING = getEnableImagesCaching()
MIN_IMAGE_CACHE_SIZE = int(os.getenv("RUNWARE_MIN_IMAGE_CACHE_SIZE"))
imageNP = (tensorImage.squeeze().numpy() * 255).astype(np.uint8)
imgSig = hashlib.sha256(imageNP.tobytes()).hexdigest()
if ENABLE_IMAGES_CACHING:
imgUUID = imageStoreGet(imgSig)
if imgUUID:
return imgUUID
image = Image.fromarray(imageNP)
with io.BytesIO() as buffer:
image.save(
buffer, format="webp", quality=100, subsampling=0, method=6, exact=True
)
imageRawData = buffer.getvalue()
imgBytes = len(imageRawData)
imgSize = int(imgBytes / 1024)
imgb64 = base64.b64encode(imageRawData).decode("utf-8")
imgDataUri = f"data:image/webp;base64,{imgb64}"
if ENABLE_IMAGES_CACHING and imgSize >= MIN_IMAGE_CACHE_SIZE:
try:
loop = asyncio.get_running_loop()
loop.call_soon(lambda: asyncio.ensure_future(uploadAndCacheImage(imgSig, imgDataUri)))
except RuntimeError:
def run_coro():
new_loop = asyncio.new_event_loop()
asyncio.set_event_loop(new_loop)
new_loop.run_until_complete(uploadAndCacheImage(imgSig, imgDataUri))
new_loop.close()
threading.Thread(target=run_coro).start()
return imgDataUri
def convertTensor2IMGForVideo(tensorImage):
"""Convert tensor to image and force upload to get UUID for video frame images"""
imageNP = (tensorImage.squeeze().numpy() * 255).astype(np.uint8)
imgSig = hashlib.sha256(imageNP.tobytes()).hexdigest()
# Check if already cached
imgUUID = imageStoreGet(imgSig)
if imgUUID:
return imgUUID
# Convert to data URI - use PNG for video frame images
image = Image.fromarray(imageNP)
with io.BytesIO() as buffer:
image.save(buffer, format="PNG")
imageRawData = buffer.getvalue()
imgb64 = base64.b64encode(imageRawData).decode("utf-8")
imgDataUri = f"data:image/png;base64,{imgb64}"
# Force upload to get UUID - use synchronous approach to avoid event loop conflicts
try:
# Use synchronous upload instead of async
uploadTaskConfig = [
{"taskType": "imageUpload", "taskUUID": genRandUUID(), "image": imgDataUri}
]
uploadResult = inferenecRequest(uploadTaskConfig)
if (
uploadResult
and "data" in uploadResult
and "imageUUID" in uploadResult["data"][0]
):
uploaded_uuid = uploadResult["data"][0]["imageUUID"]
# Cache the UUID synchronously
try:
with open(IMAGE_CACHE_FILE, "r") as f:
cache = json.load(f)
expires = (datetime.now() + timedelta(days=30)).isoformat()
cache[imgSig] = {"uuid": uploaded_uuid, "expires": expires}
with open(IMAGE_CACHE_FILE, "w") as f:
json.dump(cache, f, indent=2)
except Exception as e:
print(f"[Warning] Error caching image: {e}")
return uploaded_uuid
else:
print("[Warning] Failed to upload image, returning data URI")
return imgDataUri
except Exception as e:
print(f"[Warning] Error uploading image: {e}")
return imgDataUri
def convertIMG2Tensor(b64img):
imgbytes = base64.b64decode(b64img)
image = Image.open(io.BytesIO(imgbytes))
imageNP = np.array(image).astype(np.float32) / 255.0
tensorImage = torch.from_numpy(imageNP).squeeze()
return tensorImage
def convertImageB64List(imageDataObject):
images = ()
for result in imageDataObject["data"]:
generatedImage = result.get(
"imageBase64Data",
result.get(
"maskImageBase64Data", result.get("guideImageBase64Data", False)
),
)
generatedImage = convertIMG2Tensor(generatedImage)
images += (generatedImage,)
images = torch.stack(images, dim=0)
return images
class VideoObject:
def __init__(self, video_url, width=None, height=None):
self.video_url = video_url
# Use provided dimensions or default to 864x480 for Seedance Lite
self.width = width if width is not None else 864
self.height = height if height is not None else 480
def get_dimensions(self):
return (self.width, self.height)
def save_to(self, filename, **kwargs):
"""Save video to file by downloading from URL with retry logic"""
max_retries = 10
retry_delays = [2, 5, 10, 15, 20] # Longer delays for video server issues
for attempt in range(max_retries):
try:
response = requests.get(self.video_url, stream=True, timeout=30)
print(f"[Video Download] Attempt {attempt + 1}: {response}")
if response.status_code == 422:
print(f"[Video Download] Server still processing, waiting...")
time.sleep(retry_delays[min(attempt, len(retry_delays) - 1)])
continue
if response.status_code == 502:
print(f"[Video Download] Server error (502), retrying in {retry_delays[min(attempt, len(retry_delays) - 1)]} seconds...")
time.sleep(retry_delays[min(attempt, len(retry_delays) - 1)])
continue
response.raise_for_status()
with open(filename, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"[Video Download] Successfully downloaded video to {filename}")
return True
except requests.exceptions.RequestException as e:
print(f"[Video Download] Attempt {attempt + 1} failed: {e}")
if attempt < max_retries - 1:
time.sleep(retry_delays[min(attempt, len(retry_delays) - 1)])
continue
else:
print(f"[Video Download] All {max_retries} attempts failed. Video URL: {self.video_url}")
return False
except Exception as e:
print(f"[Video Download] Unexpected error: {e}")
return False
return False
def __str__(self):
return f"VideoObject(url={self.video_url}, dimensions={self.width}x{self.height})"
def convertVideoB64List(videoDataObject, width=None, height=None):
videos = ()
for result in videoDataObject["data"]:
generatedVideo = result.get("videoBase64Data", False)
if generatedVideo:
# For base64 data, create a video object (would need proper decoding in full implementation)
video_obj = VideoObject(f"data:video/mp4;base64,{generatedVideo}", width, height)
videos += (video_obj,)
else:
# If no base64 data, try to get video URL
videoURL = result.get("videoURL", False)
if videoURL:
# Create a proper video object that ComfyUI can handle
video_obj = VideoObject(videoURL, width, height)
videos += (video_obj,)
return videos
def pollVideoResult(taskUUID):
"""Poll for video generation result using task UUID with getResponse"""
global RUNWARE_API_KEY, RUNWARE_API_BASE_URL, SESSION_TIMEOUT
pollConfig = [
{
"taskType": "getResponse",
"taskUUID": taskUUID,
}
]
headers = {
"Authorization": f"Bearer {RUNWARE_API_KEY}",
"Content-Type": "application/json",
"Accept-Encoding": "gzip, deflate, br, zstd",
}
try:
def recaller():
return session.post(
RUNWARE_API_BASE_URL,
headers=headers,
json=pollConfig,
timeout=30, # Shorter timeout for polling
allow_redirects=False,
stream=True,
)
pollResult = generalRequestWrapper(recaller)
try:
pollResult = pollResult.json()
except json.JSONDecodeError as e:
print(f"[Debugging] Runware Poll JSON Decode Error: {str(e)}")
return None
if "errors" in pollResult:
print(f"[Debugging] Poll error: {pollResult}")
return pollResult
else:
return pollResult
except Exception as e:
print(f"[Debugging] Poll request failed: {str(e)}")
return None
+1
View File
@@ -10,6 +10,7 @@ class vaeSearch:
"All",
"FLUX.1-Schnell",
"FLUX.1-Dev",
"FLUX.1 Krea",
"Pony",
"SD 1.5",
"SD 1.5 Hyper",
+342
View File
@@ -0,0 +1,342 @@
from .utils import runwareUtils as rwUtils
from .videoModelSearch import videoModelSearch
class RunwareFrameImages:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {},
"optional": {
"image1": ("IMAGE", {
"tooltip": "First frame image. When only one image is provided, it becomes the first frame automatically.",
}),
"frame1_position": (["auto", "first", "last"], {
"default": "auto",
"tooltip": "Position for the first image. 'auto' uses automatic distribution rules.",
}),
"image2": ("IMAGE", {
"tooltip": "Second frame image. With two images, they become first and last frames automatically.",
}),
"frame2_position": (["auto", "first", "last"], {
"default": "auto",
"tooltip": "Position for the second image. 'auto' uses automatic distribution rules.",
}),
"image3": ("IMAGE", {
"tooltip": "Third frame image. Will be distributed between first and last frames.",
}),
"frame3_position": (["auto", "first", "last"], {
"default": "auto",
"tooltip": "Position for the third image. 'auto' uses automatic distribution rules.",
}),
"image4": ("IMAGE", {
"tooltip": "Fourth frame image. Will be distributed between first and last frames.",
}),
"frame4_position": (["auto", "first", "last"], {
"default": "auto",
"tooltip": "Position for the fourth image. 'auto' uses automatic distribution rules.",
}),
}
}
DESCRIPTION = "Configure frame images for video generation with precise positioning control. Supports automatic distribution or manual positioning."
FUNCTION = "create_frame_images"
RETURN_TYPES = ("RUNWAREFRAMEIMAGES",)
RETURN_NAMES = ("Frame Images",)
CATEGORY = "Runware"
def create_frame_images(self, **kwargs):
frame_images = []
# 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"
image = kwargs.get(image_key)
position = kwargs.get(position_key, "auto")
if image is not None:
# Upload image and get UUID
image_uuid = rwUtils.convertTensor2IMGForVideo(image)
image_url = f"https://im.runware.ai/image/ii/{image_uuid}.webp"
frame_data = {
"inputImage": image_url
}
# Only add frame position if not auto
if position != "auto":
frame_data["frame"] = position
frame_images.append(frame_data)
return (frame_images,)
class txt2vid:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"Model": ("RUNWAREVIDEOMODEL", {
"tooltip": "Connect a Runware Video Model From Runware Video Model Search Node.",
}),
"positivePrompt": ("STRING", {
"multiline": True,
"placeholder": "Positive Prompt: a text instruction to guide the model on generating the video. 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 video. 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 video. 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 video."
}),
"Multi Inference Mode": ("BOOLEAN", {
"tooltip": "If Enabled the node will skip the video 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 video.\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\"",
}),
"useCustomDimensions": ("BOOLEAN", {
"tooltip": "Model Default: Uses optimal dimensions for selected model. Custom: Uses width/height values below.",
"default": False,
"label_on": "Custom",
"label_off": "Model Default",
}),
"width": ("INT", {
"tooltip": "Width in pixels. Only used when 'Custom' is selected above.",
"default": 1024,
"min": 256,
"max": 1920,
"step": 1,
}),
"height": ("INT", {
"tooltip": "Height in pixels. Only used when 'Custom' is selected above.",
"default": 576,
"min": 256,
"max": 1920,
"step": 1,
}),
"duration": ("INT", {
"tooltip": "The duration of the video in seconds.",
"default": 5,
"min": 1,
"max": 30,
}),
"fps": ("INT", {
"tooltip": "Frames per second for the generated video. Only used when 'Use FPS' is enabled.",
"default": 24,
"min": 8,
"max": 60,
}),
"useFps": (["enable", "disabled"], {
"tooltip": "Enable to include fps parameter in API request. Disable if your model doesn't support fps.",
"default": "enable",
}),
"outputFormat": (["mp4", "webm", "mov"], {
"default": "mp4",
"tooltip": "Choose the output video format.",
}),
"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. Note: Only supported by Wan models.",
"default": rwUtils.genRandSeed(),
"min": 1,
"max": 9223372036854776000,
}),
"batchSize": ("INT", {
"tooltip": "The number of videos to generate in a single request.",
"default": 1,
"min": 1,
"max": 4,
}),
},
"optional": {
"frameImages": ("RUNWAREFRAMEIMAGES", {
"tooltip": "Frame images configuration from Runware Frame Images node. Allows precise control over frame positioning.",
}),
"referenceImages": ("RUNWAREREFERENCEIMAGES", {
"tooltip": "Connect a Runware Reference Images node to provide reference images for the subject. These reference images help the AI maintain identity consistency during the video generation process.",
}),
"providerSettings": ("DICT", {
"tooltip": "Provider-specific settings for video generation.",
}),
}
}
@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 Videos Lightning Fast With Runware Video Inference Engine."
FUNCTION = "generateVideo"
RETURN_TYPES = ("VIDEO", "RUNWARETASK")
RETURN_NAMES = ("VIDEO", "RW-Task")
CATEGORY = "Runware"
def generateVideo(self, **kwargs):
runwareVideoModel = 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")
providerSettings = kwargs.get("providerSettings", None)
frameImages = kwargs.get("frameImages", None)
referenceImages = kwargs.get("referenceImages", None)
useCustomDimensions = kwargs.get("useCustomDimensions", False)
customWidth = kwargs.get("width", 864)
customHeight = kwargs.get("height", 480)
duration = kwargs.get("duration", 5)
fps = kwargs.get("fps", 24)
useFps = kwargs.get("useFps", "enable")
outputFormat = kwargs.get("outputFormat", "mp4")
batchSize = kwargs.get("batchSize", 1)
seed = kwargs.get("seed", 1)
# Handle model input - could be dict or string
if isinstance(runwareVideoModel, dict):
model = runwareVideoModel.get("model", "")
else:
model = runwareVideoModel
# Determine dimensions based on custom setting
if useCustomDimensions:
width = customWidth
height = customHeight
else:
model_dimensions = videoModelSearch.MODEL_DIMENSIONS.get(model, {"width": 1024, "height": 576})
width = model_dimensions["width"]
height = model_dimensions["height"]
genConfig = [
{
"taskType": "videoInference",
"taskUUID": rwUtils.genRandUUID(),
"positivePrompt": positivePrompt,
"height": height,
"width": width,
"model": model,
"outputFormat": outputFormat,
"numberResults": batchSize,
"includeCost": True,
}
]
# Add fps parameter only if enabled
if useFps == "enable":
genConfig[0]["fps"] = fps
# Add duration parameter - unified for all models
genConfig[0]["duration"] = duration
# Add seed parameter only for supported models
if model in videoModelSearch.SEED_SUPPORTED_MODELS:
genConfig[0]["seed"] = seed
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"
# Add frameImages if provided from Runware Frame Images node
if frameImages is not None and len(frameImages) > 0:
genConfig[0]["frameImages"] = frameImages
print(f"[Debugging] Frame images array: {frameImages}")
# Add referenceImages if provided from Runware Reference Images node
if referenceImages is not None and len(referenceImages) > 0:
genConfig[0]["referenceImages"] = referenceImages
print(f"[Debugging] Reference images array: {referenceImages}")
if (providerSettings is not None):
genConfig[0]["providerSettings"] = providerSettings
if (multiInferenceMode):
return (None, genConfig)
else:
try:
genResult = rwUtils.inferenecRequest(genConfig)
print(f"[Debugging] Generation config: {genConfig}")
except Exception as e:
# Check if it's a dimension error and provide helpful information
error_msg = str(e)
if ("unsupported width" in error_msg.lower() or
"unsupported height" in error_msg.lower() or
"invalid width" in error_msg.lower() or
"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"]
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}.")
else:
# Re-raise the original error
raise e
# Extract task UUID for polling
taskUUID = genConfig[0]["taskUUID"]
# Poll for video completion
while True:
# 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")
# Extract more detailed error info if available
if "responseContent" in error_info:
response_content = error_info["responseContent"]
detailed_message = response_content.get("message", "")
if detailed_message:
error_message = f"{error_message}\nProvider Error: {detailed_message}"
raise Exception(f"Video generation failed: {error_message}")
if pollResult and "data" in pollResult and len(pollResult["data"]) > 0:
video_data = pollResult["data"][0]
# 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, width, height)
return videos
# If status is "processing", continue polling
# Wait before next poll
rwUtils.time.sleep(1)
# Node class mappings for ComfyUI registration
NODE_CLASS_MAPPINGS = {
"RunwareFrameImages": RunwareFrameImages,
"txt2vid": txt2vid,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"RunwareFrameImages": "Runware Frame Images",
"txt2vid": "Runware Video Inference",
}
+163
View File
@@ -0,0 +1,163 @@
class videoModelSearch:
# Define the video models by architecture with their default dimensions
VIDEO_MODELS = {
"KlingAI": [
"klingai:1@2 (KlingAI V1.0 Pro)",
"klingai:1@1 (KlingAI V1 Standard)",
"klingai:2@2 (KlingAI V1.5 Pro)",
"klingai:2@1 (KlingAI V1.5 Standard)",
"klingai:3@1 (KlingAI V1.6 Standard)",
"klingai:3@2 (KlingAI V1.6 Pro)",
"klingai:4@3 (KlingAI V2.1 Master)",
"klingai:5@1 (KlingAI V2.1 Standard (I2V))",
"klingai:5@2 (KlingAI V2.1 Pro (I2V))",
"klingai:5@3 (KlingAI V2.0 Master)",
],
"Veo": [
"google:2@0 (Veo 2.0)",
"google:3@0 (Veo 3.0)",
"google:3@1 (Veo 3.0 Fast)",
],
"Seedance": [
"bytedance:2@1 (Seedance 1.0 Pro)",
"bytedance:1@1 (Seedance 1.0 Lite)",
],
"MiniMax": [
"minimax:1@1 (MiniMax 01 Base)",
"minimax:2@1 (MiniMax 01 Director)",
"minimax:2@3 (MiniMax I2V 01 Live)",
"minimax:3@1 (MiniMax 02 Hailuo)",
],
"PixVerse": [
"pixverse:1@1 (PixVerse v3.5)",
"pixverse:1@2 (PixVerse v4)",
"pixverse:1@3 (PixVerse v4.5)",
],
"Vidu": [
"vidu:1@0 (Vidu Q1 Classic)",
"vidu:1@1 (Vidu Q1)",
"vidu:1@5 (Vidu 1.5)",
"vidu:2@0 (Vidu 2.0)",
],
"Wan": [
"runware:200@1 (Wan 2.1 1.3B)",
"runware:200@2 (Wan 2.1 14B)",
],
}
# 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
# 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
# Seedance Models
"bytedance:2@1": {"width": 864, "height": 480}, # Seedance 1.0 Pro
"bytedance:1@1": {"width": 864, "height": 480}, # Seedance 1.0 Lite
# 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
# 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
# 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
# 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
}
# Models that support seed parameter
SEED_SUPPORTED_MODELS = [
"runware:200@1", # Wan 2.1 1.3B
"runware:200@2", # Wan 2.1 14B
]
@classmethod
def INPUT_TYPES(cls):
# Get all models for "All" option
all_models = []
for models in cls.VIDEO_MODELS.values():
all_models.extend(models)
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"
], {
"tooltip": "Choose Video Model Architecture To Filter Results.",
"default": "KlingAI",
}),
"VideoList": (all_models, {
"tooltip": "Video Model Results Will Show UP Here So You Could Choose From.",
"default": "klingai:5@3 (KlingAI V2.0 Master)",
}),
"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.",
"default": False,
"label_on": "Enabled",
"label_off": "Disabled",
}),
},
}
DESCRIPTION = "Directly Search and Connect Video Models to Runware Video Inference Nodes In ComfyUI."
FUNCTION = "videoModelSearch"
RETURN_TYPES = ("RUNWAREVIDEOMODEL", "INT", "INT")
RETURN_NAMES = ("Runware Video Model", "Width", "Height")
CATEGORY = "Runware"
@classmethod
def VALIDATE_INPUTS(cls, VideoList):
return True
def videoModelSearch(self, **kwargs):
enableSearchValue = kwargs.get("Use Search Value", False)
searchInput = kwargs.get("Model Search")
if enableSearchValue:
modelAIRCode = searchInput
else:
crModel = kwargs.get("VideoList")
# Extract the full AIR identifier (everything before the first space)
modelAIRCode = crModel.split(" (")[0]
# Get dimensions for the selected model
dimensions = self.MODEL_DIMENSIONS.get(modelAIRCode, {"width": 1024, "height": 576})
return ({
"model": modelAIRCode,
}, dimensions["width"], dimensions["height"])
BIN
View File
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
{"last_node_id":5,"last_link_id":6,"nodes":[{"id":5,"type":"Runware Background Removal","pos":[481.94317626953125,-300.1940612792969],"size":[333.4461364746094,178],"flags":{},"order":1,"mode":0,"inputs":[{"name":"Image","type":"IMAGE","link":5}],"outputs":[{"name":"IMAGE","type":"IMAGE","links":[6],"slot_index":0}],"properties":{"Node name for S&R":"Runware Background Removal"},"widgets_values":[false,false,false,240,10,10],"bgcolor":"#6c5ce7"},{"id":2,"type":"PreviewImage","pos":[479.01971435546875,-74.14266204833984],"size":[343.48712158203125,246],"flags":{},"order":2,"mode":0,"inputs":[{"name":"images","type":"IMAGE","link":6}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"}},{"id":1,"type":"LoadImage","pos":[91.22550201416016,-285.5789794921875],"size":[363.7179260253906,459.1794128417969],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[{"name":"IMAGE","type":"IMAGE","links":[5],"slot_index":0},{"name":"MASK","type":"MASK","links":null}],"properties":{"Node name for S&R":"LoadImage"},"widgets_values":["r2rs1PrivateFLux.png","image"]}],"links":[[5,1,0,5,0,"IMAGE"],[6,5,0,2,0,"IMAGE"]],"groups":[],"config":{},"extra":{"ds":{"scale":1.0263162364614136,"offset":[464.15888590882696,406.82531520554556]}},"version":0.4}
{"id":"1c4ca198-5330-4190-8be6-2c7eb8a0abcb","revision":0,"last_node_id":5,"last_link_id":6,"nodes":[{"id":2,"type":"PreviewImage","pos":[479.01971435546875,-74.14266204833984],"size":[343.48712158203125,246],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":6}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.46","Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":1,"type":"LoadImage","pos":[91.22550201416016,-285.5789794921875],"size":[363.7179260253906,459.1794128417969],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","slot_index":0,"links":[5]},{"localized_name":"MASK","name":"MASK","type":"MASK","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.3.46","Node name for S&R":"LoadImage"},"widgets_values":["bee-on-flower-1024x576.jpg","image"]},{"id":5,"type":"Runware Background Removal","pos":[481.94317626953125,-300.1940612792969],"size":[333.4461364746094,226],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"Image","name":"Image","type":"IMAGE","link":5},{"localized_name":"Model","name":"Model","type":"COMBO","widget":{"name":"Model"},"link":null},{"localized_name":"Post Process Mask","name":"Post Process Mask","shape":7,"type":"BOOLEAN","widget":{"name":"Post Process Mask"},"link":null},{"localized_name":"Return Only Mask","name":"Return Only Mask","shape":7,"type":"BOOLEAN","widget":{"name":"Return Only Mask"},"link":null},{"localized_name":"Alpha Matting","name":"Alpha Matting","shape":7,"type":"BOOLEAN","widget":{"name":"Alpha Matting"},"link":null},{"localized_name":"Alpha Matting Foreground Threshold","name":"Alpha Matting Foreground Threshold","shape":7,"type":"INT","widget":{"name":"Alpha Matting Foreground Threshold"},"link":null},{"localized_name":"Alpha Matting Background Threshold","name":"Alpha Matting Background Threshold","shape":7,"type":"INT","widget":{"name":"Alpha Matting Background Threshold"},"link":null},{"localized_name":"Alpha Matting Erode Size","name":"Alpha Matting Erode Size","shape":7,"type":"INT","widget":{"name":"Alpha Matting Erode Size"},"link":null},{"localized_name":"outputFormat","name":"outputFormat","shape":7,"type":"COMBO","widget":{"name":"outputFormat"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","slot_index":0,"links":[6]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"de0a4c2e258a1f3a104d0f9994f9b7ea66e56736","Node name for S&R":"Runware Background Removal"},"widgets_values":["RemBG 1.4",false,false,240,10,10,10,"WEBP"],"bgcolor":"#6c5ce7"}],"links":[[5,1,0,5,0,"IMAGE"],[6,5,0,2,0,"IMAGE"]],"groups":[],"config":{},"extra":{"ds":{"scale":1.0263162364614136,"offset":[464.15888590882696,406.82531520554556]}},"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
+1 -1
View File
@@ -1 +1 @@
{"last_node_id":5,"last_link_id":3,"nodes":[{"id":4,"type":"Runware Image Upscaler","pos":[139.44570922851562,-279.625],"size":[315,58],"flags":{},"order":1,"mode":0,"inputs":[{"name":"Image","type":"IMAGE","link":3}],"outputs":[{"name":"IMAGE","type":"IMAGE","links":[2],"slot_index":0}],"properties":{"Node name for S&R":"Runware Image Upscaler"},"widgets_values":[4],"bgcolor":"#6c5ce7"},{"id":1,"type":"PreviewImage","pos":[497.6885986328125,-288.6935729980469],"size":[593.5640869140625,614.8204345703125],"flags":{},"order":2,"mode":0,"inputs":[{"name":"images","type":"IMAGE","link":2}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":5,"type":"LoadImage","pos":[140.2388916015625,-145.11216735839844],"size":[320.84613037109375,411.43585205078125],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[{"name":"IMAGE","type":"IMAGE","links":[3],"slot_index":0},{"name":"MASK","type":"MASK","links":null}],"properties":{"Node name for S&R":"LoadImage"},"widgets_values":["qwd2s.jpg","image"]}],"links":[[2,4,0,1,0,"IMAGE"],[3,5,0,4,0,"IMAGE"]],"groups":[],"config":{},"extra":{"ds":{"scale":1.0263162364614136,"offset":[452.4665833088276,458.4663183555447]}},"version":0.4}
{"id":"99bb8e7d-3e8e-42a4-90cc-c383ca9ecc3b","revision":0,"last_node_id":5,"last_link_id":3,"nodes":[{"id":4,"type":"Runware Image Upscaler","pos":[139.44570922851562,-279.625],"size":[315,58],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"Image","name":"Image","type":"IMAGE","link":3},{"localized_name":"upscaleFactor","name":"upscaleFactor","type":"INT","widget":{"name":"upscaleFactor"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","slot_index":0,"links":[2]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"de0a4c2e258a1f3a104d0f9994f9b7ea66e56736","Node name for S&R":"Runware Image Upscaler"},"widgets_values":[4],"bgcolor":"#6c5ce7"},{"id":1,"type":"PreviewImage","pos":[497.6885986328125,-288.6935729980469],"size":[593.5640869140625,614.8204345703125],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":2}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.46","Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":5,"type":"LoadImage","pos":[140.2388916015625,-145.11216735839844],"size":[320.84613037109375,411.43585205078125],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","slot_index":0,"links":[3]},{"localized_name":"MASK","name":"MASK","type":"MASK","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.3.46","Node name for S&R":"LoadImage"},"widgets_values":["bee-on-flower-1024x576.jpg","image"]}],"links":[[2,4,0,1,0,"IMAGE"],[3,5,0,4,0,"IMAGE"]],"groups":[],"config":{},"extra":{"ds":{"scale":1.0263162364614136,"offset":[452.4665833088276,458.4663183555447]}},"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