From d38b944da519f8516258c8f828662ec12197bb68 Mon Sep 17 00:00:00 2001 From: Alexander Tujicov Date: Wed, 9 Sep 2026 18:39:51 +0300 Subject: [PATCH] Wire GPT resolution to input aspect ratio --- README.md | 7 ++-- nano_seed.py | 90 ++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 88 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7f95ef3..ccc5916 100644 --- a/README.md +++ b/README.md @@ -70,13 +70,14 @@ You must have a **Fal.ai API Key** to use this node. * **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). +* **resolution**: Sets the maximum output edge for GPT Image models while preserving the first input image's aspect ratio: `0.5K` = 512px, `1K` = 1024px, `2K` = 2048px, and `4K` = 3840px. * **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_image_size**: Optional GPT aspect-ratio override. `auto` follows the first input image; presets or non-zero `width` and `height` override the aspect ratio, while `resolution` still caps the longest edge. * **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. @@ -88,7 +89,9 @@ Different models have different requirements implemented in the node: * 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. + * `resolution` automatically calculates dimensions from the first input image: 1K, 2K, or 4K sets a 1024px, 2048px, or 3840px maximum edge. + * Calculated dimensions are multiples of 16, and input aspect ratios beyond the API limit are clamped to 3:1. + * `gpt_image_size` or non-zero `width` and `height` can override the aspect ratio, but not the selected resolution cap. * Supports the shared GPT `mask` socket. 2. **Flux 2 Edit (`flux_2_edit`)**: diff --git a/nano_seed.py b/nano_seed.py index 1b18df6..e37d487 100644 --- a/nano_seed.py +++ b/nano_seed.py @@ -1,4 +1,5 @@ import asyncio +import math import os import re import time @@ -21,6 +22,15 @@ GPT_IMAGE_25_EDIT_URLS = { "gpt_image_2_5_sunburst_edit": "https://fal.run/openai/gpt-image-2.5/sunburst/edit", } +GPT_IMAGE_EDIT_MODELS = {"gpt_image_2_edit", *GPT_IMAGE_25_EDIT_URLS} + +GPT_RESOLUTION_MAX_EDGE = { + "0.5K": 512, + "1K": 1024, + "2K": 2048, + "4K": 3840, +} + GPT_IMAGE_SIZE_OPTIONS = [ "auto", "square_hd", @@ -35,6 +45,12 @@ GPT_IMAGE_SIZE_OPTIONS = [ ] GPT_IMAGE_SIZE_DIMENSIONS = { + "square_hd": {"width": 1024, "height": 1024}, + "square": {"width": 512, "height": 512}, + "portrait_4_3": {"width": 768, "height": 1024}, + "portrait_16_9": {"width": 576, "height": 1024}, + "landscape_4_3": {"width": 1024, "height": 768}, + "landscape_16_9": {"width": 1024, "height": 576}, "1920x1080": {"width": 1920, "height": 1080}, "2560x1440": {"width": 2560, "height": 1440}, "3840x2160": {"width": 3840, "height": 2160}, @@ -176,10 +192,54 @@ def _prepare_image_data_uris(images, resize_to=None): return data_uris -def _resolve_gpt_image_size(width, height, image_size): +def _gpt_source_dimensions(image_tensor, 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) + return width, height + + preset = GPT_IMAGE_SIZE_DIMENSIONS.get(image_size) + if preset is not None: + return preset["width"], preset["height"] + + shape = image_tensor.shape + if len(shape) == 4: + return int(shape[2]), int(shape[1]) + if len(shape) == 3: + return int(shape[1]), int(shape[0]) + raise ValueError("GPT image input must use ComfyUI IMAGE tensor dimensions.") + + +def _resolve_gpt_image_size(image_tensor, resolution, width, height, image_size): + max_edge = GPT_RESOLUTION_MAX_EDGE.get(resolution) + if max_edge is None: + raise ValueError(f"Unsupported GPT resolution: {resolution!r}") + + source_width, source_height = _gpt_source_dimensions( + image_tensor, + width, + height, + image_size, + ) + if source_width <= 0 or source_height <= 0: + raise ValueError("GPT image dimensions must be greater than zero.") + + if source_width >= source_height: + aspect_ratio = min(source_width / source_height, 3.0) + output_width = max_edge + output_height = _round_to_multiple(output_width / aspect_ratio, 16) + output_height = max( + output_height, + math.ceil((output_width / 3.0) / 16) * 16, + ) + else: + aspect_ratio = min(source_height / source_width, 3.0) + output_height = max_edge + output_width = _round_to_multiple(output_height / aspect_ratio, 16) + output_width = max( + output_width, + math.ceil((output_height / 3.0) / 16) * 16, + ) + + return {"width": output_width, "height": output_height} def _safe_error_detail(detail): @@ -467,11 +527,27 @@ class NanoSeedEdit: images = [img for img in image_inputs if img is not None] if not images: raise ValueError("At least one image input must be connected.") - + + resolved_gpt_image_size = None + if model in GPT_IMAGE_EDIT_MODELS: + resolved_gpt_image_size = _resolve_gpt_image_size( + images[0], + resolution, + width, + height, + gpt_image_size, + ) + custom_size = (width > 0 and height > 0) resize_to = None if custom_size and model not in ["nano_banana", "nano_banana_pro", "nano_banana_2"]: - resize_to = (width, height) + if resolved_gpt_image_size is not None: + resize_to = ( + resolved_gpt_image_size["width"], + resolved_gpt_image_size["height"], + ) + else: + resize_to = (width, height) img_data_uris = await asyncio.to_thread( _prepare_image_data_uris, images, @@ -530,7 +606,7 @@ class NanoSeedEdit: "image_urls": img_data_uris, "quality": quality, "background": gpt_background, - "image_size": _resolve_gpt_image_size(width, height, gpt_image_size), + "image_size": resolved_gpt_image_size, "num_images": min(num_images, 4), "output_format": gpt_output_format, "sync_mode": False, @@ -546,7 +622,7 @@ class NanoSeedEdit: "image_urls": img_data_uris, "quality": quality, "background": gpt_background, - "image_size": _resolve_gpt_image_size(width, height, gpt_image_size), + "image_size": resolved_gpt_image_size, "num_images": num_images, "output_format": gpt_output_format, "sync_mode": False,