diff --git a/__init__.py b/__init__.py index 2913dcc..e228ae3 100644 --- a/__init__.py +++ b/__init__.py @@ -15,9 +15,11 @@ from .modules.imageCaptioning import imageCaptioning from .modules.controlNetCombine import controlNetCombine from .modules.embeddingSearch import embeddingSearch from .modules.embeddingsCombine import embeddingsCombine +from .modules.ipAdapter import ipAdapter +from .modules.ipAdapterCombine import ipAdapterCombine from .modules.vaeSearch import vaeSearch -RUNWARE_COMFYUI_VERSION = "1.0.0 Beta" +RUNWARE_COMFYUI_VERSION = "1.0.2 Beta" RESET_COLOR = "\033[0m" BLUE_COLOR = "\033[94m" @@ -46,6 +48,8 @@ NODE_CLASS_MAPPINGS = { "Runware API Manager": apiManager, "Runware Image Caption": imageCaptioning, "Runware ControlNet Combine": controlNetCombine, + "Runware IPAdapter": ipAdapter, + "Runware IPAdapters Combine": ipAdapterCombine, } NODE_DISPLAY_NAME_MAPPINGS = { diff --git a/modules/imageInference.py b/modules/imageInference.py index 2b4464e..83a3055 100644 --- a/modules/imageInference.py +++ b/modules/imageInference.py @@ -104,6 +104,9 @@ class txt2img: "Lora": ("RUNWARELORA", { "tooltip": "Connect a Runware Lora From Lora Search Node Or Lora Combine For Multiple Lora's Together.", }), + "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.", }), @@ -147,6 +150,7 @@ class txt2img: promptWeighting = kwargs.get("Prompt Weighting", "Disabled") runwareControlNet = kwargs.get("ControlNet", None) runwareLora = kwargs.get("Lora", None) + runwareIPAdapters = kwargs.get("IPAdapters", None) runwareRefiner = kwargs.get("Refiner", None) runwareEmbedding = kwargs.get("Embeddings", None) runwareVAE = kwargs.get("VAE", None) @@ -202,6 +206,11 @@ class txt2img: 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 diff --git a/modules/ipAdapter.py b/modules/ipAdapter.py new file mode 100644 index 0000000..e7b83f3 --- /dev/null +++ b/modules/ipAdapter.py @@ -0,0 +1,50 @@ +from .utils import runwareUtils as rwUtils + +class ipAdapter: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "Reference Image": ("IMAGE",), + "Model": ([ + "runware:55@1 (IP Adapter SDXL)", + "runware:55@2 (IP Adapter SDXL Plus)", + "runware:55@3 (IP Adapter SDXL Plus Face)", + "runware:55@4 (IP Adapter SDXL Vit-H)", + "runware:55@5 (IP Adapter SD 1.5)", + "runware:55@6 (IP Adapter SD 1.5 Plus)", + "runware:55@7 (IP Adapter SD 1.5 Light)", + "runware:55@8 (IP Adapter SD 1.5 Plus Face)", + "runware:55@9 (IP Adapter SD 1.5 Full Face)", + "runware:55@10 (IP Adapter SD 1.5 Vit-G)" + ], { + "tooltip": "Choose IP Adapter model to use for reference-based image generation.", + "default": "runware:55@1 (IP Adapter SDXL)", + }), + "weight": ("FLOAT", { + "default": 1.0, + "min": 0.0, + "max": 1.0, + "step": 0.1, + "tooltip": "Represents the strength or influence of this IP-Adapter in the generation process.\nA value of 0 means no influence, while 1 means maximum influence.", + }), + }, + } + + RETURN_TYPES = ("RUNWAREIPAdapter",) + RETURN_NAMES = ("IPAdapter",) + FUNCTION = "ipAdapter" + CATEGORY = "Runware" + DESCRIPTION = "IP-Adapters enable image-prompted generation, allowing you to use reference images to guide the style and content of your generations. Multiple IP Adapters can be used simultaneously." + + def ipAdapter(self, **kwargs): + refImage = kwargs.get("Reference Image") + Model = kwargs.get("Model") + weight = kwargs.get("weight") + modelAirCode = Model.split(" ")[0] + guideImage = rwUtils.convertTensor2IMG(refImage) + return ({ + "model": modelAirCode, + "guideImage": guideImage, + "weight": round(weight, 2), + },) \ No newline at end of file diff --git a/modules/ipAdapterCombine.py b/modules/ipAdapterCombine.py new file mode 100644 index 0000000..25777c2 --- /dev/null +++ b/modules/ipAdapterCombine.py @@ -0,0 +1,37 @@ +class ipAdapterCombine: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "IPAdapter 1": ("RUNWAREIPAdapter", { + "tooltip": "Connect an IPAdapter from Runware IPAdapter node.", + }), + }, + "optional": { + "IPAdapter 2": ("RUNWAREIPAdapter", { + "tooltip": "Connect an IPAdapter from Runware IPAdapter node.", + }), + "IPAdapter 3": ("RUNWAREIPAdapter", { + "tooltip": "Connect an IPAdapter from Runware IPAdapter node.", + }), + }, + } + + DESCRIPTION = "Combine multiple IPAdapters to connect with Runware Image Inference." + FUNCTION = "ipAdapterCombine" + RETURN_TYPES = ("RUNWAREIPAdapter",) + RETURN_NAMES = ("IPAdapters",) + CATEGORY = "Runware" + + def ipAdapterCombine(self, **kwargs): + ipAdapter1 = kwargs.get("IPAdapter 1") + ipAdapter2 = kwargs.get("IPAdapter 2", None) + ipAdapter3 = kwargs.get("IPAdapter 3", None) + + ipAdapterArray = [] + ipAdapterArray.append(ipAdapter1) + if(ipAdapter2): + ipAdapterArray.append(ipAdapter2) + if(ipAdapter3): + ipAdapterArray.append(ipAdapter3) + return (ipAdapterArray,) \ No newline at end of file diff --git a/modules/utils/runwareUtils.py b/modules/utils/runwareUtils.py index 85e4292..e526b3b 100644 --- a/modules/utils/runwareUtils.py +++ b/modules/utils/runwareUtils.py @@ -10,6 +10,7 @@ import random import base64 import torch import uuid +import time import os import io @@ -23,11 +24,26 @@ RUNWARE_REMBG_OUTPUT_FORMATS = { } RUNWARE_API_BASE_URL = "https://api.runware.ai/v1" +MAX_RETRIES = 2 +RETRY_COOLDOWN = 2 + 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: + time.sleep(RETRY_COOLDOWN) + continue + return False + def getAPIKey(): apiKey = os.getenv("RUNWARE_API_KEY") if apiKey and isinstance(apiKey, str) and len(apiKey) > 24: @@ -138,7 +154,10 @@ def checkAPIKey(apiKey): }] try: - genResult = session.post(RUNWARE_API_BASE_URL, headers=headers, json=genConfig, timeout=10, allow_redirects=False, stream=True) + 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"]) @@ -165,7 +184,10 @@ def inferenecRequest(genConfig): } try: - genResult = session.post(RUNWARE_API_BASE_URL, headers=headers, json=genConfig, timeout=SESSION_TIMEOUT, allow_redirects=False, stream=True) + def recaller(): + return session.post(RUNWARE_API_BASE_URL, headers=headers, json=genConfig, timeout=SESSION_TIMEOUT, allow_redirects=False, stream=True) + + genResult = generalRequestWrapper(recaller) genResult = genResult.json() if("errors" in genResult): raise Exception(genResult["errors"][0]["message"])