From 1fbb853e863a41fadf3d9305ad6198d634a40d29 Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Wed, 3 Sep 2025 14:20:18 -0400 Subject: [PATCH] feat: Add video inference features and improvements - Add Runware Video Inference node with frame images and reference images support - Add conditional parameter support for steps and CFG scale in image inference - Add useFps parameter for video inference with enable/disabled options for Vidu support --- .DS_Store | Bin 0 -> 6148 bytes README.md | 11 +- __init__.py | 4 +- modules/apiManager.py | 124 ++++++------ modules/frameImages.py | 87 +++++++++ modules/imageInference.py | 26 ++- modules/multiInference.py | 3 +- modules/utils/runwareUtils.py | 31 ++- modules/videoInference.py | 176 +++++++++++++----- workflows/.DS_Store | Bin 0 -> 6148 bytes workflows/Runware_Adetailer.json | 2 +- workflows/Runware_Basic_Image_Inference.json | 2 +- ...nware_Basic_Image_Inference_IPAdapter.json | 2 +- .../Runware_Basic_Image_Inference_Lora.json | 2 +- workflows/Runware_ControlNet.json | 2 +- .../Runware_Image_Inference_Captioning.json | 2 +- .../Runware_Image_Inference_Embedding.json | 2 +- workflows/Runware_Image_to_Video.json | 2 +- workflows/Runware_Inpainting.json | 2 +- workflows/Runware_PhotoMaker_v2.json | 2 +- workflows/Runware_Text_To_Video.json.json | 2 +- 21 files changed, 340 insertions(+), 144 deletions(-) create mode 100644 .DS_Store create mode 100644 modules/frameImages.py create mode 100644 workflows/.DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..d2717f3b9c773dd92ce2308d309dc65953a6ab8d GIT binary patch literal 6148 zcmeHK%}T>S5T0$TO(;SR3OxqA7OZV4h?h|73mDOZN^MBh(3q8`HHT8jSzpK}@p+ut z-H4?XJc-yDnEhtwC(C{t_6Gn&Zx$W`H~?Uw5|(Ui)(FK(SEOV-ghGAe4q}jyK^kN$ z+3fg>4A9!yxMC6}ForMdmp%ot3^4i@JjGEuZ8qOUsa)CGu3A;AX5IJ?GV`ay=`8IH zCs))vmog3tdl+0qqio>pp2%c6jFQnr6-40(Q?4(gB$QcC&XO=ywZ0Bmwq*~T`h4E% zHXClc(_S>(`B4k)PP@BU*w)_u!SUH(96u)VMbjzp&r`Bvuz*)I*39hLpCqwN?lD?a z78Q)l05iZ0YzhNr|FdeFGGU%4Gr$b|m;u@!TvS5OV6IVZ9oW$Ik>V9X613?pLFgLv z4CWd!f+BP&qAnHYi6L}3`dyRf8O$~6au8}{{Eit}m=}srqodzd=^#9f+%f~qz%m16 z-F4{vKmGpwzg)yUW`G&^R}6?s-|zQuO6F``nH-(9F6u2R3FYM)KTFV1M=|EoQCvq= Zf_|3_M9*NZ5j`k;5l}R6!wmc?10P3+P0|1W literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 2fba480..df37ff9 100644 --- a/README.md +++ b/README.md @@ -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,10 +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 Text to Video**: Generate videos from text prompts using advanced AI models. 🎥 -- **Runware Image to Video**: Create videos from reference images with optional text guidance. 🖼️➡️🎥 +- **Runware Video Inference**: Generate videos from text prompts using advanced AI models with support for frame images and reference images. 🎬 --- diff --git a/__init__.py b/__init__.py index 561e95b..1b997b1 100644 --- a/__init__.py +++ b/__init__.py @@ -27,8 +27,9 @@ 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" @@ -68,6 +69,7 @@ NODE_CLASS_MAPPINGS = { "Runware Reference Images": referenceImages, "Runware Video Inference": txt2vid, "Runware Video Model Search": videoModelSearch, + "Runware Frame Images": RunwareFrameImages, } NODE_DISPLAY_NAME_MAPPINGS = { diff --git a/modules/apiManager.py b/modules/apiManager.py index 924cf3e..5a0d3d5 100644 --- a/modules/apiManager.py +++ b/modules/apiManager.py @@ -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 = () \ No newline at end of file diff --git a/modules/frameImages.py b/modules/frameImages.py new file mode 100644 index 0000000..1ec0fd0 --- /dev/null +++ b/modules/frameImages.py @@ -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", +} diff --git a/modules/imageInference.py b/modules/imageInference.py index 86d6d14..1e3ecf4 100644 --- a/modules/imageInference.py +++ b/modules/imageInference.py @@ -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"): diff --git a/modules/multiInference.py b/modules/multiInference.py index a154031..192bff2 100644 --- a/modules/multiInference.py +++ b/modules/multiInference.py @@ -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!") diff --git a/modules/utils/runwareUtils.py b/modules/utils/runwareUtils.py index d7d5186..534c094 100644 --- a/modules/utils/runwareUtils.py +++ b/modules/utils/runwareUtils.py @@ -479,16 +479,22 @@ class VideoObject: 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 - - while True: + for attempt in range(max_retries): try: response = requests.get(self.video_url, stream=True, timeout=30) - print(response) + print(f"[Video Download] Attempt {attempt + 1}: {response}") if response.status_code == 422: - - time.sleep(1) + 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() @@ -497,10 +503,19 @@ class VideoObject: 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 @@ -561,8 +576,8 @@ def pollVideoResult(taskUUID): return None if "errors" in pollResult: - print(f"[Debugging] Poll error: {pollResult['errors'][0]['message']}") - return None + print(f"[Debugging] Poll error: {pollResult}") + return pollResult else: return pollResult except Exception as e: diff --git a/modules/videoInference.py b/modules/videoInference.py index f98f78e..c5b5c09 100644 --- a/modules/videoInference.py +++ b/modules/videoInference.py @@ -2,6 +2,77 @@ 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 @@ -58,40 +129,38 @@ class txt2vid: "max": 30, }), "fps": ("INT", { - "tooltip": "Frames per second for the generated video.", + "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.", }), - "batchSize": ("INT", { - "tooltip": "The number of videos to generate in a single request.", - "default": 1, - "min": 1, - "max": 4, - }), - "includeCost": ("BOOLEAN", { - "tooltip": "Include cost information in the response.", - "default": True, - "label_on": "Enabled", - "label_off": "Disabled", - }), "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": { - "firstImage": ("IMAGE", { - "tooltip": "Image to use as the first frame of the video.", + "frameImages": ("RUNWAREFRAMEIMAGES", { + "tooltip": "Frame images configuration from Runware Frame Images node. Allows precise control over frame positioning.", }), - "lastImage": ("IMAGE", { - "tooltip": "Image to use as the last frame of the video.", + "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.", @@ -122,16 +191,16 @@ class txt2vid: multiInferenceMode = kwargs.get("Multi Inference Mode", False) promptWeighting = kwargs.get("Prompt Weighting", "Disabled") providerSettings = kwargs.get("providerSettings", None) - firstImage = kwargs.get("firstImage", None) - lastImage = kwargs.get("lastImage", 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) - includeCost = kwargs.get("includeCost", True) seed = kwargs.get("seed", 1) # Handle model input - could be dict or string @@ -159,13 +228,16 @@ class txt2vid: "height": height, "width": width, "model": model, - "fps": fps, "outputFormat": outputFormat, "numberResults": batchSize, - "includeCost": includeCost, + "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 @@ -181,31 +253,14 @@ class txt2vid: genConfig[0]["promptWeighting"] = "sdEmbeds" else: genConfig[0]["promptWeighting"] = "compel" - # Build frameImages array from firstImage and lastImage parameters - frameImages = [] - if (firstImage is not None): - # Force upload to get UUID for video frame images - firstImageUUID = rwUtils.convertTensor2IMGForVideo(firstImage) - # Construct full URL with PNG extension - firstImageURL = f"https://im.runware.ai/image/ii/{firstImageUUID}.webp" - print(f"[Debugging] First image URL: {firstImageURL}") - frameImages.append({ - "inputImage": firstImageURL - }) - - if (lastImage is not None): - # Force upload to get UUID for video frame images - lastImageUUID = rwUtils.convertTensor2IMGForVideo(lastImage) - # Construct full URL with PNG extension - lastImageURL = f"https://im.runware.ai/image/ii/{lastImageUUID}.webp" - print(f"[Debugging] Last image URL: {lastImageURL}") - frameImages.append({ - "inputImage": lastImageURL - }) - - if frameImages: + # 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 @@ -243,6 +298,20 @@ class txt2vid: 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] @@ -254,9 +323,20 @@ class txt2vid: if "videoURL" in video_data or "videoBase64Data" in video_data: videos = rwUtils.convertVideoB64List(pollResult, width, height) return videos - elif status != "processing": - raise Exception(f"Video generation failed: {video_data.get('error', 'Unknown error')}") + # If status is "processing", continue polling # Wait before next poll - rwUtils.time.sleep(1) \ No newline at end of file + 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", +} \ No newline at end of file diff --git a/workflows/.DS_Store b/workflows/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0