diff --git a/README.md b/README.md index 95ab26c..7f95ef3 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ A Custom Node that integrates [Fal.ai's](https://fal.ai) powerful image editing | **Nano Banana Pro** | `nano_banana_pro` | Enhanced quality version of Nano Banana. | | **Nano Banana 2** | `nano_banana_2` | New generation model with web search and thinking options. | | **GPT Image 2 Edit** | `gpt_image_2_edit` | OpenAI image editing with quality and mask controls. | +| **GPT Image 2.5 Flare Edit** | `gpt_image_2_5_flare_edit` | Precise GPT Image 2.5 editing with high-resolution, background, quality, format, compression, and mask controls. | +| **GPT Image 2.5 Sunburst Edit** | `gpt_image_2_5_sunburst_edit` | Precision-focused GPT Image 2.5 editing with the same extended controls. | | **Grok Imagine Edit** | `grok_imagine_edit` | xAI image edit model, up to 3 input images. | | **Seedream 4.5** | `seedream_4.5` | ByteDance's model. Optimized for high-res editing. | | **Seedream 5 Pro Edit** | `seedream_5` | ByteDance Seedream 5.0 Pro region-precise editing. | @@ -58,55 +60,67 @@ You must have a **Fal.ai API Key** to use this node. * **model**: Select the specific AI model to use. * **fal_key**: Optional UI fallback for your API key when `FAL_KEY` is not set. * **image1 - image10**: Connect the images you want to edit. -* **mask**: Optional GPT Image 2 edit mask. Other models ignore it. +* **mask**: Optional GPT Image 2 or GPT Image 2.5 edit mask. Other models ignore it. ### Settings * **width / height**: Output resolution. * *Note:* `nano_banana` models ignore this and use `aspect_ratio`. * *Note:* `seedream_4.5` requires high resolutions (see constraints below). -* **num_images**: How many variations to generate (Max 6). +* **num_images**: How many variations to generate. GPT Image 2.5 supports up to 10; existing model-specific caps remain enforced. * **num_inference_steps**: Number of inference steps for supported models. * **seed**: Seed for reproducibility. * **aspect_ratio**: Set target aspect ratio (e.g., 16:9, 1:1). +* **quality**: GPT quality. GPT Image 2 supports `auto` through `high`; GPT Image 2.5 also supports `xhigh` and `max`. * **enable_web_search**: Enables web search context for supported models. * **thinking_level**: `off`, `minimal`, or `high` for supported models. * **concurrent_runs**: Number of independent fal.ai queue jobs to submit in parallel from this node. This is separate from workflow-level concurrency, which automatically overlaps independent NanoSeed nodes. Each run requests its own `num_images`; outputs are returned in run order. Models with an explicit seed increment it for each additional run. * **queue_timeout**: Maximum wait per queue job in seconds. Timed-out jobs receive a best-effort cancellation request. +* **gpt_background**: `auto`, `transparent`, or `opaque` for GPT Image models. +* **gpt_image_size**: GPT output-size preset, including 1080p, 1440p, and 3840x2160. Non-zero `width` and `height` override this preset. +* **gpt_output_format**: `png`, `jpeg`, or `webp` for GPT Image models. +* **gpt_output_compression**: Optional GPT Image 2.5 JPEG/WebP compression from 0-100. Leave at `-1` to use the API default. ### ⚠️ Model Specific Constraints Different models have different requirements implemented in the node: -1. **Flux 2 Edit (`flux_2_edit`)**: +1. **GPT Image 2.5 Flare / Sunburst**: + * Supports all 10 NanoSeed reference-image sockets and up to **10 output images**. + * Supports `auto`, `low`, `medium`, `high`, `xhigh`, and `max` quality. Higher levels increase cost and latency. + * Supports automatic, transparent, or opaque backgrounds and PNG, JPEG, or WebP output. + * Includes 1080p, 1440p, and **3840x2160** presets. Custom non-zero `width` and `height` take priority. + * Supports the shared GPT `mask` socket. + +2. **Flux 2 Edit (`flux_2_edit`)**: * **Single Image Only:** Do not connect `image2` through `image5`. * **Resolution:** Width and Height must be between **512px and 2048px**. -2. **Flux 2 Klein 9B Edit (`flux_2_klein_9b_edit`)**: +3. **Flux 2 Klein 9B Edit (`flux_2_klein_9b_edit`)**: * Supports up to **4 input images**. * `num_inference_steps` is limited to **4-8**. -3. **Seedream 4.5 (`seedream_4.5`)**: +4. **Seedream 4.5 (`seedream_4.5`)**: * **High Res Only:** Width and Height must be between **1920px and 4096px**. * **Pixel Count:** Total pixels (W x H) must be between ~3.6MP and 16.7MP. * **Batch Limit:** The sum of input images and output `num_images` cannot exceed 15. -4. **Seedream 5 Pro Edit (`seedream_5`)**: +5. **Seedream 5 Pro Edit (`seedream_5`)**: * Supports up to **10 input images**. * If `aspect_ratio` is not `auto`, the selected aspect ratio is prioritized and converted to Seedream's `image_size`. * If `aspect_ratio` is `auto`, the selected `resolution` is mapped to Seedream's automatic size presets. * Pro supports `auto_1K` and `auto_2K`; `4K` falls back to Pro's highest automatic preset. -5. **Seedream 5 Lite Edit (`seedream_5_lite`)**: +6. **Seedream 5 Lite Edit (`seedream_5_lite`)**: * Supports up to **10 input images**. * Uses the same aspect-ratio-first behavior as Seedream 5 Pro. * Supports Lite automatic presets up to `auto_4K`. -6. **Nano Banana / Pro / 2**: +7. **Nano Banana / Pro / 2**: * These models rely on `aspect_ratio` and `resolution` settings rather than custom width/height integers. * `nano_banana_2` supports `0.5K`, `1K`, `2K`, and `4K` resolution presets. * `nano_banana_2` also supports extended aspect ratios: `4:1`, `1:4`, `8:1`, and `1:8`. -7. **Grok Imagine Edit (`grok_imagine_edit`)**: +8. **Grok Imagine Edit (`grok_imagine_edit`)**: * Supports up to **3 input images**. * Uses up to **4 output images**. diff --git a/nano_seed.py b/nano_seed.py index 4709803..1b18df6 100644 --- a/nano_seed.py +++ b/nano_seed.py @@ -16,6 +16,30 @@ FAL_HTTP_TIMEOUT_SECONDS = 60 FAL_QUEUE_POLL_INTERVAL_SECONDS = 1.0 FAL_MAX_CONCURRENT_RUNS = 8 +GPT_IMAGE_25_EDIT_URLS = { + "gpt_image_2_5_flare_edit": "https://fal.run/openai/gpt-image-2.5/flare/edit", + "gpt_image_2_5_sunburst_edit": "https://fal.run/openai/gpt-image-2.5/sunburst/edit", +} + +GPT_IMAGE_SIZE_OPTIONS = [ + "auto", + "square_hd", + "square", + "portrait_4_3", + "portrait_16_9", + "landscape_4_3", + "landscape_16_9", + "1920x1080", + "2560x1440", + "3840x2160", +] + +GPT_IMAGE_SIZE_DIMENSIONS = { + "1920x1080": {"width": 1920, "height": 1080}, + "2560x1440": {"width": 2560, "height": 1440}, + "3840x2160": {"width": 3840, "height": 2160}, +} + SEEDREAM_5_AUTO_IMAGE_SIZE_BY_RESOLUTION = { "seedream_5": { "0.5K": "auto_1K", @@ -152,6 +176,12 @@ def _prepare_image_data_uris(images, resize_to=None): return data_uris +def _resolve_gpt_image_size(width, height, image_size): + if width > 0 and height > 0: + return {"width": width, "height": height} + return GPT_IMAGE_SIZE_DIMENSIONS.get(image_size, image_size) + + def _safe_error_detail(detail): detail = re.sub( r"data:[^,\s\"']+,[A-Za-z0-9+/=]+", @@ -370,7 +400,7 @@ class NanoSeedEdit: return { "required": { "prompt": ("STRING", {"default": "Edit the image according to this prompt.", "multiline": True}), - "model": (["nano_banana", "nano_banana_pro", "nano_banana_2", "gpt_image_2_edit", "grok_imagine_edit", "seedream_4.5", "seedream_5", "seedream_5_lite", "qwen_edit_plus", "flux_2_edit", "flux_2_pro", "flux_2_flex", "flux_2_klein_9b_edit"],), + "model": (["nano_banana", "nano_banana_pro", "nano_banana_2", "gpt_image_2_edit", "gpt_image_2_5_flare_edit", "gpt_image_2_5_sunburst_edit", "grok_imagine_edit", "seedream_4.5", "seedream_5", "seedream_5_lite", "qwen_edit_plus", "flux_2_edit", "flux_2_pro", "flux_2_flex", "flux_2_klein_9b_edit"],), "fal_key": ("STRING", {"default": "", "multiline": False}), }, "optional": { @@ -387,16 +417,20 @@ class NanoSeedEdit: "mask": ("IMAGE",), "width": ("INT", {"default": 0, "min": 0, "max": 4096, "display": "number"}), "height": ("INT", {"default": 0, "min": 0, "max": 4096, "display": "number"}), - "num_images": ("INT", {"default": 1, "min": 1, "max": 6}), + "num_images": ("INT", {"default": 1, "min": 1, "max": 10}), "num_inference_steps": ("INT", {"default": 28, "min": 1, "max": 100}), "seed": ("INT", {"default": 0, "min": 0, "max": 2**32 - 1}), "aspect_ratio": (["auto", "21:9", "16:9", "3:2", "4:3", "5:4", "1:1", "4:5", "3:4", "2:3", "9:16", "4:1", "1:4", "8:1", "1:8"], {"default": "auto"}), "resolution": (["0.5K", "1K", "2K", "4K"], {"default": "1K"}), - "quality": (["low", "medium", "high"], {"default": "high"}), + "quality": (["auto", "low", "medium", "high", "xhigh", "max"], {"default": "high"}), "enable_web_search": ("BOOLEAN", {"default": False}), "thinking_level": (["off", "minimal", "high"], {"default": "off"}), "concurrent_runs": ("INT", {"default": 1, "min": 1, "max": 8, "step": 1}), "queue_timeout": ("INT", {"default": 900, "min": 60, "max": 3600, "step": 30}), + "gpt_background": (["auto", "transparent", "opaque"], {"default": "auto"}), + "gpt_image_size": (GPT_IMAGE_SIZE_OPTIONS, {"default": "auto"}), + "gpt_output_format": (["png", "jpeg", "webp"], {"default": "png"}), + "gpt_output_compression": ("INT", {"default": -1, "min": -1, "max": 100, "step": 1}), } } @@ -410,7 +444,8 @@ class NanoSeedEdit: image6=None, image7=None, image8=None, image9=None, image10=None, mask=None, width=0, height=0, num_images=1, num_inference_steps=28, seed=0, aspect_ratio="auto", resolution="1K", quality="high", enable_web_search=False, thinking_level="off", acceleration="none", - concurrent_runs=1, queue_timeout=900): # Hardcoded to none, kept for compatibility + concurrent_runs=1, queue_timeout=900, gpt_background="auto", gpt_image_size="auto", + gpt_output_format="png", gpt_output_compression=-1): # Hardcoded to none, kept for compatibility env_fal_key = (os.environ.get("FAL_KEY") or "").strip() ui_fal_key = (fal_key or "").strip() if ui_fal_key == "your_fal_key_here": @@ -444,7 +479,7 @@ class NanoSeedEdit: ) # Enforce limits (Updated: Removed Flux 2 single image limit) - if model in ["seedream_4.5", "seedream_5", "seedream_5_pro", "seedream_5_lite"] and len(img_data_uris) + num_images > 15: + if model in ["seedream_4.5", "seedream_5", "seedream_5_pro", "seedream_5_lite"] and len(img_data_uris) + min(num_images, 6) > 15: raise ValueError("Seedream: Total inputs + outputs must <=15.") # Model-specific payloads @@ -487,17 +522,37 @@ class NanoSeedEdit: if thinking_level != "off": payload["thinking_level"] = thinking_level elif model == "gpt_image_2_edit": + if quality in {"xhigh", "max"}: + raise ValueError("GPT Image 2 quality supports auto, low, medium, or high.") url = "https://fal.run/openai/gpt-image-2/edit" payload = { "prompt": prompt, "image_urls": img_data_uris, "quality": quality, - "num_images": num_images, - "output_format": "png", + "background": gpt_background, + "image_size": _resolve_gpt_image_size(width, height, gpt_image_size), + "num_images": min(num_images, 4), + "output_format": gpt_output_format, "sync_mode": False, } - if custom_size: - payload["image_size"] = {"width": width, "height": height} + if mask is not None: + mask_data_uri = await asyncio.to_thread(tensor2data_uri, mask) + if mask_data_uri is not None: + payload["mask_url"] = mask_data_uri + elif model in GPT_IMAGE_25_EDIT_URLS: + url = GPT_IMAGE_25_EDIT_URLS[model] + payload = { + "prompt": prompt, + "image_urls": img_data_uris, + "quality": quality, + "background": gpt_background, + "image_size": _resolve_gpt_image_size(width, height, gpt_image_size), + "num_images": num_images, + "output_format": gpt_output_format, + "sync_mode": False, + } + if gpt_output_format in {"jpeg", "webp"} and gpt_output_compression >= 0: + payload["output_compression"] = gpt_output_compression if mask is not None: mask_data_uri = await asyncio.to_thread(tensor2data_uri, mask) if mask_data_uri is not None: