diff --git a/paintplus/.env.example b/paintplus/.env.example index 93e4b13..2d14587 100644 --- a/paintplus/.env.example +++ b/paintplus/.env.example @@ -19,18 +19,22 @@ # local_gpu = FREE, runs on YOUR GPU — best option if you have an NVIDIA card # (use docker-compose.gpu.yml — models auto-download on first use) # mock = Free, returns original image unchanged (UI testing only) -# openai = DALL-E 3 / gpt-image-1 (~$0.02-0.04/image) -# stability = Stability AI SDXL (~$0.01/image) -# replicate = Multiple models (~$0.002-0.03/image) +# openai = gpt-image-2 generation + edits (~$0.006-0.21/image, see below) +# stability = NOT IMPLEMENTED YET — config field exists but no driver in +# remote_provider.py; setting this breaks every AI call +# replicate = NOT IMPLEMENTED YET — same as above (Replicate IS used for +# Smart Select's SAM fallback, but that's a separate code path) # invokeai = Self-hosted InvokeAI running on another machine # comfyui = Self-hosted ComfyUI running on another machine # # GPU QUICK-START: # docker compose -f docker-compose.gpu.yml up --build -# (AI_PROVIDER defaults to local_gpu in that compose file) +# (AI_PROVIDER defaults to local_gpu in that compose file — but only if this +# var is unset/blank; since AI_PROVIDER=local_gpu is set explicitly below, +# that's what you get either way) # ============================================================================= -AI_PROVIDER=replicate +AI_PROVIDER=local_gpu # ── Local GPU settings (only relevant when AI_PROVIDER=local_gpu) ──────────── # Auto-download HuggingFace models on first request (true/false) @@ -40,7 +44,11 @@ LOCAL_GPU_MAX_PIPELINES=2 # HuggingFace token — only needed for gated/private models #HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # Override auto-selected model for any operation (leave blank = auto by VRAM tier) -#HF_MODEL_INPAINT=your-org/your-inpaint-model +# On 4-6GB cards (Quadro P2200, GTX 1060/1660, etc.) the auto-tier picks either +# slow SDXL+CPU-offload or a generic (not hand/face-tuned) SD checkpoint. +# Lykon/dreamshaper-8-inpainting is SD1.5-based (1.7GB, fast, no offload needed) +# and noticeably better on hands/faces — worth forcing on small cards: +#HF_MODEL_INPAINT=Lykon/dreamshaper-8-inpainting #HF_MODEL_TXT2IMG=your-org/your-txt2img-model #HF_MODEL_IMG2IMG=your-org/your-img2img-model # ───────────────────────────────────────────────────────────────────────────── @@ -76,17 +84,19 @@ LOCAL_GPU_MAX_PIPELINES=2 REPLICATE_API_KEY=r8_PASTE_YOUR_KEY_HERE # ─────────────────────────────────────────────────────────────────────────── -# OPENAI (cloud, dall-e-3 / gpt-image-1) +# OPENAI (cloud, gpt-image-2) # Get key at: https://platform.openai.com/api-keys # AI_PROVIDER=openai # ─────────────────────────────────────────────────────────────────────────── +# dall-e-2 and dall-e-3 were retired May 12, 2026; gpt-image-1 deprecates +# Oct 23, 2026. gpt-image-2 is the current model and handles both +# generation and masked edits (inpaint/img2img/outpaint) — no org +# verification step needed, unlike gpt-image-1. +# Pricing (1024x1024): ~$0.006 low / $0.053 medium / $0.211 high quality. #OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -#OPENAI_MODEL=dall-e-3 +#OPENAI_MODEL=gpt-image-2 # 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 +#OPENAI_EDIT_MODEL=gpt-image-2 # ─────────────────────────────────────────────────────────────────────────── # INVOKEAI (self-hosted, best for Flux/SDXL) diff --git a/paintplus/README.md b/paintplus/README.md index 8033ed7..e784f14 100644 --- a/paintplus/README.md +++ b/paintplus/README.md @@ -65,7 +65,7 @@ BUILDID=$(date +%s) ./bring-up-local-gpu.sh | Provider | Setup | Cost | Quality | |---|---|---|---| | `local_gpu` | GPU machine + nvidia-container-toolkit | Free | Best (SDXL/FLUX auto-selected by VRAM) | -| `openai` | `OPENAI_API_KEY=sk-...` | ~$0.02–0.04/image | DALL-E 3 | +| `openai` | `OPENAI_API_KEY=sk-...` | ~$0.006–0.21/image | gpt-image-2 | | `replicate` | `REPLICATE_API_KEY=r8_...` | ~$0.002–0.03/image | Multiple models | | `invokeai` | InvokeAI running on another machine | Self-hosted | FLUX/SDXL | | `comfyui` | ComfyUI running on another machine | Self-hosted | Any model | diff --git a/paintplus/backend/app/config.py b/paintplus/backend/app/config.py index 017bc8e..5fea79f 100644 --- a/paintplus/backend/app/config.py +++ b/paintplus/backend/app/config.py @@ -27,8 +27,8 @@ class Settings(BaseSettings): # Provider API Keys openai_api_key: str = "" - 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) + openai_model: str = "gpt-image-2" # text-to-image (generations endpoint) + openai_edit_model: str = "gpt-image-2" # inpaint/img2img/outpaint (edits endpoint) stability_api_key: str = "" replicate_api_key: str = "" diff --git a/paintplus/backend/app/services/ai_provider.py b/paintplus/backend/app/services/ai_provider.py index 1ca4587..e7d5b28 100644 --- a/paintplus/backend/app/services/ai_provider.py +++ b/paintplus/backend/app/services/ai_provider.py @@ -62,12 +62,22 @@ class AIProvider(ABC): class OpenAIProvider(AIProvider): - """OpenAI DALL-E 2 based image editing (NOTE: Lower quality than DALL-E 3)""" + """OpenAI image API — gpt-image-2 (generation + edits, same model/endpoint family).""" - def __init__(self, api_key: str): + def __init__(self, api_key: str, model: str = "gpt-image-2", edit_model: str = "gpt-image-2"): self.api_key = api_key + self.model = model + self.edit_model = edit_model self.base_url = "https://api.openai.com/v1" + async def _fetch_result(self, client: httpx.AsyncClient, item: dict) -> bytes: + # gpt-image-1/2 only ever return b64_json; dall-e-2/dall-e-3 default to a url. + if item.get('b64_json'): + return base64.b64decode(item['b64_json']) + image_response = await client.get(item['url']) + image_response.raise_for_status() + return image_response.content + async def edit_image( self, patch_image_bytes: bytes, @@ -77,7 +87,7 @@ class OpenAIProvider(AIProvider): full_image_bytes: Optional[bytes] = None, model: Optional[str] = None ) -> bytes: - """Edit image using OpenAI DALL-E 2 (NOTE: Uses older model, lower quality)""" + """Edit image using OpenAI's images/edits endpoint""" async with httpx.AsyncClient(timeout=60.0) as client: files = { @@ -86,6 +96,7 @@ class OpenAIProvider(AIProvider): } data = { + 'model': model or self.edit_model, 'prompt': prompt, 'n': 1, 'size': '1024x1024' # Will be adjusted based on input @@ -104,13 +115,7 @@ class OpenAIProvider(AIProvider): response.raise_for_status() result = response.json() - - # Download the generated image - image_url = result['data'][0]['url'] - image_response = await client.get(image_url) - image_response.raise_for_status() - - return image_response.content + return await self._fetch_result(client, result['data'][0]) async def text_to_image( self, @@ -120,10 +125,11 @@ class OpenAIProvider(AIProvider): model: Optional[str] = None, negative_prompt: Optional[str] = None ) -> bytes: - """Generate image using OpenAI DALL-E""" + """Generate image using OpenAI's images/generations endpoint""" async with httpx.AsyncClient(timeout=60.0) as client: data = { + 'model': model or self.model, 'prompt': prompt, 'n': 1, 'size': f'{width}x{height}' if width == height else '1024x1024' @@ -141,13 +147,7 @@ class OpenAIProvider(AIProvider): response.raise_for_status() result = response.json() - - # Download the generated image - image_url = result['data'][0]['url'] - image_response = await client.get(image_url) - image_response.raise_for_status() - - return image_response.content + return await self._fetch_result(client, result['data'][0]) class StabilityAIProvider(AIProvider): @@ -518,7 +518,7 @@ def get_ai_provider(provider_name: Optional[str] = None, model: Optional[str] = if provider == "openai": if not settings.openai_api_key: raise ValueError("OpenAI API key not configured") - return OpenAIProvider(settings.openai_api_key) + return OpenAIProvider(settings.openai_api_key, settings.openai_model, settings.openai_edit_model) elif provider == "stability": if not settings.stability_api_key: diff --git a/paintplus/backend/app/services/remote_provider.py b/paintplus/backend/app/services/remote_provider.py index 4d6f43e..a83fac9 100644 --- a/paintplus/backend/app/services/remote_provider.py +++ b/paintplus/backend/app/services/remote_provider.py @@ -28,12 +28,11 @@ class RemoteAIProvider(ABC): class OpenAIRemoteProvider(RemoteAIProvider): - """OpenAI image API — gpt-image-1 / dall-e-3.""" + """OpenAI image API — gpt-image-2 (generation + edits, same model/endpoint family).""" - def __init__(self, api_key: str, model: str = "dall-e-3", edit_model: str = "gpt-image-1"): + def __init__(self, api_key: str, model: str = "gpt-image-2", edit_model: str = "gpt-image-2"): 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" @@ -41,7 +40,7 @@ class OpenAIRemoteProvider(RemoteAIProvider): 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. + # gpt-image-1/2 only ever return 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"]) diff --git a/paintplus/docker-compose.gpu.yml b/paintplus/docker-compose.gpu.yml index 4d0ea7b..ed49184 100644 --- a/paintplus/docker-compose.gpu.yml +++ b/paintplus/docker-compose.gpu.yml @@ -96,8 +96,8 @@ 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} + - OPENAI_MODEL=${OPENAI_MODEL:-gpt-image-2} + - OPENAI_EDIT_MODEL=${OPENAI_EDIT_MODEL:-gpt-image-2} - REPLICATE_API_KEY=${REPLICATE_API_KEY:-} - STABILITY_API_KEY=${STABILITY_API_KEY:-} diff --git a/paintplus/docker-compose.yml b/paintplus/docker-compose.yml index 3e6c6ab..2ff9f93 100644 --- a/paintplus/docker-compose.yml +++ b/paintplus/docker-compose.yml @@ -14,8 +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} + - OPENAI_MODEL=${OPENAI_MODEL:-gpt-image-2} + - OPENAI_EDIT_MODEL=${OPENAI_EDIT_MODEL:-gpt-image-2} - STABILITY_API_KEY=${STABILITY_API_KEY:-} - REPLICATE_API_KEY=${REPLICATE_API_KEY:-} - INVOKEAI_URL=${INVOKEAI_URL:-} diff --git a/paintplus/frontend/src/js/modules/tools/ai_provider_settings.js b/paintplus/frontend/src/js/modules/tools/ai_provider_settings.js index b4694e6..f18e54c 100644 --- a/paintplus/frontend/src/js/modules/tools/ai_provider_settings.js +++ b/paintplus/frontend/src/js/modules/tools/ai_provider_settings.js @@ -115,8 +115,8 @@ class Tools_ai_provider_settings_class { { name: 'openai_model', title: 'OpenAI model:', - value: ls_get('openai_model', 'dall-e-3'), - values: ['dall-e-3', 'dall-e-2'], + value: ls_get('openai_model', 'gpt-image-2'), + values: ['gpt-image-2', 'gpt-image-1'], type: 'select', }, // ── InvokeAI ────────────────────────────────────────────── @@ -171,7 +171,7 @@ class Tools_ai_provider_settings_class { ls_set('provider_img2img', params.provider_img2img || ''); ls_set('provider_outpaint', params.provider_outpaint || ''); ls_set('openai_key', params.openai_key || ''); - ls_set('openai_model', params.openai_model || 'dall-e-3'); + ls_set('openai_model', params.openai_model || 'gpt-image-2'); ls_set('invokeai_url', params.invokeai_url || ''); ls_set('invokeai_model', params.invokeai_model || 'flux-dev'); ls_set('comfyui_url', params.comfyui_url || ''); @@ -187,7 +187,7 @@ class Tools_ai_provider_settings_class { ai_provider_img2img: params.provider_img2img || '', ai_provider_outpaint: params.provider_outpaint || '', openai_api_key: params.openai_key || '', - openai_model: params.openai_model || 'dall-e-3', + openai_model: params.openai_model || 'gpt-image-2', invokeai_url: params.invokeai_url || '', invokeai_default_model: params.invokeai_model || 'flux-dev', comfyui_url: params.comfyui_url || '',