selfit-camera commited on
Commit
0d79725
·
1 Parent(s): 9aefdc2
Files changed (2) hide show
  1. app.py +140 -136
  2. util.py +93 -39
app.py CHANGED
@@ -140,25 +140,29 @@ def edit_image_interface(input_image, prompt, request: gr.Request, progress=gr.P
140
  """
141
  Interface function for processing image editing
142
  """
143
- # 提取用户IP
144
- client_ip = request.client.host
145
- x_forwarded_for = dict(request.headers).get('x-forwarded-for')
146
- if x_forwarded_for:
147
- client_ip = x_forwarded_for
148
- if client_ip not in IP_Dict:
149
- IP_Dict[client_ip] = 0
150
- IP_Dict[client_ip] += 1
 
151
 
152
-
153
- if input_image is None:
154
- return None, "Please upload an image first"
155
-
156
- if not prompt or prompt.strip() == "":
157
- return None, "Please enter editing prompt"
158
-
159
- # 检查prompt长度是否大于3个字符
160
- if len(prompt.strip()) <= 3:
161
- return None, "❌ Editing prompt must be more than 3 characters"
 
 
 
162
 
163
  # 检查图片是否包含NSFW内容
164
  nsfw_result = None
@@ -196,9 +200,14 @@ def edit_image_interface(input_image, prompt, request: gr.Request, progress=gr.P
196
  status_message = ""
197
 
198
  def progress_callback(message):
199
- nonlocal status_message
200
- status_message = message
201
- progress(0.5, desc=message)
 
 
 
 
 
202
 
203
  try:
204
  # 打印成功访问的信息
@@ -209,93 +218,59 @@ def edit_image_interface(input_image, prompt, request: gr.Request, progress=gr.P
209
 
210
  if result_url:
211
  print(f"✅ Processing completed successfully - IP: {client_ip}, result_url: {result_url}", flush=True)
212
- progress(1.0, desc="Processing completed")
 
 
 
 
213
  return result_url, "✅ " + message
214
  else:
215
  print(f"❌ Processing failed - IP: {client_ip}, error: {message}", flush=True)
216
  return None, "❌ " + message
217
 
218
  except Exception as e:
 
219
  return None, f"❌ Error occurred during processing: {str(e)}"
220
 
221
- # 局部编辑处理函数
222
- # 状态更新函数
223
- def update_global_input_state(image):
224
- """更新全局编辑输入图片状态"""
225
- return image
226
-
227
- def update_global_output_state(image):
228
- """更新全局编辑输出图片状态"""
229
- return image
230
-
231
- def update_local_input_state(image):
232
- """更新局部编辑输入图片状态"""
233
- return image
234
-
235
- def update_local_output_state(image):
236
- """更新局部编辑输出图片状态"""
237
- return image
238
-
239
- def use_global_output_as_input(output_image):
240
- """将全局编辑的输出图片设为输入图片"""
241
- if output_image is not None:
242
- return output_image, output_image
243
- return None, None
244
-
245
- def use_local_output_as_input(output_image):
246
- """将局部编辑的输出图片设为输入图片"""
247
- if output_image is not None:
248
- # 对于局部编辑,我们需要创建一个新的ImageEditor格式
249
- # 将输出图片作为背景,不包含任何编辑层
250
- editor_data = {
251
- "background": output_image,
252
- "layers": [],
253
- "composite": output_image # 添加composite键以避免KeyError
254
- }
255
- return editor_data, output_image
256
- return None, None
257
-
258
- def load_global_input_from_state(state_image):
259
- """从状态加载全局编辑输入图片"""
260
- return state_image
261
-
262
- def load_local_input_from_state(state_image):
263
- """从状态加载局部编辑输入图片"""
264
- return state_image
265
 
266
  def local_edit_interface(image_dict, prompt, request: gr.Request, progress=gr.Progress()):
267
  """
268
  处理局部编辑请求
269
  """
270
- # 提取用户IP
271
- client_ip = request.client.host
272
- x_forwarded_for = dict(request.headers).get('x-forwarded-for')
273
- if x_forwarded_for:
274
- client_ip = x_forwarded_for
275
- if client_ip not in IP_Dict:
276
- IP_Dict[client_ip] = 0
277
- IP_Dict[client_ip] += 1
 
278
 
279
-
280
- if image_dict is None:
281
- return None, "Please upload an image and draw the area to edit"
282
-
283
- # Check if background and layers exist
284
- if "background" not in image_dict or "layers" not in image_dict:
285
- return None, "Please draw the area to edit on the image"
286
-
287
- base_image = image_dict["background"]
288
- layers = image_dict["layers"]
289
-
290
- if not layers:
291
- return None, "Please draw the area to edit on the image"
292
 
293
- if not prompt or prompt.strip() == "":
294
- return None, "Please enter editing prompt"
295
-
296
- # Check prompt length
297
- if len(prompt.strip()) <= 3:
298
- return None, " Editing prompt must be more than 3 characters"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
 
300
  # 检查图片是否包含NSFW内容
301
  nsfw_result = None
@@ -330,9 +305,14 @@ def local_edit_interface(image_dict, prompt, request: gr.Request, progress=gr.Pr
330
  status_message = ""
331
 
332
  def progress_callback(message):
333
- nonlocal status_message
334
- status_message = message
335
- progress(0.5, desc=message)
 
 
 
 
 
336
 
337
  try:
338
  print(f"✅ Local editing started - IP: {client_ip}, count: {IP_Dict[client_ip]}, prompt: {prompt.strip()}", flush=True)
@@ -342,13 +322,18 @@ def local_edit_interface(image_dict, prompt, request: gr.Request, progress=gr.Pr
342
 
343
  if result_url:
344
  print(f"✅ Local editing completed successfully - IP: {client_ip}, result_url: {result_url}", flush=True)
345
- progress(1.0, desc="Processing completed")
 
 
 
 
346
  return result_url, "✅ " + message
347
  else:
348
  print(f"❌ Local editing processing failed - IP: {client_ip}, error: {message}", flush=True)
349
  return None, "❌ " + message
350
 
351
  except Exception as e:
 
352
  return None, f"❌ Error occurred during processing: {str(e)}"
353
 
354
  # Create Gradio interface
@@ -377,14 +362,20 @@ def create_app():
377
  margin-top: 10px;
378
  width: 100%;
379
  }
 
 
 
 
 
 
 
 
 
380
  """
381
  ) as app:
382
 
383
- # 使用State组件保持图片状态,防止切换tab时丢失
384
- global_input_state = gr.State()
385
- global_output_state = gr.State()
386
- local_input_state = gr.State()
387
- local_output_state = gr.State()
388
 
389
  gr.Markdown(
390
  """
@@ -463,30 +454,27 @@ def create_app():
463
  outputs=prompt_input
464
  )
465
 
466
- # 绑定按钮点击事件
467
  edit_button.click(
468
  fn=edit_image_interface,
469
  inputs=[input_image, prompt_input],
470
  outputs=[output_image, status_output],
471
- show_progress=True
472
- ).then(
473
- fn=update_global_output_state,
474
- inputs=[output_image],
475
- outputs=[global_output_state]
476
  )
477
 
478
- # 绑定 "Use as Input" 按钮
 
 
 
 
 
479
  use_as_input_btn.click(
480
- fn=use_global_output_as_input,
481
  inputs=[output_image],
482
- outputs=[input_image, global_input_state]
483
- )
484
-
485
- # 绑定输入图片变化事件以更新状态
486
- input_image.change(
487
- fn=update_global_input_state,
488
- inputs=[input_image],
489
- outputs=[global_input_state]
490
  )
491
 
492
  # Local editing tab
@@ -559,35 +547,51 @@ def create_app():
559
  outputs=local_prompt_input
560
  )
561
 
562
- # 绑定局部编辑按钮点击事件
563
  local_edit_button.click(
564
  fn=local_edit_interface,
565
  inputs=[local_input_image, local_prompt_input],
566
  outputs=[local_output_image, local_status_output],
567
- show_progress=True
568
- ).then(
569
- fn=update_local_output_state,
570
- inputs=[local_output_image],
571
- outputs=[local_output_state]
572
  )
573
 
574
- # 绑定局部编辑 "Use as Input" 按钮
 
 
 
 
 
 
 
 
 
 
 
575
  local_use_as_input_btn.click(
576
- fn=use_local_output_as_input,
577
  inputs=[local_output_image],
578
- outputs=[local_input_image, local_input_state]
579
- )
580
-
581
- # 绑定局部编辑输入图片变化事件以更新状态
582
- local_input_image.change(
583
- fn=update_local_input_state,
584
- inputs=[local_input_image],
585
- outputs=[local_input_state]
586
  )
587
 
588
  return app
589
 
590
  if __name__ == "__main__":
591
  app = create_app()
592
- app.queue() # Enable queue to handle concurrent requests
593
- app.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  """
141
  Interface function for processing image editing
142
  """
143
+ try:
144
+ # 提取用户IP
145
+ client_ip = request.client.host
146
+ x_forwarded_for = dict(request.headers).get('x-forwarded-for')
147
+ if x_forwarded_for:
148
+ client_ip = x_forwarded_for
149
+ if client_ip not in IP_Dict:
150
+ IP_Dict[client_ip] = 0
151
+ IP_Dict[client_ip] += 1
152
 
153
+
154
+ if input_image is None:
155
+ return None, "Please upload an image first"
156
+
157
+ if not prompt or prompt.strip() == "":
158
+ return None, "Please enter editing prompt"
159
+
160
+ # 检查prompt长度是否大于3个字符
161
+ if len(prompt.strip()) <= 3:
162
+ return None, "❌ Editing prompt must be more than 3 characters"
163
+ except Exception as e:
164
+ print(f"⚠️ Request preprocessing error: {e}")
165
+ return None, "❌ Request processing error"
166
 
167
  # 检查图片是否包含NSFW内容
168
  nsfw_result = None
 
200
  status_message = ""
201
 
202
  def progress_callback(message):
203
+ try:
204
+ nonlocal status_message
205
+ status_message = message
206
+ # 增加错误处理,防止 progress 更新失败
207
+ if progress is not None:
208
+ progress(0.5, desc=message)
209
+ except Exception as e:
210
+ print(f"⚠️ Progress update failed: {e}")
211
 
212
  try:
213
  # 打印成功访问的信息
 
218
 
219
  if result_url:
220
  print(f"✅ Processing completed successfully - IP: {client_ip}, result_url: {result_url}", flush=True)
221
+ try:
222
+ if progress is not None:
223
+ progress(1.0, desc="Processing completed")
224
+ except Exception as e:
225
+ print(f"⚠️ Final progress update failed: {e}")
226
  return result_url, "✅ " + message
227
  else:
228
  print(f"❌ Processing failed - IP: {client_ip}, error: {message}", flush=True)
229
  return None, "❌ " + message
230
 
231
  except Exception as e:
232
+ print(f"❌ Processing exception - IP: {client_ip}, error: {str(e)}")
233
  return None, f"❌ Error occurred during processing: {str(e)}"
234
 
235
+ # 不再需要复杂的状态管理函数,已简化为内联函数
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
 
237
  def local_edit_interface(image_dict, prompt, request: gr.Request, progress=gr.Progress()):
238
  """
239
  处理局部编辑请求
240
  """
241
+ try:
242
+ # 提取用户IP
243
+ client_ip = request.client.host
244
+ x_forwarded_for = dict(request.headers).get('x-forwarded-for')
245
+ if x_forwarded_for:
246
+ client_ip = x_forwarded_for
247
+ if client_ip not in IP_Dict:
248
+ IP_Dict[client_ip] = 0
249
+ IP_Dict[client_ip] += 1
250
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
 
252
+ if image_dict is None:
253
+ return None, "Please upload an image and draw the area to edit"
254
+
255
+ # Check if background and layers exist
256
+ if "background" not in image_dict or "layers" not in image_dict:
257
+ return None, "Please draw the area to edit on the image"
258
+
259
+ base_image = image_dict["background"]
260
+ layers = image_dict["layers"]
261
+
262
+ if not layers:
263
+ return None, "Please draw the area to edit on the image"
264
+
265
+ if not prompt or prompt.strip() == "":
266
+ return None, "Please enter editing prompt"
267
+
268
+ # Check prompt length
269
+ if len(prompt.strip()) <= 3:
270
+ return None, "❌ Editing prompt must be more than 3 characters"
271
+ except Exception as e:
272
+ print(f"⚠️ Local edit request preprocessing error: {e}")
273
+ return None, "❌ Request processing error"
274
 
275
  # 检查图片是否包含NSFW内容
276
  nsfw_result = None
 
305
  status_message = ""
306
 
307
  def progress_callback(message):
308
+ try:
309
+ nonlocal status_message
310
+ status_message = message
311
+ # 增加错误处理,防止 progress 更新失败
312
+ if progress is not None:
313
+ progress(0.5, desc=message)
314
+ except Exception as e:
315
+ print(f"⚠️ Local edit progress update failed: {e}")
316
 
317
  try:
318
  print(f"✅ Local editing started - IP: {client_ip}, count: {IP_Dict[client_ip]}, prompt: {prompt.strip()}", flush=True)
 
322
 
323
  if result_url:
324
  print(f"✅ Local editing completed successfully - IP: {client_ip}, result_url: {result_url}", flush=True)
325
+ try:
326
+ if progress is not None:
327
+ progress(1.0, desc="Processing completed")
328
+ except Exception as e:
329
+ print(f"⚠️ Local edit final progress update failed: {e}")
330
  return result_url, "✅ " + message
331
  else:
332
  print(f"❌ Local editing processing failed - IP: {client_ip}, error: {message}", flush=True)
333
  return None, "❌ " + message
334
 
335
  except Exception as e:
336
+ print(f"❌ Local editing exception - IP: {client_ip}, error: {str(e)}")
337
  return None, f"❌ Error occurred during processing: {str(e)}"
338
 
339
  # Create Gradio interface
 
362
  margin-top: 10px;
363
  width: 100%;
364
  }
365
+ """,
366
+ # 改善并发性能的配置
367
+ head="""
368
+ <script>
369
+ // 减少客户端状态更新频率,避免过度的 SSE 连接
370
+ if (window.gradio) {
371
+ window.gradio.update_frequency = 2000; // 2秒更新一次
372
+ }
373
+ </script>
374
  """
375
  ) as app:
376
 
377
+ # 减少State组件,只保留必要的
378
+ # 移除了大部分State组件以减少状态管理复杂度
 
 
 
379
 
380
  gr.Markdown(
381
  """
 
454
  outputs=prompt_input
455
  )
456
 
457
+ # 绑定按钮点击事件 - 简化,移除状态管理
458
  edit_button.click(
459
  fn=edit_image_interface,
460
  inputs=[input_image, prompt_input],
461
  outputs=[output_image, status_output],
462
+ show_progress=True,
463
+ # 增加并发设置
464
+ concurrency_limit=10, # 限制并发数
465
+ api_name="global_edit"
 
466
  )
467
 
468
+ # 简化 "Use as Input" 按钮,直接复制图片
469
+ def simple_use_as_input(output_img):
470
+ if output_img is not None:
471
+ return output_img
472
+ return None
473
+
474
  use_as_input_btn.click(
475
+ fn=simple_use_as_input,
476
  inputs=[output_image],
477
+ outputs=[input_image]
 
 
 
 
 
 
 
478
  )
479
 
480
  # Local editing tab
 
547
  outputs=local_prompt_input
548
  )
549
 
550
+ # 绑定局部编辑按钮点击事件 - 简化,移除状态管理
551
  local_edit_button.click(
552
  fn=local_edit_interface,
553
  inputs=[local_input_image, local_prompt_input],
554
  outputs=[local_output_image, local_status_output],
555
+ show_progress=True,
556
+ # 增加并发设置
557
+ concurrency_limit=8, # 局部编辑更复杂,限制更少的并发
558
+ api_name="local_edit"
 
559
  )
560
 
561
+ # 简化局部编辑 "Use as Input" 按钮
562
+ def simple_local_use_as_input(output_img):
563
+ if output_img is not None:
564
+ # 创建简单的 ImageEditor 格式
565
+ editor_data = {
566
+ "background": output_img,
567
+ "layers": [],
568
+ "composite": output_img
569
+ }
570
+ return editor_data
571
+ return None
572
+
573
  local_use_as_input_btn.click(
574
+ fn=simple_local_use_as_input,
575
  inputs=[local_output_image],
576
+ outputs=[local_input_image]
 
 
 
 
 
 
 
577
  )
578
 
579
  return app
580
 
581
  if __name__ == "__main__":
582
  app = create_app()
583
+ # 改善队列配置以处理高并发和防止 SSE 连接问题
584
+ app.queue(
585
+ default_concurrency_limit=20, # 默认并发限制
586
+ max_size=50, # 队列最大大小
587
+ api_open=False # 关闭 API 访问,减少资源消耗
588
+ )
589
+ app.launch(
590
+ server_name="0.0.0.0",
591
+ show_error=True, # 显示详细错误信息
592
+ quiet=False, # 保持日志输出
593
+ max_threads=40, # 增加线程池大小
594
+ height=800,
595
+ favicon_path=None, # 减少资源加载
596
+ enable_queue=True
597
+ )
util.py CHANGED
@@ -118,12 +118,18 @@ class R2Api:
118
  retry_count = 0
119
  while retry_count < 3:
120
  try:
121
- self.session.put(url, data=image_data, headers=headers, timeout=8)
122
- break
123
- except (requests.exceptions.Timeout, requests.exceptions.RequestException):
 
 
 
 
 
124
  retry_count += 1
125
  if retry_count == 3:
126
- raise Exception('Failed to upload file to R2 after 3 retries!')
 
127
  continue
128
  print("upload_from_memory time is ====>", time.time() - t1)
129
  return f"{self.domain}{cloud_path}"
@@ -284,7 +290,7 @@ def upload_mask_image_r2(client_ip, time_id, mask_image):
284
 
285
  def submit_image_edit_task(user_image_url, prompt, task_type="80", mask_image_url=""):
286
  """
287
- Submit image editing task
288
  """
289
  headers = {
290
  'Content-Type': 'application/json',
@@ -300,28 +306,52 @@ def submit_image_edit_task(user_image_url, prompt, task_type="80", mask_image_ur
300
  "is_private": "0"
301
  }
302
 
303
- try:
304
- response = requests.post(
305
- f'{UKAPIURL}/public_image_edit',
306
- headers=headers,
307
- json=data
308
- )
309
-
310
- if response.status_code == 200:
311
- result = response.json()
312
- if result.get('code') == 0:
313
- return result['data']['task_id'], None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  else:
315
- return None, f"API Error: {result.get('message', 'Unknown error')}"
316
- else:
317
- return None, f"HTTP Error: {response.status_code}"
318
- except Exception as e:
319
- return None, f"Request Exception: {str(e)}"
 
 
 
 
 
 
 
 
 
320
 
321
 
322
  def check_task_status(task_id):
323
  """
324
- Query task status
325
  """
326
  headers = {
327
  'Content-Type': 'application/json',
@@ -332,24 +362,48 @@ def check_task_status(task_id):
332
  "task_id": task_id
333
  }
334
 
335
- try:
336
- response = requests.post(
337
- f'{UKAPIURL}/status_image_edit',
338
- headers=headers,
339
- json=data
340
- )
341
-
342
- if response.status_code == 200:
343
- result = response.json()
344
- if result.get('code') == 0:
345
- task_data = result['data']
346
- return task_data['status'], task_data.get('output1'), task_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  else:
348
- return 'error', None, result.get('message', 'Unknown error')
349
- else:
350
- return 'error', None, f"HTTP Error: {response.status_code}"
351
- except Exception as e:
352
- return 'error', None, f"Request Exception: {str(e)}"
 
 
 
 
 
 
 
 
 
353
 
354
 
355
  def process_image_edit(img_input, prompt, progress_callback=None):
 
118
  retry_count = 0
119
  while retry_count < 3:
120
  try:
121
+ response = self.session.put(url, data=image_data, headers=headers, timeout=15)
122
+ if response.status_code == 200:
123
+ break
124
+ else:
125
+ print(f"⚠️ Upload failed with status code: {response.status_code}")
126
+ retry_count += 1
127
+ except (requests.exceptions.Timeout, requests.exceptions.RequestException) as e:
128
+ print(f"⚠️ Upload retry {retry_count + 1}/3 failed: {e}")
129
  retry_count += 1
130
  if retry_count == 3:
131
+ raise Exception(f'Failed to upload file to R2 after 3 retries! Last error: {str(e)}')
132
+ time.sleep(1) # 等待1秒后重试
133
  continue
134
  print("upload_from_memory time is ====>", time.time() - t1)
135
  return f"{self.domain}{cloud_path}"
 
290
 
291
  def submit_image_edit_task(user_image_url, prompt, task_type="80", mask_image_url=""):
292
  """
293
+ Submit image editing task with improved error handling
294
  """
295
  headers = {
296
  'Content-Type': 'application/json',
 
306
  "is_private": "0"
307
  }
308
 
309
+ retry_count = 0
310
+ max_retries = 3
311
+
312
+ while retry_count < max_retries:
313
+ try:
314
+ response = requests.post(
315
+ f'{UKAPIURL}/public_image_edit',
316
+ headers=headers,
317
+ json=data,
318
+ timeout=30 # 增加超时时间
319
+ )
320
+
321
+ if response.status_code == 200:
322
+ result = response.json()
323
+ if result.get('code') == 0:
324
+ return result['data']['task_id'], None
325
+ else:
326
+ return None, f"API Error: {result.get('message', 'Unknown error')}"
327
+ elif response.status_code in [502, 503, 504]: # 服务器错误,可以重试
328
+ retry_count += 1
329
+ if retry_count < max_retries:
330
+ print(f"⚠️ Server error {response.status_code}, retrying {retry_count}/{max_retries}")
331
+ time.sleep(2) # 等待2秒后重试
332
+ continue
333
+ else:
334
+ return None, f"HTTP Error after {max_retries} retries: {response.status_code}"
335
  else:
336
+ return None, f"HTTP Error: {response.status_code}"
337
+
338
+ except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
339
+ retry_count += 1
340
+ if retry_count < max_retries:
341
+ print(f"⚠️ Network error, retrying {retry_count}/{max_retries}: {e}")
342
+ time.sleep(2)
343
+ continue
344
+ else:
345
+ return None, f"Network error after {max_retries} retries: {str(e)}"
346
+ except Exception as e:
347
+ return None, f"Request Exception: {str(e)}"
348
+
349
+ return None, f"Failed after {max_retries} retries"
350
 
351
 
352
  def check_task_status(task_id):
353
  """
354
+ Query task status with improved error handling
355
  """
356
  headers = {
357
  'Content-Type': 'application/json',
 
362
  "task_id": task_id
363
  }
364
 
365
+ retry_count = 0
366
+ max_retries = 2 # 状态查询重试次数少一些
367
+
368
+ while retry_count < max_retries:
369
+ try:
370
+ response = requests.post(
371
+ f'{UKAPIURL}/status_image_edit',
372
+ headers=headers,
373
+ json=data,
374
+ timeout=15 # 状态查询超时时间短一些
375
+ )
376
+
377
+ if response.status_code == 200:
378
+ result = response.json()
379
+ if result.get('code') == 0:
380
+ task_data = result['data']
381
+ return task_data['status'], task_data.get('output1'), task_data
382
+ else:
383
+ return 'error', None, result.get('message', 'Unknown error')
384
+ elif response.status_code in [502, 503, 504]: # 服务器错误,可以重试
385
+ retry_count += 1
386
+ if retry_count < max_retries:
387
+ print(f"⚠️ Status check server error {response.status_code}, retrying {retry_count}/{max_retries}")
388
+ time.sleep(1) # 状态查询重试间隔短一些
389
+ continue
390
+ else:
391
+ return 'error', None, f"HTTP Error after {max_retries} retries: {response.status_code}"
392
  else:
393
+ return 'error', None, f"HTTP Error: {response.status_code}"
394
+
395
+ except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
396
+ retry_count += 1
397
+ if retry_count < max_retries:
398
+ print(f"⚠️ Status check network error, retrying {retry_count}/{max_retries}: {e}")
399
+ time.sleep(1)
400
+ continue
401
+ else:
402
+ return 'error', None, f"Network error after {max_retries} retries: {str(e)}"
403
+ except Exception as e:
404
+ return 'error', None, f"Request Exception: {str(e)}"
405
+
406
+ return 'error', None, f"Failed after {max_retries} retries"
407
 
408
 
409
  def process_image_edit(img_input, prompt, progress_callback=None):