selfit-camera commited on
Commit
b9247d3
·
1 Parent(s): 57ab548
Files changed (3) hide show
  1. app.py +326 -96
  2. requirements.txt +1 -1
  3. util.py +245 -87
app.py CHANGED
@@ -4,12 +4,12 @@ import os
4
  import shutil
5
  import tempfile
6
  import time
7
- from util import process_image_edit, get_country_info_safe, get_location_info_safe, contains_chinese
8
  from nfsw import NSFWDetector
9
 
10
  # 配置参数
11
  NSFW_TIME_WINDOW = 5 # 时间窗口:5分钟
12
- NSFW_LIMIT = 6 # 限制次数:6次
13
 
14
  IP_Dict = {}
15
  NSFW_Dict = {} # 记录每个IP的NSFW违规次数
@@ -151,32 +151,6 @@ def edit_image_interface(input_image, prompt, request: gr.Request, progress=gr.P
151
 
152
  # 获取IP属地信息
153
  country_info = get_country_info_safe(client_ip)
154
- location_info = get_location_info_safe(client_ip)
155
-
156
- # # 检查IP是否因NSFW违规过多而被屏蔽 3
157
- # if client_ip in NSFW_Dict and NSFW_Dict[client_ip] >= 5:
158
- # print(f"❌ IP blocked due to excessive NSFW violations - IP: {client_ip}({country_info}), violations: {NSFW_Dict[client_ip]}")
159
-
160
- # # 检查是否需要特殊处理(中文prompt或中国IP)
161
- # has_chinese = contains_chinese(prompt.strip()) if prompt else False
162
- # is_china_ip = location_info.get("country", "").lower() == "中国"
163
-
164
- # if has_chinese or is_china_ip:
165
- # # 构建特殊警告信息
166
- # prov = location_info.get("prov", "未知省份")
167
- # city = location_info.get("city", "未知城市")
168
- # isp = location_info.get("isp", "Unknown")
169
-
170
- # # 处理城市名称
171
- # city_name = city.replace("市", "") if city != "未知城市" else "当地"
172
-
173
- # # 处理ISP名称,去掉"中国"前缀
174
- # isp_name = isp.replace("中国", "") if isp != "Unknown" else "未知"
175
-
176
- # warning_msg = f"您的ip地址为{client_ip},地点为{prov}-{city},您的网络服务商为{isp_name},检测到您目前正在浏览,制作淫秽内容,已经上报给 {city_name}市公安局,已加急处理,请耐心等待警察上门调查"
177
- # return None, f"❌ {warning_msg}"
178
- # else:
179
- # return None, f"❌ Your ip {client_ip}, your region has been blocked for too much nsfw content"
180
 
181
  if input_image is None:
182
  return None, "Please upload an image first"
@@ -195,7 +169,6 @@ def edit_image_interface(input_image, prompt, request: gr.Request, progress=gr.P
195
  # 直接使用PIL Image对象进行检测,避免文件路径问题
196
  nsfw_result = nsfw_detector.predict_pil_label_only(input_image)
197
 
198
-
199
  if nsfw_result.lower() == "nsfw":
200
  print(f"🔍 NSFW检测结果: ❌❌❌ {nsfw_result} - IP: {client_ip}({country_info})")
201
  # 检查NSFW频率限制
@@ -213,10 +186,6 @@ def edit_image_interface(input_image, prompt, request: gr.Request, progress=gr.P
213
  # 未超过频率限制,记录此次检测但允许继续处理
214
  record_nsfw_detection(client_ip)
215
  window_info = get_current_window_info(client_ip)
216
- # print(f"🔍 NSFW检测记录 - IP: {client_ip}({country_info})")
217
- # print(f" 时间窗口: {window_info['window_start']} - {window_info['window_end']}")
218
- # print(f" 当前计数: {window_info['current_count']}/{NSFW_LIMIT}, 允许继续处理")
219
- # 不return,允许继续处理图片编辑
220
  else:
221
  print(f"🔍 NSFW检测结果: ✅✅✅ {nsfw_result} - IP: {client_ip}({country_info})")
222
 
@@ -227,13 +196,6 @@ def edit_image_interface(input_image, prompt, request: gr.Request, progress=gr.P
227
  if IP_Dict[client_ip]>10 and country_info.lower() in ["印度", "巴基斯坦"]:
228
  print(f"❌ Content not allowed - IP: {client_ip}({country_info}), count: {IP_Dict[client_ip]}, prompt: {prompt.strip()}")
229
  return None, "❌ Content not allowed. Please modify your prompt"
230
- # if IP_Dict[client_ip]>18 and country_info.lower() in ["中国"]:
231
- # print(f"❌ Content not allowed - IP: {client_ip}({country_info}), count: {IP_Dict[client_ip]}, prompt: {prompt.strip()}")
232
- # return None, "❌ Content not allowed. Please modify your prompt"
233
- # if client_ip.lower() in ["221.194.171.230", "101.126.56.37", "101.126.56.44"]:
234
- # print(f"❌ Content not allowed - IP: {client_ip}({country_info}), count: {IP_Dict[client_ip]}, prompt: {prompt.strip()}")
235
- # return None, "❌ Content not allowed. Please modify your prompt"
236
-
237
 
238
  result_url = None
239
  status_message = ""
@@ -261,6 +223,139 @@ def edit_image_interface(input_image, prompt, request: gr.Request, progress=gr.P
261
  except Exception as e:
262
  return None, f"❌ Error occurred during processing: {str(e)}"
263
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  # Create Gradio interface
265
  def create_app():
266
  with gr.Blocks(
@@ -283,9 +378,19 @@ def create_app():
283
  border-radius: 10px;
284
  background-color: #f8f9fa;
285
  }
 
 
 
 
286
  """
287
  ) as app:
288
 
 
 
 
 
 
 
289
  gr.Markdown(
290
  """
291
  # 🎨 AI Image Editor
@@ -293,72 +398,197 @@ def create_app():
293
  elem_classes=["main-container"]
294
  )
295
 
296
- with gr.Row():
297
- with gr.Column(scale=1):
298
- gr.Markdown("### 📸 Upload Image")
299
- input_image = gr.Image(
300
- label="Select image to edit",
301
- type="pil",
302
- height=400,
303
- elem_classes=["upload-area"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  )
305
 
306
- gr.Markdown("### ✍️ Editing Instructions")
307
- prompt_input = gr.Textbox(
308
- label="Enter editing prompt",
309
- placeholder="For example: change background to beach, add rainbow, remove background, etc...",
310
- lines=3,
311
- max_lines=5
312
  )
313
 
314
- edit_button = gr.Button(
315
- "🚀 Start Editing",
316
- variant="primary",
317
- size="lg"
 
318
  )
319
 
320
- with gr.Column(scale=1):
321
- gr.Markdown("### 🎯 Editing Result")
322
- output_image = gr.Image(
323
- label="Edited image",
324
- height=400,
325
- elem_classes=["result-area"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  )
327
 
328
- status_output = gr.Textbox(
329
- label="Processing status",
330
- lines=2,
331
- max_lines=3,
332
- interactive=False
333
  )
334
-
335
- # Example area
336
- gr.Markdown("### 💡 Prompt Examples")
337
- with gr.Row():
338
- example_prompts = [
339
- "Change the character's background to a sunny seaside with blue waves.",
340
- "Change the character's background to New York at night with neon lights.",
341
- "Change the character's background to a fairytale castle with bright colors.",
342
- "Change background to forest",
343
- "Change background to snow mountain"
344
- ]
345
-
346
- for prompt in example_prompts:
347
- gr.Button(
348
- prompt,
349
- size="sm"
350
- ).click(
351
- lambda p=prompt: p,
352
- outputs=prompt_input
353
  )
354
-
355
- # Bind button click event
356
- edit_button.click(
357
- fn=edit_image_interface,
358
- inputs=[input_image, prompt_input],
359
- outputs=[output_image, status_output],
360
- show_progress=True
361
- )
362
 
363
  return app
364
 
 
4
  import shutil
5
  import tempfile
6
  import time
7
+ from util import process_image_edit, process_local_image_edit, get_country_info_safe
8
  from nfsw import NSFWDetector
9
 
10
  # 配置参数
11
  NSFW_TIME_WINDOW = 5 # 时间窗口:5分钟
12
+ NSFW_LIMIT = 10 # 限制次数:6次
13
 
14
  IP_Dict = {}
15
  NSFW_Dict = {} # 记录每个IP的NSFW违规次数
 
151
 
152
  # 获取IP属地信息
153
  country_info = get_country_info_safe(client_ip)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
  if input_image is None:
156
  return None, "Please upload an image first"
 
169
  # 直接使用PIL Image对象进行检测,避免文件路径问题
170
  nsfw_result = nsfw_detector.predict_pil_label_only(input_image)
171
 
 
172
  if nsfw_result.lower() == "nsfw":
173
  print(f"🔍 NSFW检测结果: ❌❌❌ {nsfw_result} - IP: {client_ip}({country_info})")
174
  # 检查NSFW频率限制
 
186
  # 未超过频率限制,记录此次检测但允许继续处理
187
  record_nsfw_detection(client_ip)
188
  window_info = get_current_window_info(client_ip)
 
 
 
 
189
  else:
190
  print(f"🔍 NSFW检测结果: ✅✅✅ {nsfw_result} - IP: {client_ip}({country_info})")
191
 
 
196
  if IP_Dict[client_ip]>10 and country_info.lower() in ["印度", "巴基斯坦"]:
197
  print(f"❌ Content not allowed - IP: {client_ip}({country_info}), count: {IP_Dict[client_ip]}, prompt: {prompt.strip()}")
198
  return None, "❌ Content not allowed. Please modify your prompt"
 
 
 
 
 
 
 
199
 
200
  result_url = None
201
  status_message = ""
 
223
  except Exception as e:
224
  return None, f"❌ Error occurred during processing: {str(e)}"
225
 
226
+ # 局部编辑处理函数
227
+ # 状态更新函数
228
+ def update_global_input_state(image):
229
+ """更新全局编辑输入图片状态"""
230
+ return image
231
+
232
+ def update_global_output_state(image):
233
+ """更新全局编辑输出图片状态"""
234
+ return image
235
+
236
+ def update_local_input_state(image):
237
+ """更新局部编辑输入图片状态"""
238
+ return image
239
+
240
+ def update_local_output_state(image):
241
+ """更新局部编辑输出图片状态"""
242
+ return image
243
+
244
+ def use_global_output_as_input(output_image):
245
+ """将全局编辑的输出图片设为输入图片"""
246
+ if output_image is not None:
247
+ return output_image, output_image
248
+ return None, None
249
+
250
+ def use_local_output_as_input(output_image):
251
+ """将局部编辑的输出图片设为输入图片"""
252
+ if output_image is not None:
253
+ # 对于局部编辑,我们需要创建一个新的ImageEditor格式
254
+ # 将输出图片作为背景,不包含任何编辑层
255
+ return {"background": output_image, "layers": []}, 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
+ # 获取IP属地信息
280
+ country_info = get_country_info_safe(client_ip)
281
+
282
+ if image_dict is None:
283
+ return None, "Please upload an image and draw the area to edit"
284
+
285
+ # Check if background and layers exist
286
+ if "background" not in image_dict or "layers" not in image_dict:
287
+ return None, "Please draw the area to edit on the image"
288
+
289
+ base_image = image_dict["background"]
290
+ layers = image_dict["layers"]
291
+
292
+ if not layers:
293
+ return None, "Please draw the area to edit on the image"
294
+
295
+ if not prompt or prompt.strip() == "":
296
+ return None, "Please enter editing prompt"
297
+
298
+ # Check prompt length
299
+ if len(prompt.strip()) <= 3:
300
+ return None, "❌ Editing prompt must be more than 3 characters"
301
+
302
+ # 检查图片是否包含NSFW内容
303
+ nsfw_result = None
304
+ if nsfw_detector is not None and base_image is not None:
305
+ try:
306
+ nsfw_result = nsfw_detector.predict_pil_label_only(base_image)
307
+
308
+ if nsfw_result.lower() == "nsfw":
309
+ print(f"🔍 NSFW检测结果: ❌❌❌ {nsfw_result} - IP: {client_ip}({country_info})")
310
+ # 检查NSFW频率限制
311
+ is_rate_limited, wait_time = check_nsfw_rate_limit(client_ip)
312
+
313
+ if is_rate_limited:
314
+ wait_minutes = int(wait_time / 60) + 1
315
+ window_info = get_current_window_info(client_ip)
316
+ print(f"⚠️ NSFW频率限制 - IP: {client_ip}({country_info})")
317
+ print(f" 时间窗口: {window_info['window_start']} - {window_info['window_end']}")
318
+ print(f" 当前计数: {window_info['current_count']}/{NSFW_LIMIT}, 需要等待 {wait_minutes} 分钟")
319
+ return None, f"❌ Please wait {wait_minutes} minutes before generating again"
320
+ else:
321
+ record_nsfw_detection(client_ip)
322
+ window_info = get_current_window_info(client_ip)
323
+ else:
324
+ print(f"🔍 NSFW检测结果: ✅✅✅ {nsfw_result} - IP: {client_ip}({country_info})")
325
+
326
+ except Exception as e:
327
+ print(f"⚠️ NSFW检测失败: {e}")
328
+
329
+ # IP访问限制检查
330
+ if IP_Dict[client_ip]>10 and country_info.lower() in ["印度", "巴基斯坦"]:
331
+ print(f"❌ Content not allowed - IP: {client_ip}({country_info}), count: {IP_Dict[client_ip]}, prompt: {prompt.strip()}")
332
+ return None, "❌ Content not allowed. Please modify your prompt"
333
+
334
+ result_url = None
335
+ status_message = ""
336
+
337
+ def progress_callback(message):
338
+ nonlocal status_message
339
+ status_message = message
340
+ progress(0.5, desc=message)
341
+
342
+ try:
343
+ print(f"✅ Local editing started - IP: {client_ip}({country_info}), count: {IP_Dict[client_ip]}, prompt: {prompt.strip()}", flush=True)
344
+
345
+ # 调用局部图像编辑处理函数
346
+ result_url, message = process_local_image_edit(base_image, layers, prompt.strip(), progress_callback)
347
+
348
+ if result_url:
349
+ print(f"✅ Local editing completed successfully - IP: {client_ip}({country_info}), result_url: {result_url}", flush=True)
350
+ progress(1.0, desc="Processing completed")
351
+ return result_url, "✅ " + message
352
+ else:
353
+ print(f"❌ Local editing processing failed - IP: {client_ip}({country_info}), error: {message}", flush=True)
354
+ return None, "❌ " + message
355
+
356
+ except Exception as e:
357
+ return None, f"❌ Error occurred during processing: {str(e)}"
358
+
359
  # Create Gradio interface
360
  def create_app():
361
  with gr.Blocks(
 
378
  border-radius: 10px;
379
  background-color: #f8f9fa;
380
  }
381
+ .use-as-input-btn {
382
+ margin-top: 10px;
383
+ width: 100%;
384
+ }
385
  """
386
  ) as app:
387
 
388
+ # 使用State组件保持图片状态,防止切换tab时丢失
389
+ global_input_state = gr.State()
390
+ global_output_state = gr.State()
391
+ local_input_state = gr.State()
392
+ local_output_state = gr.State()
393
+
394
  gr.Markdown(
395
  """
396
  # 🎨 AI Image Editor
 
398
  elem_classes=["main-container"]
399
  )
400
 
401
+ with gr.Tabs():
402
+ # Global editing tab
403
+ with gr.Tab("🌍 Global Editing"):
404
+ with gr.Row():
405
+ with gr.Column(scale=1):
406
+ gr.Markdown("### 📸 Upload Image")
407
+ input_image = gr.Image(
408
+ label="Select image to edit",
409
+ type="pil",
410
+ height=512,
411
+ elem_classes=["upload-area"]
412
+ )
413
+
414
+ gr.Markdown("### ✍️ Editing Instructions")
415
+ prompt_input = gr.Textbox(
416
+ label="Enter editing prompt",
417
+ placeholder="For example: change background to beach, add rainbow, remove background, etc...",
418
+ lines=3,
419
+ max_lines=5
420
+ )
421
+
422
+ edit_button = gr.Button(
423
+ "🚀 Start Editing",
424
+ variant="primary",
425
+ size="lg"
426
+ )
427
+
428
+ with gr.Column(scale=1):
429
+ gr.Markdown("### 🎯 Editing Result")
430
+ output_image = gr.Image(
431
+ label="Edited image",
432
+ height=320,
433
+ elem_classes=["result-area"]
434
+ )
435
+
436
+ # 添加 "Use as Input" 按钮
437
+ use_as_input_btn = gr.Button(
438
+ "🔄 Use as Input",
439
+ variant="secondary",
440
+ size="sm",
441
+ elem_classes=["use-as-input-btn"]
442
+ )
443
+
444
+ status_output = gr.Textbox(
445
+ label="Processing status",
446
+ lines=2,
447
+ max_lines=3,
448
+ interactive=False
449
+ )
450
+
451
+ # Example area
452
+ gr.Markdown("### 💡 Prompt Examples")
453
+ with gr.Row():
454
+ example_prompts = [
455
+ "Change the character's background to a sunny seaside with blue waves",
456
+ "Change the character's background to New York at night with neon lights",
457
+ "Change the character's background to a fairytale castle with bright colors",
458
+ "Change background to forest",
459
+ "Change background to snow mountain"
460
+ ]
461
+
462
+ for prompt in example_prompts:
463
+ gr.Button(
464
+ prompt,
465
+ size="sm"
466
+ ).click(
467
+ lambda p=prompt: p,
468
+ outputs=prompt_input
469
+ )
470
+
471
+ # 绑定按钮点击事件
472
+ edit_button.click(
473
+ fn=edit_image_interface,
474
+ inputs=[input_image, prompt_input],
475
+ outputs=[output_image, status_output],
476
+ show_progress=True
477
+ ).then(
478
+ fn=update_global_output_state,
479
+ inputs=[output_image],
480
+ outputs=[global_output_state]
481
  )
482
 
483
+ # 绑定 "Use as Input" 按钮
484
+ use_as_input_btn.click(
485
+ fn=use_global_output_as_input,
486
+ inputs=[output_image],
487
+ outputs=[input_image, global_input_state]
 
488
  )
489
 
490
+ # 绑定输入图片变化事件以更新状态
491
+ input_image.change(
492
+ fn=update_global_input_state,
493
+ inputs=[input_image],
494
+ outputs=[global_input_state]
495
  )
496
 
497
+ # Local editing tab
498
+ with gr.Tab("🖌️ Local Editing"):
499
+ with gr.Row():
500
+ with gr.Column(scale=1):
501
+ gr.Markdown("### 📸 Upload Image and Draw Edit Area")
502
+ local_input_image = gr.ImageEditor(
503
+ label="Upload image and draw mask",
504
+ type="pil",
505
+ height=512,
506
+ brush=gr.Brush(colors=["#ff0000"], default_size=60),
507
+ elem_classes=["upload-area"]
508
+ )
509
+
510
+ gr.Markdown("### ✍️ Editing Instructions")
511
+ local_prompt_input = gr.Textbox(
512
+ label="Enter local editing prompt",
513
+ placeholder="For example: change selected area hair to golden, add patterns to selected object, change selected area color, etc...",
514
+ lines=3,
515
+ max_lines=5
516
+ )
517
+
518
+ local_edit_button = gr.Button(
519
+ "🎯 Start Local Editing",
520
+ variant="primary",
521
+ size="lg"
522
+ )
523
+
524
+ with gr.Column(scale=1):
525
+ gr.Markdown("### 🎯 Editing Result")
526
+ local_output_image = gr.Image(
527
+ label="Local edited image",
528
+ height=320,
529
+ elem_classes=["result-area"]
530
+ )
531
+
532
+ # 添加 "Use as Input" 按钮
533
+ local_use_as_input_btn = gr.Button(
534
+ "🔄 Use as Input",
535
+ variant="secondary",
536
+ size="sm",
537
+ elem_classes=["use-as-input-btn"]
538
+ )
539
+
540
+ local_status_output = gr.Textbox(
541
+ label="Processing status",
542
+ lines=2,
543
+ max_lines=3,
544
+ interactive=False
545
+ )
546
+
547
+ # Local editing examples
548
+ gr.Markdown("### 💡 Local Editing Prompt Examples")
549
+ with gr.Row():
550
+ local_example_prompts = [
551
+ "Change selected area hair to golden",
552
+ "Add pattern designs to selected clothing",
553
+ "Change selected area to different material",
554
+ "Add decorations to selected object",
555
+ "Change selected area color and style"
556
+ ]
557
+
558
+ for prompt in local_example_prompts:
559
+ gr.Button(
560
+ prompt,
561
+ size="sm"
562
+ ).click(
563
+ lambda p=prompt: p,
564
+ outputs=local_prompt_input
565
+ )
566
+
567
+ # 绑定局部编辑按钮点击事件
568
+ local_edit_button.click(
569
+ fn=local_edit_interface,
570
+ inputs=[local_input_image, local_prompt_input],
571
+ outputs=[local_output_image, local_status_output],
572
+ show_progress=True
573
+ ).then(
574
+ fn=update_local_output_state,
575
+ inputs=[local_output_image],
576
+ outputs=[local_output_state]
577
  )
578
 
579
+ # 绑定局部编辑 "Use as Input" 按钮
580
+ local_use_as_input_btn.click(
581
+ fn=use_local_output_as_input,
582
+ inputs=[local_output_image],
583
+ outputs=[local_input_image, local_input_state]
584
  )
585
+
586
+ # 绑定局部编辑输入图片变化事件以更新状态
587
+ local_input_image.change(
588
+ fn=update_local_input_state,
589
+ inputs=[local_input_image],
590
+ outputs=[local_input_state]
 
 
 
 
 
 
 
 
 
 
 
 
 
591
  )
 
 
 
 
 
 
 
 
592
 
593
  return app
594
 
requirements.txt CHANGED
@@ -1,4 +1,4 @@
1
- gradio>=4.0.0
2
  opencv-python>=4.8.0
3
  requests>=2.28.0
4
  func-timeout>=4.3.5
 
1
+ gradio==5.42.0
2
  opencv-python>=4.8.0
3
  requests>=2.28.0
4
  func-timeout>=4.3.5
util.py CHANGED
@@ -142,103 +142,138 @@ def upload_user_img_r2(clientIp, timeId, img):
142
 
143
  @func_timeout.func_set_timeout(10)
144
  def get_country_info(ip):
145
- """获取IP对应的国家信息"""
146
  try:
147
- # 使用您指定的新接口 URL
148
  url = f"https://qifu-api.baidubce.com/ip/geo/v1/district?ip={ip}"
149
  ret = requests.get(url)
150
- ret.raise_for_status() # 如果请求失败 (例如 404, 500), 会抛出异常
151
 
152
  json_data = ret.json()
153
 
154
- # 根据新的JSON结构,国家信息在 'data' -> 'country' 路径下
155
  if json_data.get("code") == "Success":
156
  country = json_data.get("data", {}).get("country")
157
  return country if country else "Unknown"
158
  else:
159
- # 处理API返回错误码的情况
160
- print(f"API请求失败: {json_data.get('msg', '未知错误')}")
161
  return "Unknown"
162
 
163
  except requests.exceptions.RequestException as e:
164
- print(f"网络请求失败: {e}")
165
  return "Unknown"
166
  except Exception as e:
167
- print(f"获取IP属地失败: {e}")
168
  return "Unknown"
169
 
170
- @func_timeout.func_set_timeout(10)
171
- def get_location_info(ip):
172
- """获取IP对应的详细位置信息"""
173
- try:
174
- # 使用您指定的新接口 URL
175
- url = f"https://qifu-api.baidubce.com/ip/geo/v1/district?ip={ip}"
176
- ret = requests.get(url)
177
- ret.raise_for_status()
178
-
179
- json_data = ret.json()
180
-
181
- if json_data.get("code") == "Success":
182
- data = json_data.get("data", {})
183
- return {
184
- "country": data.get("country", "Unknown"),
185
- "prov": data.get("prov", "Unknown"),
186
- "city": data.get("city", "Unknown"),
187
- "isp": data.get("isp", "Unknown"),
188
- "owner": data.get("owner", "Unknown"),
189
- "full_data": data
190
- }
191
- else:
192
- print(f"API请求失败: {json_data.get('msg', '未知错误')}")
193
- return {
194
- "country": "Unknown",
195
- "prov": "Unknown",
196
- "city": "Unknown",
197
- "isp": "Unknown",
198
- "owner": "Unknown",
199
- "full_data": {}
200
- }
201
-
202
- except requests.exceptions.RequestException as e:
203
- print(f"网络请求失败: {e}")
204
- return {"country": "Unknown", "prov": "Unknown", "city": "Unknown", "isp": "Unknown", "owner": "Unknown", "full_data": {}}
205
- except Exception as e:
206
- print(f"获取IP属地失败: {e}")
207
- return {"country": "Unknown", "prov": "Unknown", "city": "Unknown", "isp": "Unknown", "owner": "Unknown", "full_data": {}}
208
-
209
  def get_country_info_safe(ip):
210
- """安全获取IP属地信息,出错时返回Unknown"""
211
  try:
212
  return get_country_info(ip)
213
  except func_timeout.FunctionTimedOut:
214
- print(f"获取IP属地超时: {ip}")
215
  return "Unknown"
216
  except Exception as e:
217
- print(f"获取IP属地失败: {e}")
218
  return "Unknown"
219
 
220
- def get_location_info_safe(ip):
221
- """安全获取IP详细位置信息,出错时返回默认值"""
222
- try:
223
- return get_location_info(ip)
224
- except func_timeout.FunctionTimedOut:
225
- print(f"获取IP位置超时: {ip}")
226
- return {"country": "Unknown", "prov": "Unknown", "city": "Unknown", "isp": "Unknown", "owner": "Unknown", "full_data": {}}
227
- except Exception as e:
228
- print(f"获取IP位置失败: {e}")
229
- return {"country": "Unknown", "prov": "Unknown", "city": "Unknown", "isp": "Unknown", "owner": "Unknown", "full_data": {}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
 
231
- def contains_chinese(text):
232
- """检测文本是否包含中文字符"""
233
- import re
234
- return bool(re.search(r'[\u4e00-\u9fff]', text))
235
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
 
237
 
238
 
239
- def submit_image_edit_task(user_image_url, prompt):
240
  """
241
- 提交图片编辑任务
242
  """
243
  headers = {
244
  'Content-Type': 'application/json',
@@ -247,7 +282,8 @@ def submit_image_edit_task(user_image_url, prompt):
247
 
248
  data = {
249
  "user_image": user_image_url,
250
- "task_type": "80",
 
251
  "prompt": prompt,
252
  "secret_key": "219ngu",
253
  "is_private": "0"
@@ -274,7 +310,7 @@ def submit_image_edit_task(user_image_url, prompt):
274
 
275
  def check_task_status(task_id):
276
  """
277
- 查询任务状态
278
  """
279
  headers = {
280
  'Content-Type': 'application/json',
@@ -307,52 +343,52 @@ def check_task_status(task_id):
307
 
308
  def process_image_edit(img_input, prompt, progress_callback=None):
309
  """
310
- 处理图片编辑的完整流程
311
 
312
  Args:
313
- img_input: 可以是文件路径(str)PIL Image对象
314
- prompt: 编辑指令
315
- progress_callback: 进度回调函数
316
  """
317
  temp_img_path = None
318
  try:
319
- # 生成客户端 IP 和时间戳
320
- client_ip = "127.0.0.1" # 默认IP
321
  time_id = int(time.time())
322
 
323
- # 处理输入图像 - 支持PIL Image和文件路径
324
- if hasattr(img_input, 'save'): # PIL Image对象
325
- # 创建临时文件
326
  temp_dir = tempfile.mkdtemp()
327
  temp_img_path = os.path.join(temp_dir, f"temp_img_{time_id}.jpg")
328
 
329
- # 保存PIL Image为临时文件
330
  if img_input.mode != 'RGB':
331
  img_input = img_input.convert('RGB')
332
  img_input.save(temp_img_path, 'JPEG', quality=95)
333
 
334
  img_path = temp_img_path
335
- print(f"💾 PIL Image已保存为临时文件: {temp_img_path}")
336
  else:
337
- # 假设是文件路径
338
  img_path = img_input
339
 
340
  if progress_callback:
341
  progress_callback("uploading image...")
342
 
343
- # 上传用户图片
344
  uploaded_url = upload_user_img_r2(client_ip, time_id, img_path)
345
  if not uploaded_url:
346
  return None, "image upload failed"
347
 
348
- # 从上传 URL 中提取实际的图片 URL
349
  if "?" in uploaded_url:
350
  uploaded_url = uploaded_url.split("?")[0]
351
 
352
  if progress_callback:
353
  progress_callback("submitting edit task...")
354
 
355
- # 提交图片编辑任务
356
  task_id, error = submit_image_edit_task(uploaded_url, prompt)
357
  if error:
358
  return None, error
@@ -360,8 +396,8 @@ def process_image_edit(img_input, prompt, progress_callback=None):
360
  if progress_callback:
361
  progress_callback(f"task submitted, ID: {task_id}, processing...")
362
 
363
- # 等待任务完成
364
- max_attempts = 60 # 最多等待10分钟
365
  for attempt in range(max_attempts):
366
  status, output_url, task_data = check_task_status(task_id)
367
 
@@ -395,9 +431,131 @@ def process_image_edit(img_input, prompt, progress_callback=None):
395
  temp_dir = os.path.dirname(temp_img_path)
396
  if os.path.exists(temp_dir):
397
  os.rmdir(temp_dir)
398
- print(f"🗑️ 已清理临时文件: {temp_img_path}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
  except Exception as cleanup_error:
400
- print(f"⚠️ 清理临时文件失败: {cleanup_error}")
401
 
402
 
403
  if __name__ == "__main__":
 
142
 
143
  @func_timeout.func_set_timeout(10)
144
  def get_country_info(ip):
145
+ """Get country information for IP address"""
146
  try:
147
+ # Use the new API URL
148
  url = f"https://qifu-api.baidubce.com/ip/geo/v1/district?ip={ip}"
149
  ret = requests.get(url)
150
+ ret.raise_for_status() # Raises exception if request fails (e.g. 404, 500)
151
 
152
  json_data = ret.json()
153
 
154
+ # Based on new JSON structure, country info is under 'data' -> 'country' path
155
  if json_data.get("code") == "Success":
156
  country = json_data.get("data", {}).get("country")
157
  return country if country else "Unknown"
158
  else:
159
+ # Handle API error codes
160
+ print(f"API request failed: {json_data.get('msg', 'Unknown error')}")
161
  return "Unknown"
162
 
163
  except requests.exceptions.RequestException as e:
164
+ print(f"Network request failed: {e}")
165
  return "Unknown"
166
  except Exception as e:
167
+ print(f"Failed to get IP location: {e}")
168
  return "Unknown"
169
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  def get_country_info_safe(ip):
171
+ """Safely get IP location info, returns Unknown on error"""
172
  try:
173
  return get_country_info(ip)
174
  except func_timeout.FunctionTimedOut:
175
+ print(f"IP location request timeout: {ip}")
176
  return "Unknown"
177
  except Exception as e:
178
+ print(f"Failed to get IP location: {e}")
179
  return "Unknown"
180
 
181
+ def create_mask_from_layers(base_image, layers):
182
+ """
183
+ Create mask image from ImageEditor layers
184
+
185
+ Args:
186
+ base_image (PIL.Image): Original image
187
+ layers (list): ImageEditor layer data
188
+
189
+ Returns:
190
+ PIL.Image: Black and white mask image
191
+ """
192
+ from PIL import Image, ImageDraw
193
+ import numpy as np
194
+
195
+ # Create blank mask with same size as original image
196
+ mask = Image.new('L', base_image.size, 0) # 'L' mode is grayscale, 0 is black
197
+
198
+ if not layers:
199
+ return mask
200
+
201
+ # Iterate through all layers, set drawn areas to white
202
+ for layer in layers:
203
+ if layer is not None:
204
+ # Convert layer to numpy array
205
+ layer_array = np.array(layer)
206
+
207
+ # Check layer format
208
+ if len(layer_array.shape) == 3: # RGB/RGBA format
209
+ # If RGBA, check alpha channel
210
+ if layer_array.shape[2] == 4:
211
+ # Use alpha channel as mask
212
+ alpha_channel = layer_array[:, :, 3]
213
+ # Set non-transparent areas (alpha > 0) to white
214
+ mask_array = np.where(alpha_channel > 0, 255, 0).astype(np.uint8)
215
+ else:
216
+ # RGB format, check if not pure black (0,0,0)
217
+ # Assume drawn areas are non-black
218
+ non_black = np.any(layer_array > 0, axis=2)
219
+ mask_array = np.where(non_black, 255, 0).astype(np.uint8)
220
+ elif len(layer_array.shape) == 2: # Grayscale
221
+ # Use grayscale values directly, set non-zero areas to white
222
+ mask_array = np.where(layer_array > 0, 255, 0).astype(np.uint8)
223
+ else:
224
+ continue
225
+
226
+ # Convert mask_array to PIL image and merge into total mask
227
+ layer_mask = Image.fromarray(mask_array, mode='L')
228
+ # Resize to match original image
229
+ if layer_mask.size != base_image.size:
230
+ layer_mask = layer_mask.resize(base_image.size, Image.LANCZOS)
231
+
232
+ # Merge masks (use maximum value to ensure all drawn areas are included)
233
+ mask_array_current = np.array(mask)
234
+ layer_mask_array = np.array(layer_mask)
235
+ combined_mask_array = np.maximum(mask_array_current, layer_mask_array)
236
+ mask = Image.fromarray(combined_mask_array, mode='L')
237
+
238
+ return mask
239
 
 
 
 
 
240
 
241
+ def upload_mask_image_r2(client_ip, time_id, mask_image):
242
+ """
243
+ Upload mask image to R2
244
+
245
+ Args:
246
+ client_ip (str): Client IP
247
+ time_id (int): Timestamp
248
+ mask_image (PIL.Image): Mask image
249
+
250
+ Returns:
251
+ str: Uploaded URL
252
+ """
253
+ file_name = f"{client_ip.replace('.', '')}{time_id}_mask.png"
254
+ local_path = os.path.join(tmpFolder, file_name)
255
+
256
+ try:
257
+ # Save mask image as PNG format (supports transparency)
258
+ mask_image.save(local_path, 'PNG')
259
+
260
+ # Upload to R2
261
+ res = R2Api().upload_file(local_path, file_name)
262
+
263
+ return res
264
+ except Exception as e:
265
+ print(f"Failed to upload mask image: {e}")
266
+ return None
267
+ finally:
268
+ # Clean up local files
269
+ if os.path.exists(local_path):
270
+ os.remove(local_path)
271
 
272
 
273
 
274
+ def submit_image_edit_task(user_image_url, prompt, task_type="80", mask_image_url=""):
275
  """
276
+ Submit image editing task
277
  """
278
  headers = {
279
  'Content-Type': 'application/json',
 
282
 
283
  data = {
284
  "user_image": user_image_url,
285
+ "mask_image": mask_image_url,
286
+ "task_type": task_type,
287
  "prompt": prompt,
288
  "secret_key": "219ngu",
289
  "is_private": "0"
 
310
 
311
  def check_task_status(task_id):
312
  """
313
+ Query task status
314
  """
315
  headers = {
316
  'Content-Type': 'application/json',
 
343
 
344
  def process_image_edit(img_input, prompt, progress_callback=None):
345
  """
346
+ Complete process for image editing
347
 
348
  Args:
349
+ img_input: Can be file path (str) or PIL Image object
350
+ prompt: Editing instructions
351
+ progress_callback: Progress callback function
352
  """
353
  temp_img_path = None
354
  try:
355
+ # Generate client IP and timestamp
356
+ client_ip = "127.0.0.1" # Default IP
357
  time_id = int(time.time())
358
 
359
+ # Process input image - supports PIL Image and file path
360
+ if hasattr(img_input, 'save'): # PIL Image object
361
+ # Create temporary file
362
  temp_dir = tempfile.mkdtemp()
363
  temp_img_path = os.path.join(temp_dir, f"temp_img_{time_id}.jpg")
364
 
365
+ # Save PIL Image as temporary file
366
  if img_input.mode != 'RGB':
367
  img_input = img_input.convert('RGB')
368
  img_input.save(temp_img_path, 'JPEG', quality=95)
369
 
370
  img_path = temp_img_path
371
+ print(f"💾 PIL Image saved as temporary file: {temp_img_path}")
372
  else:
373
+ # Assume it's a file path
374
  img_path = img_input
375
 
376
  if progress_callback:
377
  progress_callback("uploading image...")
378
 
379
+ # Upload user image
380
  uploaded_url = upload_user_img_r2(client_ip, time_id, img_path)
381
  if not uploaded_url:
382
  return None, "image upload failed"
383
 
384
+ # Extract actual image URL from upload URL
385
  if "?" in uploaded_url:
386
  uploaded_url = uploaded_url.split("?")[0]
387
 
388
  if progress_callback:
389
  progress_callback("submitting edit task...")
390
 
391
+ # Submit image editing task
392
  task_id, error = submit_image_edit_task(uploaded_url, prompt)
393
  if error:
394
  return None, error
 
396
  if progress_callback:
397
  progress_callback(f"task submitted, ID: {task_id}, processing...")
398
 
399
+ # Wait for task completion
400
+ max_attempts = 60 # Wait up to 10 minutes
401
  for attempt in range(max_attempts):
402
  status, output_url, task_data = check_task_status(task_id)
403
 
 
431
  temp_dir = os.path.dirname(temp_img_path)
432
  if os.path.exists(temp_dir):
433
  os.rmdir(temp_dir)
434
+ print(f"🗑️ Cleaned up temporary file: {temp_img_path}")
435
+ except Exception as cleanup_error:
436
+ print(f"⚠️ Failed to clean up temporary file: {cleanup_error}")
437
+
438
+
439
+ def process_local_image_edit(base_image, layers, prompt, progress_callback=None):
440
+ """
441
+ 处理局部图片编辑的完整流程
442
+
443
+ Args:
444
+ base_image (PIL.Image): 原始图片
445
+ layers (list): ImageEditor的层数据
446
+ prompt (str): 编辑指令
447
+ progress_callback: 进度回调函数
448
+ """
449
+ temp_img_path = None
450
+ temp_mask_path = None
451
+
452
+ try:
453
+ # Generate client IP and timestamp
454
+ client_ip = "127.0.0.1" # Default IP
455
+ time_id = int(time.time())
456
+
457
+ if progress_callback:
458
+ progress_callback("正在创建mask图片...")
459
+
460
+ # 从layers创建mask图片
461
+ mask_image = create_mask_from_layers(base_image, layers)
462
+
463
+ # 检查mask是否有内容
464
+ mask_array = np.array(mask_image)
465
+ if np.max(mask_array) == 0:
466
+ return None, "请在图片上绘制需要编辑的区域"
467
+
468
+ print(f"📝 创建mask图片成功,绘制区域像素数: {np.sum(mask_array > 0)}")
469
+
470
+ if progress_callback:
471
+ progress_callback("正在上传原始图片...")
472
+
473
+ # 处理并上传原始图片
474
+ temp_dir = tempfile.mkdtemp()
475
+ temp_img_path = os.path.join(temp_dir, f"temp_img_{time_id}.jpg")
476
+
477
+ # 保存原始图片
478
+ if base_image.mode != 'RGB':
479
+ base_image = base_image.convert('RGB')
480
+ base_image.save(temp_img_path, 'JPEG', quality=95)
481
+
482
+ # 上传原始图片
483
+ uploaded_url = upload_user_img_r2(client_ip, time_id, temp_img_path)
484
+ if not uploaded_url:
485
+ return None, "原始图片上传失败"
486
+
487
+ # 从上传 URL 中提取实际的图片 URL
488
+ if "?" in uploaded_url:
489
+ uploaded_url = uploaded_url.split("?")[0]
490
+
491
+ if progress_callback:
492
+ progress_callback("正在上传mask图片...")
493
+
494
+ # 上传mask图片
495
+ mask_url = upload_mask_image_r2(client_ip, time_id, mask_image)
496
+ if not mask_url:
497
+ return None, "mask图片上传失败"
498
+
499
+ # 从上传 URL 中提取实际的图片 URL
500
+ if "?" in mask_url:
501
+ mask_url = mask_url.split("?")[0]
502
+
503
+ print(f"📤 图片上传成功:")
504
+ print(f" 原始图片: {uploaded_url}")
505
+ print(f" Mask图片: {mask_url}")
506
+
507
+ if progress_callback:
508
+ progress_callback("正在提交局部编辑任务...")
509
+
510
+ # 提交局部图片编辑任务 (task_type=81)
511
+ task_id, error = submit_image_edit_task(uploaded_url, prompt, task_type="81", mask_image_url=mask_url)
512
+ if error:
513
+ return None, error
514
+
515
+ if progress_callback:
516
+ progress_callback(f"任务已提交,ID: {task_id},正在处理...")
517
+
518
+ print(f"🚀 局部编辑任务已提交,任务ID: {task_id}")
519
+
520
+ # Wait for task completion
521
+ max_attempts = 60 # Wait up to 10 minutes
522
+ for attempt in range(max_attempts):
523
+ status, output_url, task_data = check_task_status(task_id)
524
+
525
+ if status == 'completed':
526
+ if output_url:
527
+ print(f"✅ 局部编辑任务完成,结果: {output_url}")
528
+ return output_url, "局部图片编辑完成"
529
+ else:
530
+ return None, "任务完成但未返回结果图片"
531
+ elif status == 'error' or status == 'failed':
532
+ return None, f"任务处理失败: {task_data}"
533
+ elif status in ['queued', 'processing', 'running', 'created', 'working']:
534
+ if progress_callback:
535
+ progress_callback(f"正在处理中... (状态: {status})")
536
+ time.sleep(1) # Wait 1 second before retry
537
+ else:
538
+ if progress_callback:
539
+ progress_callback(f"未知状态: {status}")
540
+ time.sleep(1)
541
+
542
+ return None, "任务处理超时"
543
+
544
+ except Exception as e:
545
+ print(f"❌ 局部编辑处理异常: {str(e)}")
546
+ return None, f"处理过程中发生错误: {str(e)}"
547
+
548
+ finally:
549
+ # 清理临时文件
550
+ if temp_img_path and os.path.exists(temp_img_path):
551
+ try:
552
+ os.remove(temp_img_path)
553
+ temp_dir = os.path.dirname(temp_img_path)
554
+ if os.path.exists(temp_dir):
555
+ os.rmdir(temp_dir)
556
+ print(f"🗑️ Cleaned up temporary file: {temp_img_path}")
557
  except Exception as cleanup_error:
558
+ print(f"⚠️ Failed to clean up temporary file: {cleanup_error}")
559
 
560
 
561
  if __name__ == "__main__":