BYOK OpenAI gpt-image generation and editing nodes for ComfyUI — calls go directly to the OpenAI API with your own key (no proxy, no credit markup). - Two nodes: "Imagent: OpenAI Image" (text-to-image) and "Imagent: OpenAI Image Edit" (edit / inpaint / multi-reference compose). - Models gpt-image-2 / gpt-image-1.5 / gpt-image-1 with a per-model dynamic UI (comfy_api DynamicCombo): size, background, custom WxH, and input_fidelity are revealed only for the models that support them. - Edit node accepts an auto-growing set of reference images (up to 16) plus an optional inpaint mask; generate supports batches up to 8. - API key via OPENAI_API_KEY or a local config.json. Requires ComfyUI >= 0.23.0.
112 lines
5.3 KiB
Python
112 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 _legacy_inputs():
|
|
"""Per-model widgets for gpt-image-1 / gpt-image-1.5: base sizes, transparent allowed."""
|
|
return [
|
|
IO.Combo.Input("size", options=params_mod.SIZES_BASE, default="auto",
|
|
tooltip="Output size (base sizes supported by gpt-image-1.x)."),
|
|
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.
|
|
|
|
For gpt-image-2 `size` is a nested DynamicCombo (dict); for gpt-image-1.x it's
|
|
a plain combo (string).
|
|
"""
|
|
sz = model["size"]
|
|
if isinstance(sz, dict):
|
|
return sz["size"], sz.get("custom_width", 1024), sz.get("custom_height", 1024)
|
|
return sz, 1024, 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", _gpt_image_2_inputs()),
|
|
IO.DynamicCombo.Option("gpt-image-1.5", _legacy_inputs()),
|
|
IO.DynamicCombo.Option("gpt-image-1", _legacy_inputs()),
|
|
],
|
|
),
|
|
IO.Combo.Input("quality", options=params_mod.QUALITIES, default="auto",
|
|
tooltip="Rendering quality. 'auto' lets the model decide; "
|
|
"higher quality costs more."),
|
|
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")],
|
|
)
|
|
|
|
@classmethod
|
|
def execute(cls, prompt, model, quality, output_format, output_compression,
|
|
moderation, n) -> IO.NodeOutput:
|
|
size, custom_width, custom_height = unpack_size(model)
|
|
img, _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)
|