Merge pull request #150 from outis1one/claude/gifted-shannon-5ti8vx
Fix PaintPlus OpenAI edit quality: edits silently fell back to dall-e-2
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:-}
|
||||
|
||||
|
||||
@@ -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:-}
|
||||
|
||||
Reference in New Issue
Block a user