From 0c5428a57e3a5fdb8cd32ef8f66344d90d9c67b7 Mon Sep 17 00:00:00 2001 From: pixelworld AI Date: Sun, 14 Sep 2025 11:16:25 -0500 Subject: [PATCH] feat: add Seedream 4 API nodes and resolution helper utility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Seedream 4 text-to-image generation node with advanced parameters - Add Seedream 4 image-to-image transformation node - Add Seedream 4 multi-image blending node for up to 4 input images - Add Seedream 4 sequential batch generation node with auto mode - Add resolution helper utility node with preset resolution and aspect ratio calculations - Update timeout from 60s to 120s for better reliability - Add size resolution helper method to SeedImageApiHandler 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- nodes/byteplus_image_node.py | 351 ++++++++++++++++++++++++++++++++++- 1 file changed, 346 insertions(+), 5 deletions(-) diff --git a/nodes/byteplus_image_node.py b/nodes/byteplus_image_node.py index a0cae17..acb676b 100644 --- a/nodes/byteplus_image_node.py +++ b/nodes/byteplus_image_node.py @@ -44,6 +44,11 @@ class SeedResultProcessor: class SeedImageApiHandler: """Utility functions for Seed image API interactions.""" + @staticmethod + def resolve_size(width, height): + """Resolve size parameter from width and height dimensions.""" + return f"{width}x{height}" + @staticmethod def generate_image(model, arguments): """Generate image using Seed API.""" @@ -69,7 +74,7 @@ class SeedImageApiHandler: f"{config.get_base_url()}/images/generations", headers=headers, json=payload, - timeout=60 + timeout=120 ) if response.status_code == 200: @@ -92,7 +97,7 @@ class SeedImageApiHandler: class SeedreamTextToImageNode: - """Seedream Text-to-Image Generation Node""" + """Seedream 3 Text-to-Image Generation Node""" @classmethod def INPUT_TYPES(cls): @@ -139,7 +144,7 @@ class SeedreamTextToImageNode: class SeedEditImageToImageNode: - """SeedEdit Image-to-Image Generation Node""" + """SeedEdit 3 Image-to-Image Generation Node""" @classmethod def INPUT_TYPES(cls): @@ -196,14 +201,350 @@ class SeedEditImageToImageNode: ) +class Seedream4TextToImageNode: + """Seedream 4 Text-to-Image Generation Node""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "prompt": ("STRING", {"default": "", "multiline": True}), + "width": ("INT", {"default": 2048, "min": 64, "max": 4096, "step": 1}), + "height": ("INT", {"default": 2048, "min": 64, "max": 4096, "step": 1}), + }, + "optional": { + "seed": ("INT", {"default": -1, "min": -1, "max": 2147483647}), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "generate_image" + CATEGORY = "Seed/ImageGeneration" + + def generate_image(self, prompt, width, height, seed=-1): + try: + size = SeedImageApiHandler.resolve_size(width, height) + arguments = { + "prompt": prompt, + "size": size, + "response_format": "url", + "watermark": False, + } + + # Only add seed if it's not -1 (random) + if seed != -1: + arguments["seed"] = seed + + result = SeedImageApiHandler.generate_image( + "seedream-4-0-250828", + arguments + ) + + return SeedResultProcessor.process_image_result(result) + + except Exception as e: + return SeedImageApiHandler.handle_image_generation_error( + "Seedream 4 Text-to-Image", str(e) + ) + + +class Seedream4ImageToImageNode: + """Seedream 4 Image-to-Image Generation Node""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "prompt": ("STRING", {"default": "", "multiline": True}), + "image": ("IMAGE",), + "width": ("INT", {"default": 2048, "min": 64, "max": 4096, "step": 1}), + "height": ("INT", {"default": 2048, "min": 64, "max": 4096, "step": 1}), + }, + "optional": { + "seed": ("INT", {"default": -1, "min": -1, "max": 2147483647}), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "generate_image" + CATEGORY = "Seed/ImageGeneration" + + def generate_image(self, prompt, image, width, height, seed=-1): + try: + # Convert image to base64 + image_base64 = BytePlusImageUtils.image_to_base64(image) + if not image_base64: + return SeedImageApiHandler.handle_image_generation_error( + "Seedream 4 Image-to-Image", "Failed to convert image to base64" + ) + + size = SeedImageApiHandler.resolve_size(width, height) + arguments = { + "prompt": prompt, + "image": image_base64, + "size": size, + "response_format": "url", + "watermark": False, + "sequential_image_generation": "disabled", # Single image output + } + + # Only add seed if it's not -1 (random) + if seed != -1: + arguments["seed"] = seed + + result = SeedImageApiHandler.generate_image( + "seedream-4-0-250828", + arguments + ) + + return SeedResultProcessor.process_image_result(result) + + except Exception as e: + return SeedImageApiHandler.handle_image_generation_error( + "Seedream 4 Image-to-Image", str(e) + ) + + +class Seedream4MultiImageBlendingNode: + """Seedream 4 Multi-Image Blending Node""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "prompt": ("STRING", {"default": "", "multiline": True}), + "image1": ("IMAGE",), + "image2": ("IMAGE",), + "width": ("INT", {"default": 2048, "min": 64, "max": 4096, "step": 1}), + "height": ("INT", {"default": 2048, "min": 64, "max": 4096, "step": 1}), + }, + "optional": { + "image3": ("IMAGE",), + "image4": ("IMAGE",), + "seed": ("INT", {"default": -1, "min": -1, "max": 2147483647}), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "generate_image" + CATEGORY = "Seed/ImageGeneration" + + def generate_image(self, prompt, image1, image2, width, height, image3=None, image4=None, seed=-1): + try: + # Convert images to base64 + images_base64 = [] + for img in [image1, image2, image3, image4]: + if img is not None: + img_b64 = BytePlusImageUtils.image_to_base64(img) + if img_b64: + images_base64.append(img_b64) + + if len(images_base64) < 2: + return SeedImageApiHandler.handle_image_generation_error( + "Seedream 4 Multi-Image Blending", "At least 2 images are required" + ) + + size = SeedImageApiHandler.resolve_size(width, height) + arguments = { + "prompt": prompt, + "image": images_base64, + "size": size, + "response_format": "url", + "watermark": False, + "sequential_image_generation": "disabled", # Multi-image blending to single output + } + + # Only add seed if it's not -1 (random) + if seed != -1: + arguments["seed"] = seed + + result = SeedImageApiHandler.generate_image( + "seedream-4-0-250828", + arguments + ) + + return SeedResultProcessor.process_image_result(result) + + except Exception as e: + return SeedImageApiHandler.handle_image_generation_error( + "Seedream 4 Multi-Image Blending", str(e) + ) + + +class Seedream4BatchGenerationNode: + """Seedream 4 Sequential Batch Image Generation Node""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "prompt": ("STRING", {"default": "", "multiline": True}), + "max_images": ("INT", {"default": 3, "min": 1, "max": 10, "step": 1}), + "width": ("INT", {"default": 2048, "min": 64, "max": 4096, "step": 1}), + "height": ("INT", {"default": 2048, "min": 64, "max": 4096, "step": 1}), + }, + "optional": { + "image1": ("IMAGE",), + "image2": ("IMAGE",), + "seed": ("INT", {"default": -1, "min": -1, "max": 2147483647}), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "generate_images" + CATEGORY = "Seed/ImageGeneration" + + def generate_images(self, prompt, max_images, width, height, image1=None, image2=None, seed=-1): + try: + size = SeedImageApiHandler.resolve_size(width, height) + arguments = { + "prompt": prompt, + "size": size, + "response_format": "url", + "watermark": False, + "sequential_image_generation": "auto", + "sequential_image_generation_options": { + "max_images": max_images + }, + "stream": False, + } + + # Add images if provided + images_base64 = [] + for img in [image1, image2]: + if img is not None: + img_b64 = BytePlusImageUtils.image_to_base64(img) + if img_b64: + images_base64.append(img_b64) + + if images_base64: + arguments["image"] = images_base64 + + # Only add seed if it's not -1 (random) + if seed != -1: + arguments["seed"] = seed + + result = SeedImageApiHandler.generate_image( + "seedream-4-0-250828", + arguments + ) + + return SeedResultProcessor.process_image_result(result) + + except Exception as e: + return SeedImageApiHandler.handle_image_generation_error( + "Seedream 4 Batch Generation", str(e) + ) + + +class ResolutionHelperNode: + """Resolution Helper - Calculate width/height from resolution presets and aspect ratios""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "resolution": (["1K", "2K", "3K", "4K"], {"default": "2K"}), + "aspect_ratio": (["1:1 (Square)", "4:3 (Standard)", "3:4 (Portrait)", "16:9 (Widescreen)", + "9:16 (Vertical)", "3:2 (Photo)", "2:3 (Portrait Photo)", "21:9 (Ultrawide)"], + {"default": "1:1 (Square)"}), + }, + } + + RETURN_TYPES = ("INT", "INT") + RETURN_NAMES = ("width", "height") + FUNCTION = "calculate_resolution" + CATEGORY = "Seed/Utilities" + + def calculate_resolution(self, resolution, aspect_ratio): + # Base resolutions (approximate pixel counts) + base_pixels = { + "1K": 1024 * 1024, # ~1M pixels + "2K": 2048 * 2048, # ~4M pixels + "3K": 3072 * 3072, # ~9M pixels + "4K": 4096 * 4096, # ~16M pixels + } + + # Aspect ratio calculations + aspect_ratios = { + "1:1 (Square)": (1, 1), + "4:3 (Standard)": (4, 3), + "3:4 (Portrait)": (3, 4), + "16:9 (Widescreen)": (16, 9), + "9:16 (Vertical)": (9, 16), + "3:2 (Photo)": (3, 2), + "2:3 (Portrait Photo)": (2, 3), + "21:9 (Ultrawide)": (21, 9), + } + + # Get target pixel count and aspect ratio + target_pixels = base_pixels[resolution] + width_ratio, height_ratio = aspect_ratios[aspect_ratio] + + # API maximum: 16,777,216 pixels (4096²) + max_pixels = 16777216 + + # If target exceeds API limit, use the maximum instead + if target_pixels > max_pixels: + target_pixels = max_pixels + + # Calculate dimensions + # For aspect ratio w:h, if total pixels = W*H, then: + # W = width_ratio * k, H = height_ratio * k + # W * H = width_ratio * height_ratio * k^2 = target_pixels + # k = sqrt(target_pixels / (width_ratio * height_ratio)) + + import math + k = math.sqrt(target_pixels / (width_ratio * height_ratio)) + + width = int(width_ratio * k) + height = int(height_ratio * k) + + # Ensure dimensions are above minimum and total pixels don't exceed API limit + width = max(64, width) + height = max(64, height) + + # Final safety check: if calculated pixels exceed limit, scale down proportionally + if width * height > max_pixels: + scale_factor = math.sqrt(max_pixels / (width * height)) + width = int(width * scale_factor) + height = int(height * scale_factor) + + # Round to even numbers for better codec compatibility, but respect pixel limit + # Check if rounding up would exceed the limit + width_even = width if width % 2 == 0 else width + 1 + height_even = height if height % 2 == 0 else height + 1 + + if width_even * height_even > max_pixels: + # Rounding up would exceed limit, so round down instead + width = width if width % 2 == 0 else width - 1 + height = height if height % 2 == 0 else height - 1 + else: + # Safe to round up to even numbers + width = width_even + height = height_even + + return (width, height) + + # Node class mappings NODE_CLASS_MAPPINGS = { "SeedreamTextToImage": SeedreamTextToImageNode, "SeedEditImageToImage": SeedEditImageToImageNode, + "Seedream4TextToImage": Seedream4TextToImageNode, + "Seedream4ImageToImage": Seedream4ImageToImageNode, + "Seedream4MultiImageBlending": Seedream4MultiImageBlendingNode, + "Seedream4BatchGeneration": Seedream4BatchGenerationNode, + "ResolutionHelper": ResolutionHelperNode, } # Node display name mappings NODE_DISPLAY_NAME_MAPPINGS = { - "SeedreamTextToImage": "Seedream Text-to-Image", - "SeedEditImageToImage": "SeedEdit Image-to-Image", + "SeedreamTextToImage": "Seedream 3 Text-to-Image", + "SeedEditImageToImage": "SeedEdit 3 Image-to-Image", + "Seedream4TextToImage": "Seedream 4 Text-to-Image", + "Seedream4ImageToImage": "Seedream 4 Image-to-Image", + "Seedream4MultiImageBlending": "Seedream 4 Multi-Image Blending", + "Seedream4BatchGeneration": "Seedream 4 Batch Generation", + "ResolutionHelper": "Resolution Helper", }