Files
Alexander Garzon e8fdd92f21 fix: carry alpha out as a MASK output and lower the custom size floor
Manual testing in ComfyUI surfaced two defects in the gpt-image-2.5 work.

background=transparent was silently useless: the API returned a correct RGBA
PNG, but pil_to_tensor ran convert("RGB") on every response and composited the
alpha onto black. Both nodes now output (IMAGE, MASK), with the alpha carried on
the mask as 1 - alpha to match the LoadImage convention, so Join Image with
Alpha consumes it directly. The three response decode helpers collapsed into
response_item_to_pil, since the old path already decoded each item to a PIL
image and then discarded the alpha -- routing through a single decode was
smaller than adding a second one.

CUSTOM_DIM_MIN was 1024 while validate_custom_dimensions() accepts a short edge
down to 480 (1440x480 at the 3:1 aspect limit). The widget was a second, stricter
gate that disagreed with the validator, so legal sizes like 1536x864 snapped up
to 1024 and were unreachable from the UI. The floor is now derived from what the
validator accepts.

The existing tests missed both: they assert on request kwargs and never checked
that alpha survives the response, and they called run_generate() directly,
bypassing the widget bounds entirely.

Verified against the live API (transparent output recomposes to a fully
transparent corner pixel) and by manual testing in ComfyUI.
2026-09-14 11:31:32 -04:00

110 lines
5.3 KiB
Python

"""ComfyUI node: text-to-image via OpenAI images.generate.
Declared with the comfy_api IO schema. `model` is a DynamicCombo (switching the
model swaps the size/background widgets). Within gpt-image-2, `size` is itself a
nested DynamicCombo so custom_width/height appear only under size='custom'. All
behavior lives in the import-clean build.run_generate(); this file is a thin
schema + marshaling wrapper (so it requires ComfyUI to import).
"""
from __future__ import annotations
from comfy_api.latest import IO
from . import build
from . import params as params_mod
def _custom_dim(name: str, axis: str):
return IO.Int.Input(name, default=1024, min=params_mod.CUSTOM_DIM_MIN,
max=params_mod.CUSTOM_DIM_MAX, step=params_mod.CUSTOM_DIM_STEP,
tooltip=f"Used when size = custom. Multiple of 16; with {axis}: "
"aspect <= 3:1 and total pixels 655,360-8,294,400.")
def _size_combo_gpt2():
"""gpt-image-2 size as a nested DynamicCombo: width/height appear only for 'custom'."""
options = [IO.DynamicCombo.Option(s, []) for s in params_mod.SIZES_GPT2 if s != "custom"]
options.append(IO.DynamicCombo.Option(
"custom", [_custom_dim("custom_width", "height"), _custom_dim("custom_height", "width")]))
return IO.DynamicCombo.Input(
"size", tooltip="Output size. Choose 'custom' to set an exact width and height.",
options=options)
def _gpt_image_2_inputs():
"""Per-model widgets for gpt-image-2: hi-res/custom sizes, opaque/auto background."""
return [
_size_combo_gpt2(),
IO.Combo.Input("background", options=params_mod.BACKGROUNDS_GPT2, default="auto",
tooltip="Background handling. gpt-image-2 does not support transparent."),
]
def _gpt_image_25_inputs():
"""Per-model widgets for gpt-image-2.5: hi-res/custom sizes, transparent allowed."""
return [
_size_combo_gpt2(),
IO.Combo.Input("background", options=params_mod.BACKGROUNDS, default="auto",
tooltip="Background handling. 'transparent' needs png or webp output."),
]
def unpack_size(model: dict):
"""Pull (size, custom_width, custom_height) from a model dict.
`size` is itself a nested DynamicCombo, so it arrives as a dict.
"""
sz = model["size"]
return sz["size"], sz.get("custom_width", 1024), sz.get("custom_height", 1024)
class OpenAIImageGenerate(IO.ComfyNode):
@classmethod
def define_schema(cls):
return IO.Schema(
node_id="ImagentOpenAIImage",
display_name="🤖 Imagent: OpenAI Image",
category="Imagent",
description="BYOK text-to-image via OpenAI gpt-image models (direct API, no proxy).",
inputs=[
IO.String.Input("prompt", multiline=True, default="",
placeholder="Describe the image to generate…",
tooltip="Text description of the image to generate."),
IO.DynamicCombo.Input(
"model",
tooltip="OpenAI gpt-image model. Switching this changes the options below.",
options=[
IO.DynamicCombo.Option("gpt-image-2.5-flare", _gpt_image_25_inputs()),
IO.DynamicCombo.Option("gpt-image-2.5-sunburst", _gpt_image_25_inputs()),
IO.DynamicCombo.Option("gpt-image-2", _gpt_image_2_inputs()),
],
),
IO.Combo.Input("quality", options=params_mod.QUALITIES, default="auto",
tooltip="Rendering quality. 'auto' lets the model decide; "
"higher quality costs more. 'xhigh' and 'max' need "
"gpt-image-2.5 and fall back to 'high' elsewhere."),
IO.Combo.Input("output_format", options=params_mod.FORMATS, default="png",
tooltip="Image file format returned by the API."),
IO.Int.Input("output_compression", default=100, min=0, max=100,
tooltip="Compression 0-100. Applies only to jpeg and webp output."),
IO.Combo.Input("moderation", options=params_mod.MODERATIONS, default="auto",
tooltip="Content moderation strictness. 'auto' (default) or "
"'low' (less restrictive). Sent only when not 'auto'."),
IO.Int.Input("n", default=1, min=params_mod.N_MIN, max=params_mod.N_MAX, step=1,
tooltip="How many images to generate (1-8)."),
],
outputs=[IO.Image.Output(display_name="image"),
IO.Mask.Output(display_name="mask")],
)
@classmethod
def execute(cls, prompt, model, quality, output_format, output_compression,
moderation, n) -> IO.NodeOutput:
size, custom_width, custom_height = unpack_size(model)
img, mask, _info = build.run_generate(
prompt=prompt, model=model["model"], size=size, quality=quality,
background=model["background"], output_format=output_format, n=n,
moderation=moderation, custom_width=custom_width, custom_height=custom_height,
output_compression=output_compression)
return IO.NodeOutput(img, mask)