selfit-camera commited on
Commit
56dd4a4
·
1 Parent(s): f05960b
Files changed (6) hide show
  1. .gitattributes +8 -0
  2. .gitignore +7 -0
  3. app.py +899 -0
  4. push.sh +13 -0
  5. requirements.txt +10 -0
  6. util.py +600 -0
.gitattributes CHANGED
@@ -33,3 +33,11 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ # Image files
37
+ *.jpg filter=lfs diff=lfs merge=lfs -text
38
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
39
+ *.png filter=lfs diff=lfs merge=lfs -text
40
+ *.gif filter=lfs diff=lfs merge=lfs -text
41
+ *.bmp filter=lfs diff=lfs merge=lfs -text
42
+ *.webp filter=lfs diff=lfs merge=lfs -text
43
+ __pycache__/
.gitignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Ignore sample images - users will upload their own
2
+ samples/
3
+ *.DS_Store
4
+ __pycache__/
5
+ *.pyc
6
+ *.pyo
7
+ *.egg-info/
app.py ADDED
@@ -0,0 +1,899 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import threading
3
+ import os
4
+ import shutil
5
+ import tempfile
6
+ import time
7
+ from util import process_image_edit, OPENROUTER_API_KEY
8
+
9
+ # Configuration parameters for watermark removal
10
+ FREE_TRY_N = 8 # Free phase: first 8 tries without restrictions
11
+ SLOW_TRY_N = 20 # Slow phase start: 20 tries
12
+ RATE_LIMIT_20 = 20 # Full restriction: blocked after 20 tries
13
+
14
+ # Time window configuration (minutes)
15
+ PHASE_1_WINDOW = 5 # 8-20 tries: 5 minutes
16
+ MAX_IMAGES_PER_WINDOW = 2 # Max images per time window
17
+
18
+ IP_Dict = {}
19
+ # IP generation statistics and time window tracking
20
+ IP_Generation_Count = {} # Record total generation count for each IP
21
+ IP_Rate_Limit_Track = {} # Record generation count and timestamp in current time window for each IP
22
+
23
+ def get_ip_generation_count(client_ip):
24
+ """
25
+ Get IP generation count
26
+ """
27
+ if client_ip not in IP_Generation_Count:
28
+ IP_Generation_Count[client_ip] = 0
29
+ return IP_Generation_Count[client_ip]
30
+
31
+ def increment_ip_generation_count(client_ip):
32
+ """
33
+ Increment IP generation count
34
+ """
35
+ if client_ip not in IP_Generation_Count:
36
+ IP_Generation_Count[client_ip] = 0
37
+ IP_Generation_Count[client_ip] += 1
38
+ return IP_Generation_Count[client_ip]
39
+
40
+ def get_ip_phase(client_ip):
41
+ """
42
+ Get current phase for IP
43
+
44
+ Returns:
45
+ str: 'free', 'rate_limit_1', 'blocked'
46
+ """
47
+ count = get_ip_generation_count(client_ip)
48
+
49
+ if count < FREE_TRY_N: # 0-7 tries
50
+ return 'free'
51
+ elif count < SLOW_TRY_N: # 8-19 tries
52
+ return 'rate_limit_1' # 5 minutes 2 images
53
+ else: # 20+ tries
54
+ return 'blocked' # Generation blocked
55
+
56
+ def check_rate_limit_for_phase(client_ip, phase):
57
+ """
58
+ Check rate limit for specific phase
59
+
60
+ Returns:
61
+ tuple: (is_limited, wait_time_minutes, current_count)
62
+ """
63
+ if phase not in ['rate_limit_1']:
64
+ return False, 0, 0
65
+
66
+ # Determine time window
67
+ window_minutes = PHASE_1_WINDOW # 5 minutes
68
+
69
+ current_time = time.time()
70
+ window_key = f"{client_ip}_{phase}"
71
+
72
+ # Clean expired records
73
+ if window_key in IP_Rate_Limit_Track:
74
+ track_data = IP_Rate_Limit_Track[window_key]
75
+ # Check if within current time window
76
+ if current_time - track_data['start_time'] > window_minutes * 60:
77
+ # Time window expired, reset
78
+ IP_Rate_Limit_Track[window_key] = {
79
+ 'count': 0,
80
+ 'start_time': current_time,
81
+ 'last_generation': current_time
82
+ }
83
+ else:
84
+ # Initialize
85
+ IP_Rate_Limit_Track[window_key] = {
86
+ 'count': 0,
87
+ 'start_time': current_time,
88
+ 'last_generation': current_time
89
+ }
90
+
91
+ track_data = IP_Rate_Limit_Track[window_key]
92
+
93
+ # Check if exceeded limit
94
+ if track_data['count'] >= MAX_IMAGES_PER_WINDOW:
95
+ # Calculate remaining wait time
96
+ elapsed = current_time - track_data['start_time']
97
+ wait_time = (window_minutes * 60) - elapsed
98
+ wait_minutes = max(0, wait_time / 60)
99
+ return True, wait_minutes, track_data['count']
100
+
101
+ return False, 0, track_data['count']
102
+
103
+ def record_generation_attempt(client_ip, phase):
104
+ """
105
+ Record generation attempt
106
+ """
107
+ # Increment total count
108
+ increment_ip_generation_count(client_ip)
109
+
110
+ # Record time window count
111
+ if phase in ['rate_limit_1']:
112
+ window_key = f"{client_ip}_{phase}"
113
+ current_time = time.time()
114
+
115
+ if window_key in IP_Rate_Limit_Track:
116
+ IP_Rate_Limit_Track[window_key]['count'] += 1
117
+ IP_Rate_Limit_Track[window_key]['last_generation'] = current_time
118
+ else:
119
+ IP_Rate_Limit_Track[window_key] = {
120
+ 'count': 1,
121
+ 'start_time': current_time,
122
+ 'last_generation': current_time
123
+ }
124
+
125
+ # Load sample images from samples directory
126
+ def load_sample_images():
127
+ """Load sample images from samples directory"""
128
+ samples_dir = "samples"
129
+ sample_images = []
130
+
131
+ if os.path.exists(samples_dir):
132
+ for filename in os.listdir(samples_dir):
133
+ if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp')):
134
+ file_path = os.path.join(samples_dir, filename)
135
+ sample_images.append((file_path, filename))
136
+
137
+ return sample_images
138
+
139
+ # Pre-processed results for sample images (local files)
140
+ SAMPLE_RESULTS = {
141
+ "sample01.jpeg": "samples/sample01_rm.jpeg",
142
+ "sample02.jpeg": "samples/sample02_rm.jpeg",
143
+ "1.jpg": "samples/1_rm.jpg",
144
+ "4.jpg": "samples/4_rm.jpg",
145
+ "10.jpg": "samples/10_rm.jpg"
146
+ }
147
+
148
+ def generate_smart_watermark_prompt(image_url):
149
+ """Generate smart watermark removal prompt using OpenRouter API"""
150
+ import requests
151
+ import json
152
+
153
+ try:
154
+ api_key = OPENROUTER_API_KEY if 'OPENROUTER_API_KEY' in globals() else ""
155
+ if not api_key:
156
+ print("Warning: OPENROUTER_API_KEY not found")
157
+ return "remove watermark in the image"
158
+
159
+ headers = {
160
+ 'Content-Type': 'application/json',
161
+ 'Authorization': f'Bearer {api_key}'
162
+ }
163
+
164
+ data = {
165
+ "model": "google/gemini-2.5-flash-lite",
166
+ "messages": [
167
+ {"role": "user", "content": "现在用户输入了一张带水印的图片,需要生成修复prompt来引导图像生成模型去除水印。你需要找到水印,然后输出去除水印的英文prompt,指出水印的位置和特点,让模型知道即可,尽量简短。直接输出prompt即可。举例1: 'Remove the blue watermark in the lower right corner of the document'. 举例2: 'Remove the white watermark over the whole picture'"},
168
+ {"role": "assistant", "content": "好的"},
169
+ {
170
+ "role": "user",
171
+ "content": [
172
+ {"type": "text", "text": "Generate an English prompt to remove the watermark in this image."},
173
+ {"type": "image_url", "image_url": {"url": image_url}}
174
+ ]
175
+ }
176
+ ]
177
+ }
178
+
179
+ response = requests.post(
180
+ 'https://openrouter.ai/api/v1/chat/completions',
181
+ headers=headers,
182
+ json=data,
183
+ timeout=30
184
+ )
185
+
186
+ if response.status_code == 200:
187
+ result = response.json()
188
+ if result.get("choices") and len(result["choices"]) > 0:
189
+ prompt = result["choices"][0]["message"]["content"].strip()
190
+ print(f"✅ Generated smart prompt: {prompt}")
191
+ return prompt
192
+
193
+ print(f"⚠️ OpenRouter API failed with status {response.status_code}")
194
+ return "remove watermark in the image"
195
+
196
+ except Exception as e:
197
+ print(f"⚠️ Error generating smart prompt: {e}")
198
+ return "remove watermark in the image"
199
+
200
+ def remove_watermark_interface(input_image, force_removal, request: gr.Request, progress=gr.Progress()):
201
+ """
202
+ Interface function for watermark removal with phase-based limitations
203
+ """
204
+ try:
205
+ # Extract user IP
206
+ client_ip = request.client.host
207
+ x_forwarded_for = dict(request.headers).get('x-forwarded-for')
208
+ if x_forwarded_for:
209
+ client_ip = x_forwarded_for
210
+ if client_ip not in IP_Dict:
211
+ IP_Dict[client_ip] = 0
212
+ IP_Dict[client_ip] += 1
213
+
214
+ if input_image is None:
215
+ return None, "Please upload an image first", gr.update(visible=False)
216
+
217
+ # Set initial prompt for watermark removal
218
+ prompt = "remove watermark in the image"
219
+ except Exception as e:
220
+ print(f"⚠️ Request preprocessing error: {e}")
221
+ return None, "❌ Request processing error", gr.update(visible=False)
222
+
223
+ # Get user current phase
224
+ current_phase = get_ip_phase(client_ip)
225
+ current_count = get_ip_generation_count(client_ip)
226
+
227
+ print(f"📊 User phase info - IP: {client_ip}, current phase: {current_phase}, generation count: {current_count}")
228
+
229
+ # Check if completely blocked
230
+ if current_phase == 'blocked':
231
+ # Generate blocked limit button
232
+ blocked_button_html = f"""
233
+ <div style='display: flex; justify-content: center; gap: 15px; margin: 10px 0 5px 0; padding: 0px;'>
234
+ <a href='https://omnicreator.net/remove-watermark#generator' target='_blank' style='
235
+ display: inline-flex;
236
+ align-items: center;
237
+ justify-content: center;
238
+ padding: 16px 32px;
239
+ background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
240
+ color: white;
241
+ text-decoration: none;
242
+ border-radius: 12px;
243
+ font-weight: 600;
244
+ font-size: 16px;
245
+ text-align: center;
246
+ min-width: 200px;
247
+ box-shadow: 0 4px 15px rgba(231, 76, 60, 0.4);
248
+ transition: all 0.3s ease;
249
+ border: none;
250
+ '>🚀 Remove More Watermarks</a>
251
+ </div>
252
+ """
253
+ return None, f"❌ You have reached Hugging Face's free watermark removal limit. Please visit https://omnicreator.net/remove-watermark#generator for unlimited removal", gr.update(value=blocked_button_html, visible=True)
254
+
255
+ # Check rate limit (applies to rate_limit phases)
256
+ if current_phase in ['rate_limit_1']:
257
+ is_limited, wait_minutes, window_count = check_rate_limit_for_phase(client_ip, current_phase)
258
+ if is_limited:
259
+ wait_minutes_int = int(wait_minutes) + 1
260
+ # Generate rate limit button
261
+ rate_limit_button_html = f"""
262
+ <div style='display: flex; justify-content: center; gap: 15px; margin: 10px 0 5px 0; padding: 0px;'>
263
+ <a href='https://omnicreator.net/remove-watermark#generator' target='_blank' style='
264
+ display: inline-flex;
265
+ align-items: center;
266
+ justify-content: center;
267
+ padding: 16px 32px;
268
+ background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);
269
+ color: white;
270
+ text-decoration: none;
271
+ border-radius: 12px;
272
+ font-weight: 600;
273
+ font-size: 16px;
274
+ text-align: center;
275
+ min-width: 200px;
276
+ box-shadow: 0 4px 15px rgba(243, 156, 18, 0.4);
277
+ transition: all 0.3s ease;
278
+ border: none;
279
+ '>⏰ Skip Wait - Unlimited Watermark Removal</a>
280
+ </div>
281
+ """
282
+ return None, f"❌ You have reached Hugging Face's free watermark removal limit. Please visit https://omnicreator.net/remove-watermark#generator for unlimited removal, or wait {wait_minutes_int} minutes before generating again", gr.update(value=rate_limit_button_html, visible=True)
283
+
284
+ # No NSFW detection needed for watermark removal
285
+
286
+ result_url = None
287
+ status_message = ""
288
+
289
+ def progress_callback(message):
290
+ try:
291
+ nonlocal status_message
292
+ status_message = message
293
+ # Add error handling to prevent progress update failure
294
+ if progress is not None:
295
+ progress(0.5, desc=message)
296
+ except Exception as e:
297
+ print(f"⚠️ Progress update failed: {e}")
298
+
299
+ try:
300
+ # Record generation attempt (before actual generation to ensure correct count)
301
+ record_generation_attempt(client_ip, current_phase)
302
+ updated_count = get_ip_generation_count(client_ip)
303
+
304
+ print(f"✅ Processing started - IP: {client_ip}, phase: {current_phase}, total count: {updated_count}, prompt: {prompt.strip()}", flush=True)
305
+
306
+ # Force removal mode: Generate smart prompt using OpenRouter API
307
+ if force_removal:
308
+ if progress is not None:
309
+ progress(0.3, desc="💪 AI Force Mode: Analyzing watermark pattern...")
310
+
311
+ try:
312
+ # Upload image to get URL for OpenRouter API
313
+ from util import upload_user_img_r2
314
+ time_id = int(time.time())
315
+ image_url = upload_user_img_r2(client_ip, time_id, input_image)
316
+
317
+ if image_url:
318
+ # Generate smart prompt using OpenRouter API
319
+ smart_prompt = generate_smart_watermark_prompt(image_url)
320
+ if smart_prompt and smart_prompt.strip() != "remove watermark in the image":
321
+ prompt = smart_prompt
322
+ print(f"🚀 Using AI Force Mode prompt: {prompt}")
323
+ else:
324
+ print("⚠️ Using fallback prompt")
325
+ else:
326
+ print("⚠️ Failed to upload image for AI analysis, using default prompt")
327
+
328
+ except Exception as e:
329
+ print(f"⚠️ Force Mode prompt generation failed: {e}, using default prompt")
330
+
331
+ if progress is not None:
332
+ progress(0.5, desc="Starting force watermark removal...")
333
+
334
+ # Call image editing processing function
335
+ result_url, message, task_uuid = process_image_edit(input_image, prompt.strip(), progress_callback)
336
+
337
+ if result_url:
338
+ print(f"✅ Watermark removal completed successfully - IP: {client_ip}, result_url: {result_url}, task_uuid: {task_uuid}", flush=True)
339
+
340
+ # No NSFW detection needed for watermark removal
341
+ final_result = result_url
342
+ final_message = "✅ " + message
343
+
344
+ try:
345
+ if progress is not None:
346
+ progress(1.0, desc="Processing completed")
347
+ except Exception as e:
348
+ print(f"⚠️ Final progress update failed: {e}")
349
+
350
+ # Generate action buttons HTML like Trump AI Voice
351
+ action_buttons_html = ""
352
+ if task_uuid:
353
+ task_detail_url = f"https://omnicreator.net/my-creations/task/{task_uuid}"
354
+ action_buttons_html = f"""
355
+ <div style='display: flex; justify-content: center; gap: 15px; margin: 10px 0 5px 0; padding: 0px;'>
356
+ <a href='{task_detail_url}' target='_blank' style='
357
+ display: inline-flex;
358
+ align-items: center;
359
+ justify-content: center;
360
+ padding: 16px 32px;
361
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
362
+ color: white;
363
+ text-decoration: none;
364
+ border-radius: 12px;
365
+ font-weight: 600;
366
+ font-size: 16px;
367
+ text-align: center;
368
+ min-width: 160px;
369
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
370
+ transition: all 0.3s ease;
371
+ border: none;
372
+ '>🖼️ Download HD Image</a>
373
+ <a href='https://omnicreator.net/remove-watermark#generator' target='_blank' style='
374
+ display: inline-flex;
375
+ align-items: center;
376
+ justify-content: center;
377
+ padding: 16px 32px;
378
+ background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
379
+ color: white;
380
+ text-decoration: none;
381
+ border-radius: 12px;
382
+ font-weight: 600;
383
+ font-size: 16px;
384
+ text-align: center;
385
+ min-width: 160px;
386
+ box-shadow: 0 4px 15px rgba(17, 153, 142, 0.4);
387
+ transition: all 0.3s ease;
388
+ border: none;
389
+ '>🚀 Remove More Watermarks</a>
390
+ </div>
391
+
392
+ <div style='text-align: center; margin: 15px 0; padding: 15px; background: linear-gradient(135deg, #fff5f5 0%, #fed7e2 100%); border-radius: 12px; border-left: 4px solid #f56565;'>
393
+ <p style='margin: 0 0 10px 0; color: #4a5568; font-size: 14px; font-weight: 500;'>
394
+ Not satisfied with the removal result?
395
+ </p>
396
+ <a href='https://omnicreator.net/remove-watermark' target='_blank' style='
397
+ display: inline-flex;
398
+ align-items: center;
399
+ justify-content: center;
400
+ padding: 12px 24px;
401
+ background: linear-gradient(135deg, #f56565 0%, #e53e3e 100%);
402
+ color: white;
403
+ text-decoration: none;
404
+ border-radius: 8px;
405
+ font-weight: 600;
406
+ font-size: 14px;
407
+ text-align: center;
408
+ box-shadow: 0 4px 12px rgba(245, 101, 101, 0.4);
409
+ transition: all 0.3s ease;
410
+ border: none;
411
+ '>💪 Try Force Mode on Website</a>
412
+ </div>
413
+ """
414
+
415
+ return final_result, final_message, gr.update(value=action_buttons_html, visible=True)
416
+ else:
417
+ print(f"❌ Processing failed - IP: {client_ip}, error: {message}", flush=True)
418
+ return None, "❌ " + message, gr.update(visible=False)
419
+
420
+ except Exception as e:
421
+ print(f"❌ Processing exception - IP: {client_ip}, error: {str(e)}")
422
+ return None, f"❌ Error occurred during processing: {str(e)}", gr.update(visible=False)
423
+
424
+ # Create Gradio interface
425
+ def create_app():
426
+ with gr.Blocks(
427
+ title="AI Watermark Remover",
428
+ theme=gr.themes.Soft(),
429
+ css="""
430
+ .main-container {
431
+ max-width: 1200px;
432
+ margin: 0 auto;
433
+ }
434
+ .upload-area {
435
+ border: 2px dashed #ccc;
436
+ border-radius: 10px;
437
+ padding: 20px;
438
+ text-align: center;
439
+ }
440
+ .result-area {
441
+ margin-top: 20px;
442
+ padding: 20px;
443
+ border-radius: 10px;
444
+ background-color: #f8f9fa;
445
+ }
446
+ .use-as-input-btn {
447
+ margin-top: 10px;
448
+ width: 100%;
449
+ }
450
+ """,
451
+ # Improve concurrency performance configuration
452
+ head="""
453
+ <script>
454
+ // Reduce client-side state update frequency, avoid excessive SSE connections
455
+ if (window.gradio) {
456
+ window.gradio.update_frequency = 2000; // Update every 2 seconds
457
+ }
458
+ </script>
459
+ """
460
+ ) as app:
461
+
462
+ # Main title - styled for watermark removal
463
+ gr.HTML("""
464
+ <div style="text-align: center; margin: 5px auto 0px auto; max-width: 800px;">
465
+ <h1 style="color: #2c3e50; margin: 0; font-size: 3.5em; font-weight: 800; letter-spacing: 3px; text-shadow: 2px 2px 4px rgba(0,0,0,0.1);">
466
+ 💧 AI Watermark Remover
467
+ </h1>
468
+ </div>
469
+ """, padding=False)
470
+
471
+ # Powered by line below title - styled like Trump AI Voice
472
+ gr.HTML("""
473
+ <div style="text-align: center; margin: 0px auto -5px auto;">
474
+ <p style="margin: 0; font-size: 16px; color: #999; font-weight: 400;">
475
+ powered by <a href="https://omnicreator.net/remove-watermark#generator" target="_blank" style="color: #667eea; text-decoration: none;">omnicreator.net</a>
476
+ </p>
477
+ </div>
478
+ """, padding=False)
479
+
480
+ # Load sample images
481
+ sample_images = load_sample_images()
482
+
483
+ # Watermark removal interface (New 2-column layout)
484
+ with gr.Row():
485
+ # --- LEFT COLUMN (INPUT & CONTROLS) ---
486
+ with gr.Column(scale=1):
487
+ gr.Markdown("### 📸 Upload Image with Watermark")
488
+ input_image = gr.Image(
489
+ label="Select image to remove watermark",
490
+ type="pil",
491
+ height=450,
492
+ elem_classes=["upload-area"]
493
+ )
494
+
495
+ force_removal = gr.Checkbox(
496
+ label="💪 Force Removal",
497
+ info="Ultimate AI-powered watermark removal. AI analyzes the watermark for better results.",
498
+ value=False
499
+ )
500
+
501
+ remove_button = gr.Button(
502
+ "💧 Remove Watermark",
503
+ variant="primary",
504
+ size="lg"
505
+ )
506
+
507
+ # --- RIGHT COLUMN (OUTPUT) ---
508
+ with gr.Column(scale=1):
509
+ gr.Markdown("### 🎯 Watermark Removal Result")
510
+ output_image = gr.Image(
511
+ label="Image without watermark",
512
+ height=450,
513
+ elem_classes=["result-area"]
514
+ )
515
+
516
+ use_as_input_btn = gr.Button(
517
+ "🔄 Use as Input",
518
+ variant="secondary",
519
+ size="sm",
520
+ elem_classes=["use-as-input-btn"]
521
+ )
522
+
523
+ status_output = gr.Textbox(
524
+ label="Processing status",
525
+ lines=2,
526
+ max_lines=3,
527
+ interactive=False
528
+ )
529
+
530
+ action_buttons = gr.HTML(visible=False)
531
+
532
+ # Sample images gallery (moved below main interface)
533
+ sample_buttons = []
534
+ if sample_images:
535
+ gr.Markdown("### 💡 Try with Sample Images")
536
+ with gr.Row():
537
+ for idx, (img_path, img_name) in enumerate(sample_images[:4]): # Show first 4 samples
538
+ with gr.Column(scale=1):
539
+ sample_img = gr.Image(
540
+ value=img_path,
541
+ label=img_name,
542
+ height=150,
543
+ interactive=False
544
+ )
545
+ use_sample_btn = gr.Button(
546
+ f"Use {img_name}",
547
+ size="sm",
548
+ variant="secondary"
549
+ )
550
+ sample_buttons.append((use_sample_btn, img_path))
551
+
552
+ # Add sample image button functionality with demo processing
553
+ def create_sample_handler(img_path, img_name):
554
+ def handler():
555
+ import time
556
+ import os
557
+ from PIL import Image
558
+
559
+ # Wait 3 seconds as requested
560
+ time.sleep(3)
561
+
562
+ # Get the pre-processed result file path
563
+ result_file_path = SAMPLE_RESULTS.get(img_name)
564
+
565
+ if result_file_path and os.path.exists(result_file_path):
566
+ # Load the result image from local file
567
+ try:
568
+ result_image = Image.open(result_file_path)
569
+ return img_path, result_image, "✅ 示例水印移除完成!这是一个预处理的演示结果。"
570
+ except Exception as e:
571
+ print(f"Failed to load sample result from {result_file_path}: {e}")
572
+
573
+ # Fallback: just return the input image
574
+ return img_path, None, "✅ 示例图片已加载。点击 '移除水印' 开始处理。"
575
+
576
+ return handler
577
+
578
+ for sample_btn, img_path in sample_buttons:
579
+ img_name = os.path.basename(img_path)
580
+ sample_btn.click(
581
+ fn=create_sample_handler(img_path, img_name),
582
+ outputs=[input_image, output_image, status_output],
583
+ show_progress=True
584
+ )
585
+
586
+ # Bind button click events
587
+ remove_button.click(
588
+ fn=remove_watermark_interface,
589
+ inputs=[input_image, force_removal],
590
+ outputs=[output_image, status_output, action_buttons],
591
+ show_progress=True,
592
+ concurrency_limit=10,
593
+ api_name="remove_watermark"
594
+ )
595
+
596
+ # "Use as Input" button functionality
597
+ def simple_use_as_input(output_img):
598
+ if output_img is not None:
599
+ return output_img
600
+ return None
601
+
602
+ use_as_input_btn.click(
603
+ fn=simple_use_as_input,
604
+ inputs=[output_image],
605
+ outputs=[input_image]
606
+ )
607
+
608
+ # Watermark Removal SEO Content Section
609
+ gr.HTML("""
610
+ <div style="width: 100%; margin: 50px 0; padding: 0 20px;">
611
+
612
+ <div style="text-align: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 40px; border-radius: 20px; margin: 40px 0;">
613
+ <h2 style="margin: 0 0 20px 0; font-size: 2.2em; font-weight: 700;">
614
+ 💧 Unlimited AI Watermark Removal
615
+ </h2>
616
+ <p style="margin: 0 0 25px 0; font-size: 1.2em; opacity: 0.95; line-height: 1.6;">
617
+ Remove watermarks from any image instantly with advanced AI technology! Clean your photos and documents
618
+ without compromising image quality using our professional watermark removal tool.
619
+ </p>
620
+
621
+ <div style="display: flex; justify-content: center; gap: 25px; flex-wrap: wrap; margin: 30px 0;">
622
+ <a href="https://omnicreator.net/remove-watermark#generator" target="_blank" style="
623
+ display: inline-flex;
624
+ align-items: center;
625
+ justify-content: center;
626
+ padding: 20px 40px;
627
+ background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%);
628
+ color: white;
629
+ text-decoration: none;
630
+ border-radius: 15px;
631
+ font-weight: 700;
632
+ font-size: 18px;
633
+ text-align: center;
634
+ min-width: 250px;
635
+ box-shadow: 0 8px 25px rgba(255, 107, 107, 0.4);
636
+ transition: all 0.3s ease;
637
+ border: none;
638
+ transform: scale(1);
639
+ " onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
640
+ 💧 Get Unlimited Watermark Removal
641
+ </a>
642
+
643
+ </div>
644
+
645
+ <p style="color: rgba(255,255,255,0.9); font-size: 1em; margin: 20px 0 0 0;">
646
+ Join thousands of users who trust Omni Creator for professional watermark removal!
647
+ </p>
648
+ </div>
649
+
650
+ <div style="text-align: center; margin: 25px auto; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); padding: 35px; border-radius: 20px; box-shadow: 0 10px 30px rgba(0,0,0,0.1);">
651
+ <h2 style="color: #2c3e50; margin: 0 0 20px 0; font-size: 1.9em; font-weight: 700;">
652
+ 🌟 Professional AI Watermark Remover - Fast & Accurate
653
+ </h2>
654
+ <p style="color: #555; font-size: 1.1em; line-height: 1.6; margin: 0 0 20px 0; padding: 0 20px;">
655
+ Clean your images effortlessly with our advanced AI watermark removal technology. Whether you're dealing with
656
+ text watermarks, logo stamps, or transparent overlays - our intelligent algorithm removes them while preserving
657
+ the original image quality and details.
658
+ </p>
659
+ </div>
660
+
661
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 25px; margin: 40px 0;">
662
+
663
+ <div style="background: white; padding: 30px; border-radius: 15px; box-shadow: 0 5px 20px rgba(0,0,0,0.08); border-left: 5px solid #e74c3c;">
664
+ <h3 style="color: #e74c3c; margin: 0 0 15px 0; font-size: 1.4em; font-weight: 600;">
665
+ 🎯 Unlimited Removal
666
+ </h3>
667
+ <p style="color: #666; margin: 0; line-height: 1.6; font-size: 1em;">
668
+ Premium users enjoy unlimited watermark removal without daily limits or processing restrictions.
669
+ Clean as many images as you need, whenever you need them, with no quality loss.
670
+ </p>
671
+ </div>
672
+
673
+ <div style="background: white; padding: 30px; border-radius: 15px; box-shadow: 0 5px 20px rgba(0,0,0,0.08); border-left: 5px solid #3498db;">
674
+ <h3 style="color: #3498db; margin: 0 0 15px 0; font-size: 1.4em; font-weight: 600;">
675
+ 🎯 Smart Detection
676
+ </h3>
677
+ <p style="color: #666; margin: 0; line-height: 1.6; font-size: 1em;">
678
+ AI automatically identifies and removes various types of watermarks including text, logos, stamps, and
679
+ transparent overlays without affecting the main image content.
680
+ </p>
681
+ </div>
682
+
683
+ <div style="background: white; padding: 30px; border-radius: 15px; box-shadow: 0 5px 20px rgba(0,0,0,0.08); border-left: 5px solid #27ae60;">
684
+ <h3 style="color: #27ae60; margin: 0 0 15px 0; font-size: 1.4em; font-weight: 600;">
685
+ ⚡ Lightning Fast Processing
686
+ </h3>
687
+ <p style="color: #666; margin: 0; line-height: 1.6; font-size: 1em;">
688
+ Advanced AI infrastructure delivers watermark-free results in seconds. No waiting in queues,
689
+ no processing delays - just instant, professional-grade watermark removal.
690
+ </p>
691
+ </div>
692
+
693
+ <div style="background: white; padding: 30px; border-radius: 15px; box-shadow: 0 5px 20px rgba(0,0,0,0.08); border-left: 5px solid #9b59b6;">
694
+ <h3 style="color: #9b59b6; margin: 0 0 15px 0; font-size: 1.4em; font-weight: 600;">
695
+ 🎨 Quality Preservation
696
+ </h3>
697
+ <p style="color: #666; margin: 0; line-height: 1.6; font-size: 1em;">
698
+ Intelligent inpainting technology reconstructs watermark areas seamlessly while maintaining original
699
+ image quality, colors, textures, and details perfectly.
700
+ </p>
701
+ </div>
702
+
703
+ <div style="background: white; padding: 30px; border-radius: 15px; box-shadow: 0 5px 20px rgba(0,0,0,0.08); border-left: 5px solid #f39c12;">
704
+ <h3 style="color: #f39c12; margin: 0 0 15px 0; font-size: 1.4em; font-weight: 600;">
705
+ 💎 Premium Quality
706
+ </h3>
707
+ <p style="color: #666; margin: 0; line-height: 1.6; font-size: 1em;">
708
+ State-of-the-art AI models trained specifically for watermark removal deliver exceptional results.
709
+ Professional quality output suitable for commercial use and high-end projects.
710
+ </p>
711
+ </div>
712
+
713
+ <div style="background: white; padding: 30px; border-radius: 15px; box-shadow: 0 5px 20px rgba(0,0,0,0.08); border-left: 5px solid #34495e;">
714
+ <h3 style="color: #34495e; margin: 0 0 15px 0; font-size: 1.4em; font-weight: 600;">
715
+ 🌍 Universal Compatibility
716
+ </h3>
717
+ <p style="color: #666; margin: 0; line-height: 1.6; font-size: 1em;">
718
+ Works with all image formats (JPG, PNG, GIF, BMP) and removes any type of watermark. From photos to
719
+ documents, product images to artwork - we clean them all perfectly.
720
+ </p>
721
+ </div>
722
+
723
+ </div>
724
+
725
+ <div style="background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); color: white; padding: 40px; border-radius: 20px; margin: 40px 0; text-align: center;">
726
+ <h2 style="margin: 0 0 25px 0; font-size: 1.8em; font-weight: 700;">
727
+ 💎 Why Choose Premium Watermark Removal?
728
+ </h2>
729
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin: 30px 0;">
730
+
731
+ <div style="background: rgba(255,255,255,0.15); padding: 20px; border-radius: 12px;">
732
+ <h4 style="margin: 0 0 10px 0; font-size: 1.2em;">🚫 No Processing Limits</h4>
733
+ <p style="margin: 0; opacity: 0.9; font-size: 0.95em;">Remove watermarks from unlimited images without waiting periods or daily restrictions</p>
734
+ </div>
735
+
736
+ <div style="background: rgba(255,255,255,0.15); padding: 20px; border-radius: 12px;">
737
+ <h4 style="margin: 0 0 10px 0; font-size: 1.2em;">💧 Advanced Removal</h4>
738
+ <p style="margin: 0; opacity: 0.9; font-size: 0.95em;">Remove any type of watermark including complex, transparent, and multi-layered marks</p>
739
+ </div>
740
+
741
+ <div style="background: rgba(255,255,255,0.15); padding: 20px; border-radius: 12px;">
742
+ <h4 style="margin: 0 0 10px 0; font-size: 1.2em;">⚡ Priority Processing</h4>
743
+ <p style="margin: 0; opacity: 0.9; font-size: 0.95em;">Skip queues and get instant watermark removal with dedicated processing power</p>
744
+ </div>
745
+
746
+ <div style="background: rgba(255,255,255,0.15); padding: 20px; border-radius: 12px;">
747
+ <h4 style="margin: 0 0 10px 0; font-size: 1.2em;">🎨 Perfect Quality</h4>
748
+ <p style="margin: 0; opacity: 0.9; font-size: 0.95em;">Access to latest AI models for highest quality watermark removal results</p>
749
+ </div>
750
+
751
+ </div>
752
+ <div style="display: flex; justify-content: center; margin: 25px 0 0 0;">
753
+ <a href="https://omnicreator.net/remove-watermark#generator" target="_blank" style="
754
+ display: inline-flex;
755
+ align-items: center;
756
+ justify-content: center;
757
+ padding: 18px 35px;
758
+ background: rgba(255,255,255,0.9);
759
+ color: #333;
760
+ text-decoration: none;
761
+ border-radius: 15px;
762
+ font-weight: 700;
763
+ font-size: 16px;
764
+ text-align: center;
765
+ min-width: 200px;
766
+ box-shadow: 0 6px 20px rgba(0,0,0,0.3);
767
+ transition: all 0.3s ease;
768
+ border: none;
769
+ ">🌟 Start Creating Now</a>
770
+ </div>
771
+ </div>
772
+
773
+ <div style="background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 50%, #fecfef 100%); padding: 30px; border-radius: 15px; margin: 40px 0;">
774
+ <h3 style="color: #8b5cf6; text-align: center; margin: 0 0 25px 0; font-size: 1.5em; font-weight: 700;">
775
+ 💡 Pro Tips for Best Watermark Removal
776
+ </h3>
777
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 18px;">
778
+
779
+ <div style="background: rgba(255,255,255,0.85); padding: 18px; border-radius: 12px;">
780
+ <strong style="color: #8b5cf6; font-size: 1.1em;">📝 Image Quality:</strong>
781
+ <p style="color: #555; margin: 5px 0 0 0; line-height: 1.5;">Higher resolution input images (up to 10MB) generally produce better watermark removal results and finer details.</p>
782
+ </div>
783
+
784
+ <div style="background: rgba(255,255,255,0.85); padding: 18px; border-radius: 12px;">
785
+ <strong style="color: #8b5cf6; font-size: 1.1em;">🎯 Clean Images:</strong>
786
+ <p style="color: #555; margin: 5px 0 0 0; line-height: 1.5;">Images with simple backgrounds work best. Complex patterns behind watermarks may require multiple processing attempts.</p>
787
+ </div>
788
+
789
+ <div style="background: rgba(255,255,255,0.85); padding: 18px; border-radius: 12px;">
790
+ <strong style="color: #8b5cf6; font-size: 1.1em;">⚡ Iterative Process:</strong>
791
+ <p style="color: #555; margin: 5px 0 0 0; line-height: 1.5;">Use "Use as Input" feature to refine results. Multiple iterations can remove stubborn or complex watermarks completely.</p>
792
+ </div>
793
+
794
+ <div style="background: rgba(255,255,255,0.85); padding: 18px; border-radius: 12px;">
795
+ <strong style="color: #8b5cf6; font-size: 1.1em;">🖼️ File Formats:</strong>
796
+ <p style="color: #555; margin: 5px 0 0 0; line-height: 1.5;">JPG and PNG formats work best. Avoid heavily compressed images as they may affect watermark detection accuracy.</p>
797
+ </div>
798
+
799
+ </div>
800
+ </div>
801
+
802
+ <div style="text-align: center; margin: 25px auto; background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); padding: 35px; border-radius: 20px; box-shadow: 0 10px 30px rgba(0,0,0,0.1);">
803
+ <h2 style="color: #2c3e50; margin: 0 0 20px 0; font-size: 1.8em; font-weight: 700;">
804
+ 🚀 Perfect For Every Watermark Removal Need
805
+ </h2>
806
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 25px 0; text-align: left;">
807
+
808
+ <div style="background: rgba(255,255,255,0.8); padding: 20px; border-radius: 12px;">
809
+ <h4 style="color: #e74c3c; margin: 0 0 10px 0;">📄 Documents</h4>
810
+ <ul style="color: #555; margin: 0; padding-left: 18px; line-height: 1.6;">
811
+ <li>PDFs with watermarks</li>
812
+ <li>Scanned documents</li>
813
+ <li>Legal papers</li>
814
+ <li>Certificates</li>
815
+ </ul>
816
+ </div>
817
+
818
+ <div style="background: rgba(255,255,255,0.8); padding: 20px; border-radius: 12px;">
819
+ <h4 style="color: #3498db; margin: 0 0 10px 0;">📸 Photography</h4>
820
+ <ul style="color: #555; margin: 0; padding-left: 18px; line-height: 1.6;">
821
+ <li>Stock photos</li>
822
+ <li>Portrait photos</li>
823
+ <li>Event photography</li>
824
+ <li>Nature images</li>
825
+ </ul>
826
+ </div>
827
+
828
+ <div style="background: rgba(255,255,255,0.8); padding: 20px; border-radius: 12px;">
829
+ <h4 style="color: #27ae60; margin: 0 0 10px 0;">🛍️ E-commerce</h4>
830
+ <ul style="color: #555; margin: 0; padding-left: 18px; line-height: 1.6;">
831
+ <li>Product images</li>
832
+ <li>Catalog photos</li>
833
+ <li>Supplier images</li>
834
+ <li>Brand cleanup</li>
835
+ </ul>
836
+ </div>
837
+
838
+ <div style="background: rgba(255,255,255,0.8); padding: 20px; border-radius: 12px;">
839
+ <h4 style="color: #9b59b6; margin: 0 0 10px 0;">📱 Social Media</h4>
840
+ <ul style="color: #555; margin: 0; padding-left: 18px; line-height: 1.6;">
841
+ <li>Content creation</li>
842
+ <li>Profile pictures</li>
843
+ <li>Marketing visuals</li>
844
+ <li>Clean presentations</li>
845
+ </ul>
846
+ </div>
847
+
848
+ </div>
849
+ <div style="text-align: center; margin: 25px 0 0 0;">
850
+ <a href="https://omnicreator.net/remove-watermark#generator" target="_blank" style="
851
+ display: inline-flex;
852
+ align-items: center;
853
+ justify-content: center;
854
+ padding: 18px 35px;
855
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
856
+ color: white;
857
+ text-decoration: none;
858
+ border-radius: 15px;
859
+ font-weight: 700;
860
+ font-size: 16px;
861
+ text-align: center;
862
+ min-width: 220px;
863
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
864
+ transition: all 0.3s ease;
865
+ border: none;
866
+ ">💧 Start Removing Watermarks</a>
867
+ </div>
868
+ </div>
869
+
870
+ </div>
871
+
872
+ <div style="text-align: center; margin: 30px auto 20px auto; padding: 20px;">
873
+ <p style="margin: 0 0 10px 0; font-size: 18px; color: #333; font-weight: 500;">
874
+ Powered by <a href="https://omnicreator.net/remove-watermark#generator" target="_blank" style="color: #667eea; text-decoration: none; font-weight: bold;">Omni Creator</a>
875
+ </p>
876
+ <p style="margin: 0; font-size: 14px; color: #999; font-weight: 400;">
877
+ The ultimate AI watermark removal platform • Professional results, unlimited processing
878
+ </p>
879
+ </div>
880
+ """, padding=False)
881
+
882
+ return app
883
+
884
+ if __name__ == "__main__":
885
+ app = create_app()
886
+ # Improve queue configuration to handle high concurrency and prevent SSE connection issues
887
+ app.queue(
888
+ default_concurrency_limit=20, # Default concurrency limit
889
+ max_size=50, # Maximum queue size
890
+ api_open=False # Close API access to reduce resource consumption
891
+ )
892
+ app.launch(
893
+ server_name="0.0.0.0",
894
+ show_error=True, # Show detailed error information
895
+ quiet=False, # Keep log output
896
+ max_threads=40, # Increase thread pool size
897
+ height=800,
898
+ favicon_path=None # Reduce resource loading
899
+ )
push.sh ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # 设置仓库级别用户名
3
+ git config user.name "selfitcamera"
4
+ git config user.email "[email protected]"
5
+
6
+ # 验证
7
+ git config user.name
8
+ git config user.email
9
+
10
+
11
+ git add .
12
+ git commit -m "init"
13
+ git push
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio==5.42.0
2
+ opencv-python>=4.8.0
3
+ requests>=2.28.0
4
+ func-timeout>=4.3.5
5
+ numpy>=1.24.0
6
+ boto3
7
+ botocore
8
+ onnxruntime
9
+ huggingface_hub>=0.16.0
10
+ Pillow>=9.0.0
util.py ADDED
@@ -0,0 +1,600 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import os
3
+ import sys
4
+ import cv2
5
+ import json
6
+ import random
7
+ import time
8
+ import datetime
9
+ import requests
10
+ import func_timeout
11
+ import numpy as np
12
+ import gradio as gr
13
+ import boto3
14
+ import tempfile
15
+ import io
16
+ import uuid
17
+ from botocore.client import Config
18
+ from PIL import Image
19
+
20
+
21
+ # TOKEN = os.environ['TOKEN']
22
+ # APIKEY = os.environ['APIKEY']
23
+ # UKAPIURL = os.environ['UKAPIURL']
24
+
25
+ OneKey = os.environ['OneKey'].strip()
26
+ OneKey = OneKey.split("#")
27
+ TOKEN = OneKey[0]
28
+ APIKEY = OneKey[1]
29
+ UKAPIURL = OneKey[2]
30
+ LLMKEY = OneKey[3]
31
+ R2_ACCESS_KEY = OneKey[4]
32
+ R2_SECRET_KEY = OneKey[5]
33
+ R2_ENDPOINT = OneKey[6]
34
+ OPENROUTER_API_KEY = OneKey[7]
35
+
36
+
37
+ # tmpFolder is no longer needed since we upload directly from memory
38
+ # tmpFolder = "tmp"
39
+ # os.makedirs(tmpFolder, exist_ok=True)
40
+
41
+
42
+ class R2Api:
43
+
44
+ def __init__(self, session=None):
45
+ super().__init__()
46
+ self.R2_BUCKET = "omni-creator"
47
+ self.domain = "https://www.omnicreator.net/"
48
+ self.R2_ACCESS_KEY = R2_ACCESS_KEY
49
+ self.R2_SECRET_KEY = R2_SECRET_KEY
50
+ self.R2_ENDPOINT = R2_ENDPOINT
51
+
52
+ self.client = boto3.client(
53
+ "s3",
54
+ endpoint_url=self.R2_ENDPOINT,
55
+ aws_access_key_id=self.R2_ACCESS_KEY,
56
+ aws_secret_access_key=self.R2_SECRET_KEY,
57
+ config=Config(signature_version="s3v4")
58
+ )
59
+
60
+ self.session = requests.Session() if session is None else session
61
+
62
+ def upload_from_memory(self, image_data, filename, content_type='image/jpeg'):
63
+ """
64
+ Upload image data directly from memory to R2
65
+
66
+ Args:
67
+ image_data (bytes): Image data in bytes
68
+ filename (str): Filename for the uploaded file
69
+ content_type (str): MIME type of the image
70
+
71
+ Returns:
72
+ str: URL of the uploaded file
73
+ """
74
+ t1 = time.time()
75
+ headers = {"Content-Type": content_type}
76
+
77
+ cloud_path = f"QwenImageEdit/Uploads/{str(datetime.date.today())}/{filename}"
78
+ url = self.client.generate_presigned_url(
79
+ "put_object",
80
+ Params={"Bucket": self.R2_BUCKET, "Key": cloud_path, "ContentType": content_type},
81
+ ExpiresIn=604800
82
+ )
83
+
84
+ retry_count = 0
85
+ while retry_count < 3:
86
+ try:
87
+ response = self.session.put(url, data=image_data, headers=headers, timeout=15)
88
+ if response.status_code == 200:
89
+ break
90
+ else:
91
+ print(f"⚠️ Upload failed with status code: {response.status_code}")
92
+ retry_count += 1
93
+ except (requests.exceptions.Timeout, requests.exceptions.RequestException) as e:
94
+ print(f"⚠️ Upload retry {retry_count + 1}/3 failed: {e}")
95
+ retry_count += 1
96
+ if retry_count == 3:
97
+ raise Exception(f'Failed to upload file to R2 after 3 retries! Last error: {str(e)}')
98
+ time.sleep(1) # 等待1秒后重试
99
+ continue
100
+ print("upload_from_memory time is ====>", time.time() - t1)
101
+ return f"{self.domain}{cloud_path}"
102
+
103
+ def upload_file(self, local_path, cloud_path):
104
+ t1 = time.time()
105
+ head_dict = {
106
+ 'jpg': 'image/jpeg',
107
+ 'jpeg': 'image/jpeg',
108
+ 'png': 'image/png',
109
+ 'gif': 'image/gif',
110
+ 'bmp': 'image/bmp',
111
+ 'webp': 'image/webp',
112
+ 'ico': 'image/x-icon'
113
+ }
114
+ ftype = os.path.basename(local_path).split(".")[-1].lower()
115
+ ctype = head_dict.get(ftype, 'application/octet-stream')
116
+ headers = {"Content-Type": ctype}
117
+
118
+
119
+ cloud_path = f"QwenImageEdit/Uploads/{str(datetime.date.today())}/{os.path.basename(local_path)}"
120
+ url = self.client.generate_presigned_url(
121
+ "put_object",
122
+ Params={"Bucket": self.R2_BUCKET, "Key": cloud_path, "ContentType": ctype},
123
+ ExpiresIn=604800
124
+ )
125
+
126
+ retry_count = 0
127
+ while retry_count < 3:
128
+ try:
129
+ with open(local_path, 'rb') as f:
130
+ self.session.put(url, data=f.read(), headers=headers, timeout=8)
131
+ break
132
+ except (requests.exceptions.Timeout, requests.exceptions.RequestException):
133
+ retry_count += 1
134
+ if retry_count == 3:
135
+ raise Exception('Failed to upload file to R2 after 3 retries!')
136
+ continue
137
+ print("upload_file time is ====>", time.time() - t1)
138
+ return f"{self.domain}{cloud_path}"
139
+
140
+ def upload_user_img_r2(clientIp, timeId, pil_image):
141
+ """
142
+ Upload PIL Image directly to R2 without saving to local file
143
+
144
+ Args:
145
+ clientIp (str): Client IP address
146
+ timeId (int): Timestamp
147
+ pil_image (PIL.Image): PIL Image object
148
+
149
+ Returns:
150
+ str: Uploaded URL
151
+ """
152
+ # Generate unique filename using UUID to prevent file conflicts in concurrent environment
153
+ unique_id = str(uuid.uuid4())
154
+ fileName = f"user_img_{unique_id}_{timeId}.jpg"
155
+
156
+ # Convert PIL Image to bytes
157
+ img_buffer = io.BytesIO()
158
+ if pil_image.mode != 'RGB':
159
+ pil_image = pil_image.convert('RGB')
160
+ pil_image.save(img_buffer, format='JPEG', quality=95)
161
+ img_data = img_buffer.getvalue()
162
+
163
+ # Upload directly from memory
164
+ res = R2Api().upload_from_memory(img_data, fileName, 'image/jpeg')
165
+ return res
166
+
167
+
168
+
169
+ def create_mask_from_layers(base_image, layers):
170
+ """
171
+ Create mask image from ImageEditor layers
172
+
173
+ Args:
174
+ base_image (PIL.Image): Original image
175
+ layers (list): ImageEditor layer data
176
+
177
+ Returns:
178
+ PIL.Image: Black and white mask image
179
+ """
180
+ from PIL import Image, ImageDraw
181
+ import numpy as np
182
+
183
+ # Create blank mask with same size as original image
184
+ mask = Image.new('L', base_image.size, 0) # 'L' mode is grayscale, 0 is black
185
+
186
+ if not layers:
187
+ return mask
188
+
189
+ # Iterate through all layers, set drawn areas to white
190
+ for layer in layers:
191
+ if layer is not None:
192
+ # Convert layer to numpy array
193
+ layer_array = np.array(layer)
194
+
195
+ # Check layer format
196
+ if len(layer_array.shape) == 3: # RGB/RGBA format
197
+ # If RGBA, check alpha channel
198
+ if layer_array.shape[2] == 4:
199
+ # Use alpha channel as mask
200
+ alpha_channel = layer_array[:, :, 3]
201
+ # Set non-transparent areas (alpha > 0) to white
202
+ mask_array = np.where(alpha_channel > 0, 255, 0).astype(np.uint8)
203
+ else:
204
+ # RGB format, check if not pure black (0,0,0)
205
+ # Assume drawn areas are non-black
206
+ non_black = np.any(layer_array > 0, axis=2)
207
+ mask_array = np.where(non_black, 255, 0).astype(np.uint8)
208
+ elif len(layer_array.shape) == 2: # Grayscale
209
+ # Use grayscale values directly, set non-zero areas to white
210
+ mask_array = np.where(layer_array > 0, 255, 0).astype(np.uint8)
211
+ else:
212
+ continue
213
+
214
+ # Convert mask_array to PIL image and merge into total mask
215
+ layer_mask = Image.fromarray(mask_array, mode='L')
216
+ # Resize to match original image
217
+ if layer_mask.size != base_image.size:
218
+ layer_mask = layer_mask.resize(base_image.size, Image.LANCZOS)
219
+
220
+ # Merge masks (use maximum value to ensure all drawn areas are included)
221
+ mask_array_current = np.array(mask)
222
+ layer_mask_array = np.array(layer_mask)
223
+ combined_mask_array = np.maximum(mask_array_current, layer_mask_array)
224
+ mask = Image.fromarray(combined_mask_array, mode='L')
225
+
226
+ return mask
227
+
228
+
229
+ def upload_mask_image_r2(client_ip, time_id, mask_image):
230
+ """
231
+ Upload mask image to R2 directly from memory
232
+
233
+ Args:
234
+ client_ip (str): Client IP
235
+ time_id (int): Timestamp
236
+ mask_image (PIL.Image): Mask image
237
+
238
+ Returns:
239
+ str: Uploaded URL
240
+ """
241
+ # Generate unique filename using UUID to prevent file conflicts in concurrent environment
242
+ unique_id = str(uuid.uuid4())
243
+ file_name = f"mask_img_{unique_id}_{time_id}.png"
244
+
245
+ try:
246
+ # Convert mask image to bytes
247
+ img_buffer = io.BytesIO()
248
+ mask_image.save(img_buffer, format='PNG')
249
+ img_data = img_buffer.getvalue()
250
+
251
+ # Upload directly from memory
252
+ res = R2Api().upload_from_memory(img_data, file_name, 'image/png')
253
+
254
+ return res
255
+ except Exception as e:
256
+ print(f"Failed to upload mask image: {e}")
257
+ return None
258
+
259
+
260
+
261
+ def submit_image_edit_task(user_image_url, prompt, task_type="80", mask_image_url=""):
262
+ """
263
+ Submit image editing task with improved error handling using API v2
264
+ """
265
+ headers = {
266
+ 'Content-Type': 'application/json',
267
+ 'Authorization': f'Bearer {APIKEY}'
268
+ }
269
+
270
+ data = {
271
+ "user_image": user_image_url,
272
+ "user_mask": mask_image_url,
273
+ "type": task_type,
274
+ "text": prompt,
275
+ "user_uuid": APIKEY,
276
+ "priority": 0,
277
+ "secret_key": "219ngu"
278
+ }
279
+
280
+ retry_count = 0
281
+ max_retries = 3
282
+
283
+ while retry_count < max_retries:
284
+ try:
285
+ response = requests.post(
286
+ f'{UKAPIURL}/public_image_edit_v2',
287
+ headers=headers,
288
+ json=data,
289
+ timeout=30 # 增加超时时间
290
+ )
291
+
292
+ if response.status_code == 200:
293
+ result = response.json()
294
+ if result.get('code') == 0:
295
+ return result['data']['task_id'], None
296
+ else:
297
+ return None, f"API Error: {result.get('message', 'Unknown error')}"
298
+ elif response.status_code in [502, 503, 504]: # 服务器错误,可以重试
299
+ retry_count += 1
300
+ if retry_count < max_retries:
301
+ print(f"⚠️ Server error {response.status_code}, retrying {retry_count}/{max_retries}")
302
+ time.sleep(2) # 等待2秒后重试
303
+ continue
304
+ else:
305
+ return None, f"HTTP Error after {max_retries} retries: {response.status_code}"
306
+ else:
307
+ return None, f"HTTP Error: {response.status_code}"
308
+
309
+ except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
310
+ retry_count += 1
311
+ if retry_count < max_retries:
312
+ print(f"⚠️ Network error, retrying {retry_count}/{max_retries}: {e}")
313
+ time.sleep(2)
314
+ continue
315
+ else:
316
+ return None, f"Network error after {max_retries} retries: {str(e)}"
317
+ except Exception as e:
318
+ return None, f"Request Exception: {str(e)}"
319
+
320
+ return None, f"Failed after {max_retries} retries"
321
+
322
+
323
+ def check_task_status(task_id):
324
+ """
325
+ Query task status with improved error handling using API v2
326
+ """
327
+ headers = {
328
+ 'Content-Type': 'application/json',
329
+ 'Authorization': f'Bearer {APIKEY}'
330
+ }
331
+
332
+ data = {
333
+ "task_id": task_id
334
+ }
335
+
336
+ retry_count = 0
337
+ max_retries = 2 # 状态查询重试次数少一些
338
+
339
+ while retry_count < max_retries:
340
+ try:
341
+ response = requests.post(
342
+ f'{UKAPIURL}/status_image_edit_v2',
343
+ headers=headers,
344
+ json=data,
345
+ timeout=15 # 状态查询超时时间短一些
346
+ )
347
+
348
+ if response.status_code == 200:
349
+ result = response.json()
350
+ if result.get('code') == 0:
351
+ task_data = result['data']
352
+ return task_data['status'], task_data.get('image_url'), task_data
353
+ else:
354
+ return 'error', None, result.get('message', 'Unknown error')
355
+ elif response.status_code in [502, 503, 504]: # 服务器错误,可以重试
356
+ retry_count += 1
357
+ if retry_count < max_retries:
358
+ print(f"⚠️ Status check server error {response.status_code}, retrying {retry_count}/{max_retries}")
359
+ time.sleep(1) # 状态查询重试间隔短一些
360
+ continue
361
+ else:
362
+ return 'error', None, f"HTTP Error after {max_retries} retries: {response.status_code}"
363
+ else:
364
+ return 'error', None, f"HTTP Error: {response.status_code}"
365
+
366
+ except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
367
+ retry_count += 1
368
+ if retry_count < max_retries:
369
+ print(f"⚠️ Status check network error, retrying {retry_count}/{max_retries}: {e}")
370
+ time.sleep(1)
371
+ continue
372
+ else:
373
+ return 'error', None, f"Network error after {max_retries} retries: {str(e)}"
374
+ except Exception as e:
375
+ return 'error', None, f"Request Exception: {str(e)}"
376
+
377
+ return 'error', None, f"Failed after {max_retries} retries"
378
+
379
+
380
+ def process_image_edit(img_input, prompt, progress_callback=None):
381
+ """
382
+ Complete process for image editing
383
+
384
+ Args:
385
+ img_input: Can be file path (str) or PIL Image object
386
+ prompt: Editing instructions
387
+ progress_callback: Progress callback function
388
+ """
389
+ try:
390
+ # Generate client IP and timestamp
391
+ client_ip = "127.0.0.1" # Default IP
392
+ time_id = int(time.time())
393
+
394
+ # Process input image - supports PIL Image and file path
395
+ if hasattr(img_input, 'save'): # PIL Image object
396
+ pil_image = img_input
397
+ print(f"💾 Using PIL Image directly from memory")
398
+ else:
399
+ # Load from file path
400
+ pil_image = Image.open(img_input)
401
+ print(f"📁 Loaded image from file: {img_input}")
402
+
403
+ if progress_callback:
404
+ progress_callback("uploading image...")
405
+
406
+ # Upload user image directly from memory
407
+ uploaded_url = upload_user_img_r2(client_ip, time_id, pil_image)
408
+ if not uploaded_url:
409
+ return None, "image upload failed", None
410
+
411
+ # Extract actual image URL from upload URL
412
+ if "?" in uploaded_url:
413
+ uploaded_url = uploaded_url.split("?")[0]
414
+
415
+ if progress_callback:
416
+ progress_callback("submitting edit task...")
417
+
418
+ # Submit image editing task
419
+ task_id, error = submit_image_edit_task(uploaded_url, prompt)
420
+ if error:
421
+ return None, error, None
422
+
423
+ if progress_callback:
424
+ progress_callback(f"task submitted, ID: {task_id}, processing...")
425
+
426
+ # Wait for task completion
427
+ max_attempts = 60 # Wait up to 10 minutes
428
+ task_uuid = None
429
+ for attempt in range(max_attempts):
430
+ status, output_url, task_data = check_task_status(task_id)
431
+
432
+ # Extract task_uuid from task_data
433
+ if task_data and isinstance(task_data, dict):
434
+ task_uuid = task_data.get('uuid', None)
435
+
436
+ if status == 'completed':
437
+ if output_url:
438
+ return output_url, "image edit completed", task_uuid
439
+ else:
440
+ return None, "Task completed but no result image returned", task_uuid
441
+ elif status == 'error' or status == 'failed':
442
+ return None, f"task processing failed: {task_data}", task_uuid
443
+ elif status in ['queued', 'processing', 'running', 'created', 'working']:
444
+ if progress_callback:
445
+ progress_callback(f"task processing... (status: {status})")
446
+ time.sleep(1)
447
+ else:
448
+ if progress_callback:
449
+ progress_callback(f"unknown status: {status}")
450
+ time.sleep(1)
451
+
452
+ return None, "task processing timeout", task_uuid
453
+
454
+ except Exception as e:
455
+ return None, f"error occurred during processing: {str(e)}", None
456
+
457
+
458
+ def process_local_image_edit(base_image, layers, prompt, progress_callback=None):
459
+ """
460
+ 处理局部图片编辑的完整流程
461
+
462
+ Args:
463
+ base_image (PIL.Image): 原始图片
464
+ layers (list): ImageEditor的层数据
465
+ prompt (str): 编辑指令
466
+ progress_callback: 进度回调函数
467
+ """
468
+ try:
469
+ # Generate client IP and timestamp
470
+ client_ip = "127.0.0.1" # Default IP
471
+ time_id = int(time.time())
472
+
473
+ if progress_callback:
474
+ progress_callback("creating mask image...")
475
+
476
+ # 从layers创建mask图片
477
+ mask_image = create_mask_from_layers(base_image, layers)
478
+
479
+ # 检查mask是否有内容
480
+ mask_array = np.array(mask_image)
481
+ if np.max(mask_array) == 0:
482
+ return None, "please draw mask", None
483
+
484
+ print(f"📝 创建mask图片成功,绘制区域像素数: {np.sum(mask_array > 0)}")
485
+
486
+ if progress_callback:
487
+ progress_callback("uploading original image...")
488
+
489
+ # 直接从内存上传原始图片
490
+ uploaded_url = upload_user_img_r2(client_ip, time_id, base_image)
491
+ if not uploaded_url:
492
+ return None, "original image upload failed", None
493
+
494
+ # 从上传 URL 中提取实际的图片 URL
495
+ if "?" in uploaded_url:
496
+ uploaded_url = uploaded_url.split("?")[0]
497
+
498
+ if progress_callback:
499
+ progress_callback("uploading mask image...")
500
+
501
+ # 直接从内存上传mask图片
502
+ mask_url = upload_mask_image_r2(client_ip, time_id, mask_image)
503
+ if not mask_url:
504
+ return None, "mask image upload failed", None
505
+
506
+ # 从上传 URL 中提取实际的图片 URL
507
+ if "?" in mask_url:
508
+ mask_url = mask_url.split("?")[0]
509
+
510
+ print(f"📤 图片上传成功:")
511
+ print(f" 原始图片: {uploaded_url}")
512
+ print(f" Mask图片: {mask_url}")
513
+
514
+ if progress_callback:
515
+ progress_callback("submitting local edit task...")
516
+
517
+ # 提交局部图片编辑任务 (task_type=81)
518
+ task_id, error = submit_image_edit_task(uploaded_url, prompt, task_type="81", mask_image_url=mask_url)
519
+ if error:
520
+ return None, error, None
521
+
522
+ if progress_callback:
523
+ progress_callback(f"task submitted, ID: {task_id}, processing...")
524
+
525
+ print(f"🚀 局部编辑任务已提交,任务ID: {task_id}")
526
+
527
+ # Wait for task completion
528
+ max_attempts = 60 # Wait up to 10 minutes
529
+ task_uuid = None
530
+ for attempt in range(max_attempts):
531
+ status, output_url, task_data = check_task_status(task_id)
532
+
533
+ # Extract task_uuid from task_data
534
+ if task_data and isinstance(task_data, dict):
535
+ task_uuid = task_data.get('uuid', None)
536
+
537
+ if status == 'completed':
538
+ if output_url:
539
+ print(f"✅ 局部编辑任务完成,结果: {output_url}")
540
+ return output_url, "local image edit completed", task_uuid
541
+ else:
542
+ return None, "task completed but no result image returned", task_uuid
543
+ elif status == 'error' or status == 'failed':
544
+ return None, f"task processing failed: {task_data}", task_uuid
545
+ elif status in ['queued', 'processing', 'running', 'created', 'working']:
546
+ if progress_callback:
547
+ progress_callback(f"processing... (status: {status})")
548
+ time.sleep(1) # Wait 1 second before retry
549
+ else:
550
+ if progress_callback:
551
+ progress_callback(f"unknown status: {status}")
552
+ time.sleep(1)
553
+
554
+ return None, "task processing timeout", task_uuid
555
+
556
+ except Exception as e:
557
+ print(f"❌ 局部编辑处理异常: {str(e)}")
558
+ return None, f"error occurred during processing: {str(e)}", None
559
+
560
+
561
+ def download_and_check_result_nsfw(image_url, nsfw_detector=None):
562
+ """
563
+ 下载结果图片并进行NSFW检测
564
+
565
+ Args:
566
+ image_url (str): 结果图片URL
567
+ nsfw_detector: NSFW检测器实例
568
+
569
+ Returns:
570
+ tuple: (is_nsfw, error_message)
571
+ """
572
+ if nsfw_detector is None:
573
+ return False, None
574
+
575
+ try:
576
+ # 下载图片
577
+ response = requests.get(image_url, timeout=30)
578
+ if response.status_code != 200:
579
+ return False, f"Failed to download result image: HTTP {response.status_code}"
580
+
581
+ # 将图片数据转换为PIL Image
582
+ image_data = io.BytesIO(response.content)
583
+ result_image = Image.open(image_data)
584
+
585
+ # 进行NSFW检测
586
+ nsfw_result = nsfw_detector.predict_pil_label_only(result_image)
587
+
588
+ is_nsfw = nsfw_result.lower() == "nsfw"
589
+ print(f"🔍 结果图片NSFW检测: {'❌❌❌ ' + nsfw_result if is_nsfw else '✅✅✅ ' + nsfw_result}")
590
+
591
+ return is_nsfw, None
592
+
593
+ except Exception as e:
594
+ print(f"⚠️ 结果图片NSFW检测失败: {e}")
595
+ return False, f"Failed to check result image: {str(e)}"
596
+
597
+
598
+ if __name__ == "__main__":
599
+
600
+ pass