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

121 lines
5.3 KiB
Python

"""Pure request-building / dispatch logic for the two nodes.
This module deliberately imports nothing from ComfyUI (`comfy_api`), so the whole
behavior — model gating, kwargs assembly, error handling — is unit-testable
without a running ComfyUI. The IO node wrappers in node_generate.py /
node_edit.py only marshal widget values into these functions.
Public entry points: run_generate(), run_edit().
"""
from __future__ import annotations
import io
import logging
from PIL import Image
from . import client as client_mod
from . import image_io
from . import params as params_mod
log = logging.getLogger("imagent")
_NO_KEY = "Error: OPENAI_API_KEY not found (set the env var or config.json)."
_NO_IMAGES = "Error: API returned no images."
def _named_png(pil_image: Image.Image, name: str) -> io.BytesIO:
buf = io.BytesIO(image_io.pil_to_png_bytes(pil_image))
buf.name = name # OpenAI SDK infers content-type from the filename
return buf
def run_generate(*, prompt, model, size, quality, background, output_format, n,
moderation="auto", custom_width=1024, custom_height=1024,
output_compression=100):
"""Text-to-image via images.generate.
Returns (image_tensor, mask_tensor, info_string); the mask carries the
alpha channel when background='transparent'. Never raises: a failed call
yields an empty image plus a friendly error so a bad call can't crash the queue.
"""
oai = client_mod.get_client()
if oai is None:
log.warning("imagent: %s", _NO_KEY)
return (image_io.empty_image(), image_io.empty_mask(), _NO_KEY)
try:
resolved_size = params_mod.resolve_size(size, custom_width, custom_height)
resolved_background = params_mod.resolve_background(background, model)
kwargs = {
"model": model, "prompt": prompt, "n": n,
"size": resolved_size, "quality": params_mod.resolve_quality(quality, model),
"background": resolved_background, "output_format": output_format,
}
if output_format in ("jpeg", "webp"):
kwargs["output_compression"] = output_compression
if moderation != "auto":
kwargs["moderation"] = moderation
resp = oai.images.generate(**kwargs)
if not resp.data:
return (image_io.empty_image(), image_io.empty_mask(), _NO_IMAGES)
info = getattr(resp.data[0], "revised_prompt", None) \
or f"Generated {len(resp.data)} image(s) with {model}."
return (*image_io.batch_from_response_data(resp.data), info)
except Exception as exc: # noqa: BLE001 - node must not crash the queue
log.exception("imagent: image generation failed")
return (image_io.empty_image(), image_io.empty_mask(), client_mod.friendly_error(exc))
def run_edit(*, prompt, model, size, quality, background, output_format, n,
images, mask=None, moderation="auto",
custom_width=1024, custom_height=1024):
"""Image edit / inpaint / multi-reference via images.edit.
`images` is an iterable of IMAGE tensors (None entries are ignored).
Returns (image_tensor, mask_tensor, info_string); never raises (see run_generate).
"""
oai = client_mod.get_client()
if oai is None:
log.warning("imagent: %s", _NO_KEY)
return (image_io.empty_image(), image_io.empty_mask(), _NO_KEY)
refs = [t for t in (images or []) if t is not None]
if not refs:
msg = "Error: OpenAIImageEdit requires at least one reference image."
log.warning("imagent: %s", msg)
return (image_io.empty_image(), image_io.empty_mask(), msg)
if mask is not None and len(refs) > 1:
msg = ("Error: mask inpainting requires a single reference image "
"(connect only one image).")
log.warning("imagent: %s", msg)
return (image_io.empty_image(), image_io.empty_mask(), msg)
try:
resolved_size = params_mod.resolve_size(size, custom_width, custom_height)
resolved_background = params_mod.resolve_background(background, model)
# Downscale oversized references to fit the API's input pixel budget.
ref_pils = [image_io.downscale_pil_to_pixel_limit(image_io.tensor_to_pil(t)[0])
for t in refs]
kwargs = {
"model": model, "prompt": prompt,
"image": [_named_png(p, f"ref_{i}.png") for i, p in enumerate(ref_pils)],
"n": n, "size": resolved_size,
"quality": params_mod.resolve_quality(quality, model),
"background": resolved_background, "output_format": output_format,
}
if mask is not None:
kwargs["mask"] = image_io.mask_to_named_png(ref_pils[0], mask, "mask.png")
if moderation != "auto":
kwargs["moderation"] = moderation
resp = oai.images.edit(**kwargs)
if not resp.data:
return (image_io.empty_image(), image_io.empty_mask(), _NO_IMAGES)
info = getattr(resp.data[0], "revised_prompt", None) \
or f"Edited image with {model}."
return (*image_io.batch_from_response_data(resp.data), info)
except Exception as exc: # noqa: BLE001 - node must not crash the queue
log.exception("imagent: image edit failed")
return (image_io.empty_image(), image_io.empty_mask(), client_mod.friendly_error(exc))