Fix PaintPlus OpenAI edit quality: edits silently fell back to dall-e-2

inpaint()/img2img()/outpaint() called /v1/images/edits without a model
field, so OpenAI defaulted every cloud edit to dall-e-2 regardless of
configuration — while txt2img used dall-e-3. Add a separate
OPENAI_EDIT_MODEL (default gpt-image-1, the only current model that
supports masked edits at ChatGPT-comparable quality), thread it through
the provider and both compose files, and handle gpt-image-1's
b64_json-only response shape alongside the url shape dall-e-2/3 return.
This commit is contained in:
Claude
2026-06-26 13:57:04 +00:00
parent efc4100640
commit 36a126bee7
5 changed files with 28 additions and 13 deletions
+5
View File
@@ -82,6 +82,11 @@ REPLICATE_API_KEY=r8_PASTE_YOUR_KEY_HERE
# ───────────────────────────────────────────────────────────────────────────
#OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
#OPENAI_MODEL=dall-e-3
# Model for inpaint/img2img/outpaint (the /v1/images/edits endpoint).
# dall-e-3 does NOT support edits at all — keep this on gpt-image-1 for
# ChatGPT-comparable edit quality, or set dall-e-2 for the older/cheaper option.
# Note: gpt-image-1 may require completing org verification at platform.openai.com.
#OPENAI_EDIT_MODEL=gpt-image-1
# ───────────────────────────────────────────────────────────────────────────
# INVOKEAI (self-hosted, best for Flux/SDXL)
+2 -1
View File
@@ -27,7 +27,8 @@ class Settings(BaseSettings):
# Provider API Keys
openai_api_key: str = ""
openai_model: str = "dall-e-3"
openai_model: str = "dall-e-3" # text-to-image (generations endpoint)
openai_edit_model: str = "gpt-image-1" # inpaint/img2img/outpaint (edits endpoint — dall-e-3 isn't supported there)
stability_api_key: str = ""
replicate_api_key: str = ""
@@ -30,38 +30,44 @@ class RemoteAIProvider(ABC):
class OpenAIRemoteProvider(RemoteAIProvider):
"""OpenAI image API — gpt-image-1 / dall-e-3."""
def __init__(self, api_key: str, model: str = "dall-e-3"):
def __init__(self, api_key: str, model: str = "dall-e-3", edit_model: str = "gpt-image-1"):
self.api_key = api_key
self.model = model
# dall-e-3 has no edits/inpaint support at all — edits need a model of their own.
self.edit_model = edit_model
self.base_url = "https://api.openai.com/v1"
def _headers(self):
return {"Authorization": f"Bearer {self.api_key}"}
async def _fetch_result(self, client: httpx.AsyncClient, item: dict) -> bytes:
# gpt-image-1 only ever returns b64_json; dall-e-2/dall-e-3 default to a url.
if item.get("b64_json"):
return base64.b64decode(item["b64_json"])
img_r = await client.get(item["url"])
img_r.raise_for_status()
return img_r.content
async def inpaint(self, image_bytes: bytes, mask_bytes: bytes, prompt: str, params: dict) -> bytes:
model = (params or {}).get("model") or self.edit_model
async with httpx.AsyncClient(timeout=120.0) as client:
files = {
"image": ("image.png", image_bytes, "image/png"),
"mask": ("mask.png", mask_bytes, "image/png"),
}
data = {"prompt": prompt, "n": "1", "size": "1024x1024"}
data = {"model": model, "prompt": prompt, "n": "1", "size": "1024x1024"}
r = await client.post(f"{self.base_url}/images/edits", files=files, data=data, headers=self._headers())
r.raise_for_status()
url = r.json()["data"][0]["url"]
img_r = await client.get(url)
img_r.raise_for_status()
return img_r.content
return await self._fetch_result(client, r.json()["data"][0])
async def txt2img(self, prompt: str, width: int, height: int, params: dict) -> bytes:
size = f"{width}x{height}" if f"{width}x{height}" in {"256x256", "512x512", "1024x1024"} else "1024x1024"
model = (params or {}).get("model") or self.model
async with httpx.AsyncClient(timeout=120.0) as client:
data = {"model": self.model, "prompt": prompt, "n": 1, "size": size}
data = {"model": model, "prompt": prompt, "n": 1, "size": size}
r = await client.post(f"{self.base_url}/images/generations", json=data, headers=self._headers())
r.raise_for_status()
url = r.json()["data"][0]["url"]
img_r = await client.get(url)
img_r.raise_for_status()
return img_r.content
return await self._fetch_result(client, r.json()["data"][0])
async def img2img(self, image_bytes: bytes, prompt: str, strength: float, params: dict) -> bytes:
# OpenAI doesn't have img2img natively — use edits with blank mask
@@ -416,7 +422,7 @@ def _build_provider(name: str) -> Optional[RemoteAIProvider]:
if name == "openai":
if not settings.openai_api_key:
return None
return OpenAIRemoteProvider(settings.openai_api_key, settings.openai_model)
return OpenAIRemoteProvider(settings.openai_api_key, settings.openai_model, settings.openai_edit_model)
if name == "invokeai":
if not settings.invokeai_url:
+1
View File
@@ -97,6 +97,7 @@ services:
# ── Remote/cloud providers (all optional) ────────────────────────────────
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
- OPENAI_MODEL=${OPENAI_MODEL:-dall-e-3}
- OPENAI_EDIT_MODEL=${OPENAI_EDIT_MODEL:-gpt-image-1}
- REPLICATE_API_KEY=${REPLICATE_API_KEY:-}
- STABILITY_API_KEY=${STABILITY_API_KEY:-}
+2
View File
@@ -14,6 +14,8 @@ services:
- SECRET_KEY=${SECRET_KEY:-change-this-secret-key-in-production}
- AI_PROVIDER=${AI_PROVIDER:-mock}
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
- OPENAI_MODEL=${OPENAI_MODEL:-dall-e-3}
- OPENAI_EDIT_MODEL=${OPENAI_EDIT_MODEL:-gpt-image-1}
- STABILITY_API_KEY=${STABILITY_API_KEY:-}
- REPLICATE_API_KEY=${REPLICATE_API_KEY:-}
- INVOKEAI_URL=${INVOKEAI_URL:-}