From 8ddd9550ebcafc2a58df1915cad712b930982103 Mon Sep 17 00:00:00 2001 From: nofunstudio Date: Sun, 9 Nov 2025 22:34:26 -0700 Subject: [PATCH] wavespeed api node --- __init__.py | 3 + __pycache__/__init__.cpython-312.pyc | Bin 1778 -> 1884 bytes .../load_image_batch_fun.cpython-312.pyc | Bin 9313 -> 8701 bytes .../wavespeed_ai_image.cpython-312.pyc | Bin 0 -> 11761 bytes load_image_batch_fun.py | 61 ++--- wavespeed_ai_image.py | 226 ++++++++++++++++++ 6 files changed, 254 insertions(+), 36 deletions(-) create mode 100644 __pycache__/wavespeed_ai_image.cpython-312.pyc create mode 100644 wavespeed_ai_image.py diff --git a/__init__.py b/__init__.py index 4a7facb..de81cef 100644 --- a/__init__.py +++ b/__init__.py @@ -9,6 +9,7 @@ from .ksampler_fun import KSampler from .fal_recraft_upscale import FalAPI_recraft_upscale from .fal_kling_video import FalAPI_kling_video from .fal_seedance_video import FalAPI_seedance_video +from .wavespeed_ai_image import WaveSpeedAI_Image from .string_lower import StringLower from .multi_alpha_composite import MultiAlphaComposite from .flux_kontext_inpaint_conditioning import FluxKontextInpaintingConditioning @@ -26,6 +27,7 @@ NODE_CLASS_MAPPINGS = { "FalAPI_recraft_upscale": FalAPI_recraft_upscale, "FalAPI_kling_video": FalAPI_kling_video, "FalAPI_seedance_video": FalAPI_seedance_video, + "WaveSpeedAI_Image": WaveSpeedAI_Image, "StringLower": StringLower, "MultiAlphaComposite": MultiAlphaComposite, "FluxKontextInpaintingConditioning": FluxKontextInpaintingConditioning, @@ -42,6 +44,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "FalAPI_recraft_upscale": "Fal API Recraft Upscale", "FalAPI_kling_video": "Fal API Kling Video", "FalAPI_seedance_video": "Fal API Seedance Video", + "WaveSpeedAI_Image": "WaveSpeedAI Image Generation", "StringLower": "String to Lowercase", "MultiAlphaComposite": "Multi Alpha Composite", "FluxKontextInpaintingConditioning": "No Fun Flux Kontext Inpaint Conditioning", diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc index b54ecaff0a9e062a7cc73b3b96b80a367461a383..94d6cdbb1ac0274433a043f5c26202c2c566421e 100644 GIT binary patch delta 397 zcmeywdxuZsG%qg~0}wd42xh+IWMFs<;=q6al<}E|aiYdreufnO9GP6%C|O2^6oDwY zi8o|8Qe{&F=kQ0#Py8b-DxabN<|w2yq$n<8j8d#r(o~+T!T4E7Fg&p=HMk%(HO0|0 z-ZM8bJ$14KQ#vF6y7m@NaY<2TUV3~^etBw95#QtlR%xI;HLU8| zAkW+q$;nSlfvAs9N-Rmvh)*lcD-r}s6^VifVGtoQ`5bFLmnM+O2*kx}CcCp~Fd9zI zWiyJD)Vd)ocSBn5hNjmINv)6a0^&>;7=%BlFvu9+PmV3JW(=BYTlL&=vsm-f_wR delta 317 zcmcb^_lZ~IG%qg~0}x1Ze#i*oWMFs<;=lkul=1lw!$gg>!TeD&sf?-2sjR8&shp|Y zsl2KDse-A(siLXksgkMEDFSo&qGVG9Q-o54Q$$ikQ^ZolQzTL(Q>0R)Q)E(PQ{=!Z zHT7zHP{F?&sZ$GnkIVe$f&pZq+< zB}JKe>G3)F<*7wQyps>IN&{8jWmVS(Iq#N8PJUuad}eN9dTM-9Vo7pFd|GKOdwV5En0=yn;<*@?|yyQDM0o(t0;Ey>3WqeU#-FXS%>3{6T55 kG`qaKu*MB(qZ^vRHzW;#GIBr}sZUBQ+)Rz^MXEqE0B~zlhyVZp diff --git a/__pycache__/load_image_batch_fun.cpython-312.pyc b/__pycache__/load_image_batch_fun.cpython-312.pyc index ba4cad4c3423ff00614174bbe4586cb5e6e1fb43..f90209b4c46ba4beef999cb5902b3cf5c250ad59 100644 GIT binary patch delta 1674 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 diff --git a/__pycache__/wavespeed_ai_image.cpython-312.pyc b/__pycache__/wavespeed_ai_image.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..18b8912fabbb62c913b5d08f48f9358e71c245df GIT binary patch literal 11761 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 Bd^Z39 literal 0 HcmV?d00001 diff --git a/load_image_batch_fun.py b/load_image_batch_fun.py index 0b514a8..b4ddc94 100644 --- a/load_image_batch_fun.py +++ b/load_image_batch_fun.py @@ -34,11 +34,8 @@ class LoadImageBatchFun: Initialize instance-specific variables to prevent shared state issues. This is the key fix for supporting multiple instances. """ - self.current_images = [] - self.current_filenames = [] self.current_index = 0 self.last_hash = None - self.cached_images = [] self.cached_filenames = [] def load_batch_images(self, mode, index, label, path, pattern, allow_RGBA_output): @@ -58,39 +55,38 @@ class LoadImageBatchFun: raise ValueError(f"Path is not a directory: {path}") # Generate a hash for path/pattern to determine if we need to reload images - # Don't include index/mode in this hash so we can reuse cached images + # Don't include index/mode in this hash so we can reuse cached image paths cache_hash = hashlib.md5( - f"{path}_{pattern}_{allow_RGBA_output}".encode() + f"{path}_{pattern}".encode() ).hexdigest() - # Load images if cache is invalid or parameters changed - if self.last_hash != cache_hash or not self.cached_images: - print(f"[Load Image Batch Fun] Loading images from: {path}") - self.cached_images, self.cached_filenames = self._load_images_from_path( - path, pattern, allow_RGBA_output + # Load image paths if cache is invalid or parameters changed + if self.last_hash != cache_hash or not self.cached_filenames: + print(f"[Load Image Batch Fun] Loading image paths from: {path}") + self.cached_filenames = self._load_images_from_path( + path, pattern ) self.last_hash = cache_hash self.current_index = 0 - if not self.cached_images: + if not self.cached_filenames: raise ValueError(f"No images found in path: {path} with pattern: {pattern}") - total_images = len(self.cached_images) + total_images = len(self.cached_filenames) print(f"[Load Image Batch Fun] Total images found: {total_images}") - # Select image based on mode + # Select image path based on mode + selected_path = None if mode == "single_image": # Use the specified index, wrap around if needed selected_index = index % total_images - selected_image = self.cached_images[selected_index] - selected_filename = self.cached_filenames[selected_index] + selected_path = self.cached_filenames[selected_index] print(f"[Load Image Batch Fun] Single image mode - selected index: {selected_index}") elif mode == "incremental_image": # Use current index and increment selected_index = self.current_index % total_images - selected_image = self.cached_images[selected_index] - selected_filename = self.cached_filenames[selected_index] + selected_path = self.cached_filenames[selected_index] self.current_index += 1 print(f"[Load Image Batch Fun] Incremental mode - selected index: {selected_index}, next: {self.current_index}") @@ -98,19 +94,24 @@ class LoadImageBatchFun: # Random selection import random selected_index = random.randint(0, total_images - 1) - selected_image = self.cached_images[selected_index] - selected_filename = self.cached_filenames[selected_index] + selected_path = self.cached_filenames[selected_index] print(f"[Load Image Batch Fun] Random mode - selected index: {selected_index}") else: raise ValueError(f"Invalid mode: {mode}") + + # Load the selected image + selected_image = self._load_image(selected_path, allow_RGBA_output) + + # Get filename without extension + selected_filename = os.path.splitext(os.path.basename(selected_path))[0] return (selected_image, selected_filename, total_images) - def _load_images_from_path(self, path, pattern, allow_RGBA_output): + def _load_images_from_path(self, path, pattern): """ - Load all images from the specified path matching the pattern. - Returns lists of image tensors and filenames. + Get all image file paths from the specified path matching the pattern. + Returns a sorted list of full file paths. """ import glob @@ -133,23 +134,11 @@ class LoadImageBatchFun: image_files.sort() if not image_files: - return [], [] + return [] print(f"[Load Image Batch Fun] Found {len(image_files)} image files") - images = [] - filenames = [] - - for img_path in image_files: - try: - img = self._load_image(img_path, allow_RGBA_output) - images.append(img) - filenames.append(os.path.basename(img_path)) - except Exception as e: - print(f"[Load Image Batch Fun] Error loading {img_path}: {str(e)}") - continue - - return images, filenames + return image_files def _load_image(self, image_path, allow_RGBA_output): """ diff --git a/wavespeed_ai_image.py b/wavespeed_ai_image.py new file mode 100644 index 0000000..35f9cd4 --- /dev/null +++ b/wavespeed_ai_image.py @@ -0,0 +1,226 @@ +import os +import time +import requests +import tempfile +import json +import random +import numpy as np +import torch +import asyncio +import folder_paths +from io import BytesIO +from PIL import Image +import base64 + +class WaveSpeedAI_Image: + @classmethod + def INPUT_TYPES(cls): + """ + An image generation node for WavespeedAI's qwen-image/edit-plus-lora API. + Takes up to 3 images, a prompt, and other parameters to generate a new image. + """ + return { + "required": { + "api_key": ("STRING", { + "multiline": False, + "default": os.getenv("WAVESPEED_API_KEY", ""), + "display": "WaveSpeedAI API Key" + }), + "prompt": ("STRING", { + "multiline": True, + "default": "A beautiful painting", + "display": "Prompt" + }), + "image1": ("IMAGE", {"display": "Image 1"}), + "width": ("INT", {"default": 886, "min": 64, "max": 4096, "step": 8, "display": "Width"}), + "height": ("INT", {"default": 1182, "min": 64, "max": 4096, "step": 8, "display": "Height"}), + }, + "optional": { + "image2": ("IMAGE", {"display": "Image 2 (optional)"}), + "image3": ("IMAGE", {"display": "Image 3 (optional)"}), + "lora_url": ("STRING", { + "multiline": False, + "default": "", + "display": "LoRA URL (optional)" + }), + "lora_scale": ("FLOAT", { + "default": 0.84, + "min": 0.0, + "max": 2.0, + "step": 0.01, + "display": "LoRA Scale" + }), + "seed": ("INT", { + "default": -1, + "min": -1, + "max": 2147483647, + "display": "Seed (-1 for random)" + }), + "output_format": (["jpeg", "png", "webp"], { + "default": "jpeg", + "display": "Output Format" + }), + } + } + + RETURN_TYPES = ("IMAGE", "STRING", "STRING",) + RETURN_NAMES = ("image", "image_path", "generation_info",) + FUNCTION = "generate_image" + CATEGORY = "WaveSpeedAI" + + def __init__(self): + self.temp_dir = folder_paths.get_temp_directory() + self.metadata_dir = os.path.join(self.temp_dir, "wavespeed_metadata") + os.makedirs(self.metadata_dir, exist_ok=True) + + def get_temp_filename(self, output_format="jpeg"): + timestamp = int(time.time() * 1000) + random_id = random.randint(1000, 9999) + return f"wavespeed_{timestamp}_{random_id}.{output_format}" + + def save_image_and_metadata(self, image_content, generation_info, filename): + filepath = os.path.join(self.temp_dir, filename) + with open(filepath, 'wb') as f: + f.write(image_content) + + metadata_filename = f"{os.path.splitext(filename)[0]}_metadata.json" + metadata_filepath = os.path.join(self.metadata_dir, metadata_filename) + with open(metadata_filepath, "w", encoding="utf-8") as f: + json.dump(generation_info, f, indent=4, ensure_ascii=False) + return filepath, metadata_filepath + + async def generate_image(self, api_key, prompt, image1, width, height, + image2=None, image3=None, lora_url="", lora_scale=0.84, + seed=-1, output_format="jpeg"): + print("[WaveSpeedAI] Starting image generation process...") + + if not api_key: + raise ValueError("A WaveSpeedAI API key is required.") + if not prompt or not prompt.strip(): + raise ValueError("A prompt is required.") + + try: + image_urls = [] + all_images = [image1, image2, image3] + for i, img_tensor in enumerate(all_images): + if img_tensor is not None: + print(f"[WaveSpeedAI] Processing input image {i+1}...") + data_uri = await asyncio.to_thread(self.tensor_to_data_uri, img_tensor) + image_urls.append(data_uri) + + if not image_urls: + raise ValueError("At least one image input is required.") + + payload = { + "enable_base64_output": False, + "enable_sync_mode": False, + "images": image_urls, + "output_format": output_format, + "prompt": prompt, + "size": f"{width}*{height}" + } + + if lora_url.strip(): + payload["loras"] = [{"path": lora_url, "scale": lora_scale}] + + if seed != -1: + payload["seed"] = seed + + print("[WaveSpeedAI] Submitting generation task...") + submit_url = "https://api.wavespeed.ai/api/v3/wavespeed-ai/qwen-image/edit-plus-lora" + headers = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"} + + response = await asyncio.to_thread(requests.post, submit_url, headers=headers, data=json.dumps(payload)) + if response.status_code != 200: + raise ConnectionError(f"Failed to submit task. HTTP {response.status_code}: {response.text}") + + request_id = response.json()["data"]["id"] + print(f"[WaveSpeedAI] Task submitted successfully. Request ID: {request_id}") + + result_url = f"https://api.wavespeed.ai/api/v3/predictions/{request_id}/result" + + output_url = "" + final_result = {} + while True: + print("[WaveSpeedAI] Polling for result...") + response = await asyncio.to_thread(requests.get, result_url, headers={"Authorization": f"Bearer {api_key}"}) + if response.status_code == 200: + result = response.json()["data"] + status = result["status"] + if status == "completed": + print("[WaveSpeedAI] Task completed.") + output_url = result["outputs"][0] + final_result = result + break + elif status == "failed": + raise RuntimeError(f"Task failed: {result.get('error', 'Unknown error')}") + else: + print(f"[WaveSpeedAI] Status: {status}. Waiting...") + else: + raise ConnectionError(f"Failed to poll for results. HTTP {response.status_code}: {response.text}") + await asyncio.sleep(2) + + print(f"[WaveSpeedAI] Downloading image from: {output_url}") + resp = await asyncio.to_thread(requests.get, output_url) + if resp.status_code != 200: + raise ConnectionError(f"Failed to download image. HTTP {resp.status_code}") + + generation_info = { + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), + "model": "qwen-image/edit-plus-lora", + "parameters": {k: v for k, v in payload.items() if k != 'images'}, + "input_images_count": len(image_urls), + "wavespeed_result": final_result + } + + temp_filename = self.get_temp_filename(output_format) + image_path, metadata_path = await asyncio.to_thread( + self.save_image_and_metadata, resp.content, generation_info, temp_filename + ) + print(f"[WaveSpeedAI] Saved temp image -> {image_path}") + print(f"[WaveSpeedAI] Saved metadata -> {metadata_path}") + + pil_image = Image.open(BytesIO(resp.content)) + output_tensor = self.pil_to_tensor(pil_image) + + return (output_tensor, image_path, json.dumps(generation_info, indent=2)) + + except Exception as e: + error_info = { + "error": f"WaveSpeedAI generation failed: {str(e)}", + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), + } + print(f"[WaveSpeedAI] Error: {str(e)}") + return (None, "", json.dumps(error_info, indent=2)) + + def tensor_to_data_uri(self, tensor, image_format="PNG"): + pil_img = self.tensor_to_pil(tensor) + buffered = BytesIO() + pil_img.save(buffered, format=image_format) + img_str = base64.b64encode(buffered.getvalue()).decode("utf-8") + return f"data:image/{image_format.lower()};base64,{img_str}" + + def tensor_to_pil(self, tensor): + if len(tensor.shape) == 4: + tensor = tensor[0] + arr = (tensor.cpu().numpy() * 255).clip(0, 255).astype("uint8") + return Image.fromarray(arr) + + def pil_to_tensor(self, pil_img): + img_array = np.array(pil_img).astype(np.float32) / 255.0 + return torch.from_numpy(img_array)[None,] + + @classmethod + def IS_CHANGED(cls, **kwargs): + return float("NaN") + +# Register with ComfyUI +NODE_CLASS_MAPPINGS = { + "WaveSpeedAI_Image": WaveSpeedAI_Image +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "WaveSpeedAI_Image": "WaveSpeedAI Image Generation" +} + +__all__ = ["WaveSpeedAI_Image"]