selfit-camera commited on
Commit
647c33c
·
1 Parent(s): 3187e20
.DS_Store ADDED
Binary file (8.2 kB). View file
 
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ *.jpg
2
+ *.png
3
+ hf_cache/
4
+ models/
5
+ __pycache__/
app.py ADDED
@@ -0,0 +1,1401 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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_multi_image_edit, process_local_image_edit, download_and_check_result_nsfw
8
+ from nfsw import NSFWDetector
9
+
10
+ # Configuration parameters
11
+ FREE_TRY_N = 20 # Free phase: first 20 tries without restrictions
12
+ SLOW_TRY_N = 30 # Slow phase start: 30 tries
13
+ SLOW2_TRY_N = 40 # Slow phase start: 30 tries
14
+ RATE_LIMIT_60 = 60 # Full restriction: blocked after 60 tries
15
+
16
+ # Time window configuration (minutes)
17
+ PHASE_1_WINDOW = 5 # 20-30 tries: 3 minutes
18
+ PHASE_2_WINDOW = 8 # 30-40 tries: 6 minutes
19
+ PHASE_3_WINDOW = 15 # 40-60 tries: 10 minutes
20
+ MAX_IMAGES_PER_WINDOW = 2 # Max images per time window
21
+
22
+ IP_Dict = {}
23
+ # IP generation statistics and time window tracking
24
+ IP_Generation_Count = {} # Record total generation count for each IP
25
+ IP_Rate_Limit_Track = {} # Record generation count and timestamp in current time window for each IP
26
+
27
+ def get_ip_generation_count(client_ip):
28
+ """
29
+ Get IP generation count
30
+ """
31
+ if client_ip not in IP_Generation_Count:
32
+ IP_Generation_Count[client_ip] = 0
33
+ return IP_Generation_Count[client_ip]
34
+
35
+ def increment_ip_generation_count(client_ip):
36
+ """
37
+ Increment IP generation count
38
+ """
39
+ if client_ip not in IP_Generation_Count:
40
+ IP_Generation_Count[client_ip] = 0
41
+ IP_Generation_Count[client_ip] += 1
42
+ return IP_Generation_Count[client_ip]
43
+
44
+ def get_ip_phase(client_ip):
45
+ """
46
+ Get current phase for IP
47
+
48
+ Returns:
49
+ str: 'free', 'rate_limit_1', 'rate_limit_2', 'rate_limit_3', 'blocked'
50
+ """
51
+ count = get_ip_generation_count(client_ip)
52
+
53
+ if count < FREE_TRY_N: # 0-19 tries
54
+ return 'free'
55
+ elif count < SLOW_TRY_N: # 20-29 tries
56
+ return 'rate_limit_1' # NSFW blur + 3 minutes 2 images
57
+ elif count < SLOW2_TRY_N: # 30-39 tries
58
+ return 'rate_limit_2' # NSFW blur + 6 minutes 2 images
59
+ elif count < RATE_LIMIT_60: # 40-59 tries
60
+ return 'rate_limit_3' # NSFW blur + 10 minutes 2 images
61
+ else: # 60+ tries
62
+ return 'blocked' # Generation blocked
63
+
64
+ def check_rate_limit_for_phase(client_ip, phase):
65
+ """
66
+ Check rate limit for specific phase
67
+
68
+ Returns:
69
+ tuple: (is_limited, wait_time_minutes, current_count)
70
+ """
71
+ if phase not in ['rate_limit_1', 'rate_limit_2', 'rate_limit_3']:
72
+ return False, 0, 0
73
+
74
+ # Determine time window
75
+ if phase == 'rate_limit_1':
76
+ window_minutes = PHASE_1_WINDOW # 3 minutes
77
+ elif phase == 'rate_limit_2':
78
+ window_minutes = PHASE_2_WINDOW # 6 minutes
79
+ else: # rate_limit_3
80
+ window_minutes = PHASE_3_WINDOW # 10 minutes
81
+
82
+ current_time = time.time()
83
+ window_key = f"{client_ip}_{phase}"
84
+
85
+ # Clean expired records
86
+ if window_key in IP_Rate_Limit_Track:
87
+ track_data = IP_Rate_Limit_Track[window_key]
88
+ # Check if within current time window
89
+ if current_time - track_data['start_time'] > window_minutes * 60:
90
+ # Time window expired, reset
91
+ IP_Rate_Limit_Track[window_key] = {
92
+ 'count': 0,
93
+ 'start_time': current_time,
94
+ 'last_generation': current_time
95
+ }
96
+ else:
97
+ # Initialize
98
+ IP_Rate_Limit_Track[window_key] = {
99
+ 'count': 0,
100
+ 'start_time': current_time,
101
+ 'last_generation': current_time
102
+ }
103
+
104
+ track_data = IP_Rate_Limit_Track[window_key]
105
+
106
+ # Check if exceeded limit
107
+ if track_data['count'] >= MAX_IMAGES_PER_WINDOW:
108
+ # Calculate remaining wait time
109
+ elapsed = current_time - track_data['start_time']
110
+ wait_time = (window_minutes * 60) - elapsed
111
+ wait_minutes = max(0, wait_time / 60)
112
+ return True, wait_minutes, track_data['count']
113
+
114
+ return False, 0, track_data['count']
115
+
116
+ def record_generation_attempt(client_ip, phase):
117
+ """
118
+ Record generation attempt
119
+ """
120
+ # Increment total count
121
+ increment_ip_generation_count(client_ip)
122
+
123
+ # Record time window count
124
+ if phase in ['rate_limit_1', 'rate_limit_2', 'rate_limit_3']:
125
+ window_key = f"{client_ip}_{phase}"
126
+ current_time = time.time()
127
+
128
+ if window_key in IP_Rate_Limit_Track:
129
+ IP_Rate_Limit_Track[window_key]['count'] += 1
130
+ IP_Rate_Limit_Track[window_key]['last_generation'] = current_time
131
+ else:
132
+ IP_Rate_Limit_Track[window_key] = {
133
+ 'count': 1,
134
+ 'start_time': current_time,
135
+ 'last_generation': current_time
136
+ }
137
+
138
+ def apply_gaussian_blur_to_image_url(image_url, blur_strength=50):
139
+ """
140
+ Apply Gaussian blur to image URL
141
+
142
+ Args:
143
+ image_url (str): Original image URL
144
+ blur_strength (int): Blur strength, default 50 (heavy blur)
145
+
146
+ Returns:
147
+ PIL.Image: Blurred PIL Image object
148
+ """
149
+ try:
150
+ import requests
151
+ from PIL import Image, ImageFilter
152
+ import io
153
+
154
+ # Download image
155
+ response = requests.get(image_url, timeout=30)
156
+ if response.status_code != 200:
157
+ return None
158
+
159
+ # Convert to PIL Image
160
+ image_data = io.BytesIO(response.content)
161
+ image = Image.open(image_data)
162
+
163
+ # Apply heavy Gaussian blur
164
+ blurred_image = image.filter(ImageFilter.GaussianBlur(radius=blur_strength))
165
+
166
+ return blurred_image
167
+
168
+ except Exception as e:
169
+ print(f"⚠️ Failed to apply Gaussian blur: {e}")
170
+ return None
171
+
172
+ # Initialize NSFW detector (download from Hugging Face)
173
+ try:
174
+ nsfw_detector = NSFWDetector() # Auto download falconsai_yolov9_nsfw_model_quantized.pt from Hugging Face
175
+ print("✅ NSFW detector initialized successfully")
176
+ except Exception as e:
177
+ print(f"❌ NSFW detector initialization failed: {e}")
178
+ nsfw_detector = None
179
+
180
+ def edit_image_interface(input_image1, input_image2, input_image3, prompt, aspect_ratio, request: gr.Request, progress=gr.Progress()):
181
+ """
182
+ Interface function for processing multi-image editing with phase-based limitations
183
+ """
184
+ try:
185
+ # Extract user IP
186
+ client_ip = request.client.host
187
+ x_forwarded_for = dict(request.headers).get('x-forwarded-for')
188
+ if x_forwarded_for:
189
+ client_ip = x_forwarded_for
190
+ if client_ip not in IP_Dict:
191
+ IP_Dict[client_ip] = 0
192
+ IP_Dict[client_ip] += 1
193
+
194
+ # Validate input images
195
+ input_images = [input_image1, input_image2, input_image3]
196
+ valid_images = [img for img in input_images if img is not None]
197
+
198
+ if len(valid_images) < 2:
199
+ return None, "Please upload at least 2 images", gr.update(visible=False)
200
+
201
+ if len(valid_images) > 3:
202
+ return None, "Maximum 3 images allowed", gr.update(visible=False)
203
+
204
+ if not prompt or prompt.strip() == "":
205
+ return None, "Please enter editing prompt", gr.update(visible=False)
206
+
207
+ # Check if prompt length is greater than 3 characters
208
+ if len(prompt.strip()) <= 3:
209
+ return None, "❌ Editing prompt must be more than 3 characters", gr.update(visible=False)
210
+ except Exception as e:
211
+ print(f"⚠️ Request preprocessing error: {e}")
212
+ return None, "❌ Request processing error", gr.update(visible=False)
213
+
214
+ # Get user current phase
215
+ current_phase = get_ip_phase(client_ip)
216
+ current_count = get_ip_generation_count(client_ip)
217
+
218
+ print(f"📊 User phase info - IP: {client_ip}, current phase: {current_phase}, generation count: {current_count}")
219
+
220
+ # Check if completely blocked
221
+ if current_phase == 'blocked':
222
+ # Generate blocked limit button
223
+ blocked_button_html = f"""
224
+ <div style='display: flex; justify-content: center; gap: 15px; margin: 10px 0 5px 0; padding: 0px;'>
225
+ <a href='https://omnicreator.net/multi-image-edit#generator' target='_blank' style='
226
+ display: inline-flex;
227
+ align-items: center;
228
+ justify-content: center;
229
+ padding: 16px 32px;
230
+ background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
231
+ color: white;
232
+ text-decoration: none;
233
+ border-radius: 12px;
234
+ font-weight: 600;
235
+ font-size: 16px;
236
+ text-align: center;
237
+ min-width: 200px;
238
+ box-shadow: 0 4px 15px rgba(231, 76, 60, 0.4);
239
+ transition: all 0.3s ease;
240
+ border: none;
241
+ '>🚀 Unlimited Generation</a>
242
+ </div>
243
+ """
244
+ return None, f"❌ You have reached Hugging Face's free generation limit. Please visit https://omnicreator.net/multi-image-edit#generator for unlimited generation", gr.update(value=blocked_button_html, visible=True)
245
+
246
+ # Check rate limit (applies to rate_limit phases)
247
+ if current_phase in ['rate_limit_1', 'rate_limit_2', 'rate_limit_3']:
248
+ is_limited, wait_minutes, window_count = check_rate_limit_for_phase(client_ip, current_phase)
249
+ if is_limited:
250
+ wait_minutes_int = int(wait_minutes) + 1
251
+ # Generate rate limit button
252
+ rate_limit_button_html = f"""
253
+ <div style='display: flex; justify-content: center; gap: 15px; margin: 10px 0 5px 0; padding: 0px;'>
254
+ <a href='https://omnicreator.net/multi-image-edit#generator' target='_blank' style='
255
+ display: inline-flex;
256
+ align-items: center;
257
+ justify-content: center;
258
+ padding: 16px 32px;
259
+ background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);
260
+ color: white;
261
+ text-decoration: none;
262
+ border-radius: 12px;
263
+ font-weight: 600;
264
+ font-size: 16px;
265
+ text-align: center;
266
+ min-width: 200px;
267
+ box-shadow: 0 4px 15px rgba(243, 156, 18, 0.4);
268
+ transition: all 0.3s ease;
269
+ border: none;
270
+ '>⏰ Skip Wait - Unlimited Generation</a>
271
+ </div>
272
+ """
273
+ return None, f"❌ You have reached Hugging Face's free generation limit. Please visit https://omnicreator.net/multi-image-edit#generator for unlimited generation, or wait {wait_minutes_int} minutes before generating again", gr.update(value=rate_limit_button_html, visible=True)
274
+
275
+ # Parse aspect ratio to get width and height
276
+ width, height = 0, 0 # Default to auto sizing
277
+ if aspect_ratio and aspect_ratio != "Auto":
278
+ aspect_ratios = {
279
+ "16:9": (1364, 768),
280
+ "4:3": (1182, 887),
281
+ "1:1": (1024, 1024),
282
+ "3:4": (887, 1182),
283
+ "9:16": (768, 1364)
284
+ }
285
+ if aspect_ratio in aspect_ratios:
286
+ width, height = aspect_ratios[aspect_ratio]
287
+
288
+ # Handle NSFW detection based on phase
289
+ is_nsfw_task = False # Track if this task involves NSFW content
290
+
291
+ # Skip NSFW detection in free phase - check first image for NSFW
292
+ if current_phase != 'free' and nsfw_detector is not None and valid_images:
293
+ try:
294
+ nsfw_result = nsfw_detector.predict_pil_label_only(valid_images[0])
295
+
296
+ if nsfw_result.lower() == "nsfw":
297
+ is_nsfw_task = True
298
+ print(f"🔍 Input NSFW detected in {current_phase} phase: ❌❌❌ {nsfw_result} - IP: {client_ip} (will blur result)")
299
+ else:
300
+ print(f"🔍 Input NSFW check passed: ✅✅✅ {nsfw_result} - IP: {client_ip}")
301
+
302
+ except Exception as e:
303
+ print(f"⚠️ Input NSFW detection failed: {e}")
304
+ # Allow continuation when detection fails
305
+
306
+ result_url = None
307
+ status_message = ""
308
+
309
+ def progress_callback(message):
310
+ try:
311
+ nonlocal status_message
312
+ status_message = message
313
+ # Add error handling to prevent progress update failure
314
+ if progress is not None:
315
+ progress(0.5, desc=message)
316
+ except Exception as e:
317
+ print(f"⚠️ Progress update failed: {e}")
318
+
319
+ try:
320
+ # Record generation attempt (before actual generation to ensure correct count)
321
+ record_generation_attempt(client_ip, current_phase)
322
+ updated_count = get_ip_generation_count(client_ip)
323
+
324
+ print(f"✅ Multi-image processing started - IP: {client_ip}, phase: {current_phase}, total count: {updated_count}, images: {len(valid_images)}, size: {width}x{height}, prompt: {prompt.strip()}", flush=True)
325
+
326
+ # Call multi-image editing processing function
327
+ result_url, message, task_uuid = process_multi_image_edit(valid_images, prompt.strip(), width, height, progress_callback)
328
+
329
+ if result_url:
330
+ print(f"✅ Processing completed successfully - IP: {client_ip}, result_url: {result_url}, task_uuid: {task_uuid}", flush=True)
331
+
332
+ # Detect result image NSFW content (only in rate limit phases)
333
+ if nsfw_detector is not None and current_phase != 'free':
334
+ try:
335
+ if progress is not None:
336
+ progress(0.9, desc="Checking result image...")
337
+
338
+ is_nsfw, nsfw_error = download_and_check_result_nsfw(result_url, nsfw_detector)
339
+
340
+ if nsfw_error:
341
+ print(f"⚠️ Result image NSFW detection error - IP: {client_ip}, error: {nsfw_error}")
342
+ elif is_nsfw:
343
+ is_nsfw_task = True # Mark task as NSFW
344
+ print(f"🔍 Result image NSFW detected in {current_phase} phase: ❌❌❌ - IP: {client_ip} (will blur result)")
345
+ else:
346
+ print(f"🔍 Result image NSFW check passed: ✅✅✅ - IP: {client_ip}")
347
+
348
+ except Exception as e:
349
+ print(f"⚠️ Result image NSFW detection exception - IP: {client_ip}, error: {str(e)}")
350
+
351
+ # Apply blur if this is an NSFW task in rate limit phases
352
+ should_blur = False
353
+
354
+ if current_phase in ['rate_limit_1', 'rate_limit_2', 'rate_limit_3'] and is_nsfw_task:
355
+ should_blur = True
356
+
357
+ # Apply blur processing
358
+ if should_blur:
359
+ if progress is not None:
360
+ progress(0.95, desc="Applying content filter...")
361
+
362
+ blurred_image = apply_gaussian_blur_to_image_url(result_url)
363
+ if blurred_image is not None:
364
+ final_result = blurred_image # Return PIL Image object
365
+ final_message = f"⚠️ NSFW content detected, content filter applied. NSFW content is prohibited by Hugging Face, but you can generate unlimited content at our official website https://omnicreator.net/multi-image-edit#generator"
366
+ print(f"🔒 Applied Gaussian blur for NSFW content - IP: {client_ip}")
367
+ else:
368
+ # Blur failed, return original URL with warning
369
+ final_result = result_url
370
+ final_message = f"⚠️ NSFW content detected, but content filter failed. Please visit https://omnicreator.net/multi-image-edit#generator for better experience"
371
+
372
+ # Generate NSFW button for blurred content
373
+ nsfw_action_buttons_html = f"""
374
+ <div style='display: flex; justify-content: center; gap: 15px; margin: 10px 0 5px 0; padding: 0px;'>
375
+ <a href='https://omnicreator.net/multi-image-edit#generator' target='_blank' style='
376
+ display: inline-flex;
377
+ align-items: center;
378
+ justify-content: center;
379
+ padding: 16px 32px;
380
+ background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%);
381
+ color: white;
382
+ text-decoration: none;
383
+ border-radius: 12px;
384
+ font-weight: 600;
385
+ font-size: 16px;
386
+ text-align: center;
387
+ min-width: 200px;
388
+ box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4);
389
+ transition: all 0.3s ease;
390
+ border: none;
391
+ '>🔥 Unlimited NSFW Generation</a>
392
+ </div>
393
+ """
394
+ return final_result, final_message, gr.update(value=nsfw_action_buttons_html, visible=True)
395
+ else:
396
+ final_result = result_url
397
+ final_message = "✅ " + message
398
+
399
+ try:
400
+ if progress is not None:
401
+ progress(1.0, desc="Processing completed")
402
+ except Exception as e:
403
+ print(f"⚠️ Final progress update failed: {e}")
404
+
405
+ # Generate action buttons HTML like Trump AI Voice
406
+ action_buttons_html = ""
407
+ if task_uuid:
408
+ task_detail_url = f"https://omnicreator.net/my-creations/task/{task_uuid}"
409
+ action_buttons_html = f"""
410
+ <div style='display: flex; justify-content: center; gap: 15px; margin: 10px 0 5px 0; padding: 0px;'>
411
+ <a href='{task_detail_url}' target='_blank' style='
412
+ display: inline-flex;
413
+ align-items: center;
414
+ justify-content: center;
415
+ padding: 16px 32px;
416
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
417
+ color: white;
418
+ text-decoration: none;
419
+ border-radius: 12px;
420
+ font-weight: 600;
421
+ font-size: 16px;
422
+ text-align: center;
423
+ min-width: 160px;
424
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
425
+ transition: all 0.3s ease;
426
+ border: none;
427
+ '>🖼️ Download HD Image</a>
428
+ <a href='https://omnicreator.net/multi-image-edit#generator' target='_blank' style='
429
+ display: inline-flex;
430
+ align-items: center;
431
+ justify-content: center;
432
+ padding: 16px 32px;
433
+ background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
434
+ color: white;
435
+ text-decoration: none;
436
+ border-radius: 12px;
437
+ font-weight: 600;
438
+ font-size: 16px;
439
+ text-align: center;
440
+ min-width: 160px;
441
+ box-shadow: 0 4px 15px rgba(17, 153, 142, 0.4);
442
+ transition: all 0.3s ease;
443
+ border: none;
444
+ '>🚀 Unlimited Generation</a>
445
+ </div>
446
+ """
447
+
448
+ return final_result, final_message, gr.update(value=action_buttons_html, visible=True)
449
+ else:
450
+ print(f"❌ Processing failed - IP: {client_ip}, error: {message}", flush=True)
451
+ return None, "❌ " + message, gr.update(visible=False)
452
+
453
+ except Exception as e:
454
+ print(f"❌ Processing exception - IP: {client_ip}, error: {str(e)}")
455
+ return None, f"❌ Error occurred during processing: {str(e)}", gr.update(visible=False)
456
+
457
+ def local_edit_interface(image_dict, prompt, request: gr.Request, progress=gr.Progress()):
458
+ """
459
+ Handle local editing requests (with phase-based limitations)
460
+ """
461
+ try:
462
+ # Extract user IP
463
+ client_ip = request.client.host
464
+ x_forwarded_for = dict(request.headers).get('x-forwarded-for')
465
+ if x_forwarded_for:
466
+ client_ip = x_forwarded_for
467
+ if client_ip not in IP_Dict:
468
+ IP_Dict[client_ip] = 0
469
+ IP_Dict[client_ip] += 1
470
+
471
+ if image_dict is None:
472
+ return None, "Please upload an image and draw the area to edit", gr.update(visible=False)
473
+
474
+ # Check if background and layers exist
475
+ if "background" not in image_dict or "layers" not in image_dict:
476
+ return None, "Please draw the area to edit on the image", gr.update(visible=False)
477
+
478
+ base_image = image_dict["background"]
479
+ layers = image_dict["layers"]
480
+
481
+ if not layers:
482
+ return None, "Please draw the area to edit on the image", gr.update(visible=False)
483
+
484
+ if not prompt or prompt.strip() == "":
485
+ return None, "Please enter editing prompt", gr.update(visible=False)
486
+
487
+ # Check prompt length
488
+ if len(prompt.strip()) <= 3:
489
+ return None, "❌ Editing prompt must be more than 3 characters", gr.update(visible=False)
490
+ except Exception as e:
491
+ print(f"⚠️ Local edit request preprocessing error: {e}")
492
+ return None, "❌ Request processing error", gr.update(visible=False)
493
+
494
+ # Get user current phase
495
+ current_phase = get_ip_phase(client_ip)
496
+ current_count = get_ip_generation_count(client_ip)
497
+
498
+ print(f"📊 Local edit user phase info - IP: {client_ip}, current phase: {current_phase}, generation count: {current_count}")
499
+
500
+ # Check if completely blocked
501
+ if current_phase == 'blocked':
502
+ # Generate blocked limit button
503
+ blocked_button_html = f"""
504
+ <div style='display: flex; justify-content: center; gap: 15px; margin: 10px 0 5px 0; padding: 0px;'>
505
+ <a href='https://omnicreator.net/multi-image-edit#generator' target='_blank' style='
506
+ display: inline-flex;
507
+ align-items: center;
508
+ justify-content: center;
509
+ padding: 16px 32px;
510
+ background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
511
+ color: white;
512
+ text-decoration: none;
513
+ border-radius: 12px;
514
+ font-weight: 600;
515
+ font-size: 16px;
516
+ text-align: center;
517
+ min-width: 200px;
518
+ box-shadow: 0 4px 15px rgba(231, 76, 60, 0.4);
519
+ transition: all 0.3s ease;
520
+ border: none;
521
+ '>🚀 Unlimited Generation</a>
522
+ </div>
523
+ """
524
+ return None, f"❌ You have reached Hugging Face's free generation limit. Please visit https://omnicreator.net/multi-image-edit#generator for unlimited generation", gr.update(value=blocked_button_html, visible=True)
525
+
526
+ # Check rate limit (applies to rate_limit phases)
527
+ if current_phase in ['rate_limit_1', 'rate_limit_2', 'rate_limit_3']:
528
+ is_limited, wait_minutes, window_count = check_rate_limit_for_phase(client_ip, current_phase)
529
+ if is_limited:
530
+ wait_minutes_int = int(wait_minutes) + 1
531
+ # Generate rate limit button
532
+ rate_limit_button_html = f"""
533
+ <div style='display: flex; justify-content: center; gap: 15px; margin: 10px 0 5px 0; padding: 0px;'>
534
+ <a href='https://omnicreator.net/multi-image-edit#generator' target='_blank' style='
535
+ display: inline-flex;
536
+ align-items: center;
537
+ justify-content: center;
538
+ padding: 16px 32px;
539
+ background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);
540
+ color: white;
541
+ text-decoration: none;
542
+ border-radius: 12px;
543
+ font-weight: 600;
544
+ font-size: 16px;
545
+ text-align: center;
546
+ min-width: 200px;
547
+ box-shadow: 0 4px 15px rgba(243, 156, 18, 0.4);
548
+ transition: all 0.3s ease;
549
+ border: none;
550
+ '>⏰ Skip Wait - Unlimited Generation</a>
551
+ </div>
552
+ """
553
+ return None, f"❌ You have reached Hugging Face's free generation limit. Please visit https://omnicreator.net/multi-image-edit#generator for unlimited generation, or wait {wait_minutes_int} minutes before generating again", gr.update(value=rate_limit_button_html, visible=True)
554
+
555
+ # Handle NSFW detection based on phase
556
+ is_nsfw_task = False # Track if this task involves NSFW content
557
+
558
+ # Skip NSFW detection in free phase
559
+ if current_phase != 'free' and nsfw_detector is not None and base_image is not None:
560
+ try:
561
+ nsfw_result = nsfw_detector.predict_pil_label_only(base_image)
562
+
563
+ if nsfw_result.lower() == "nsfw":
564
+ is_nsfw_task = True
565
+ print(f"🔍 Local edit input NSFW detected in {current_phase} phase: ❌❌❌ {nsfw_result} - IP: {client_ip} (will blur result)")
566
+ else:
567
+ print(f"🔍 Local edit input NSFW check passed: ✅✅✅ {nsfw_result} - IP: {client_ip}")
568
+
569
+ except Exception as e:
570
+ print(f"⚠️ Local edit input NSFW detection failed: {e}")
571
+ # Allow continuation when detection fails
572
+
573
+ result_url = None
574
+ status_message = ""
575
+
576
+ def progress_callback(message):
577
+ try:
578
+ nonlocal status_message
579
+ status_message = message
580
+ # Add error handling to prevent progress update failure
581
+ if progress is not None:
582
+ progress(0.5, desc=message)
583
+ except Exception as e:
584
+ print(f"⚠️ Local edit progress update failed: {e}")
585
+
586
+ try:
587
+ # Record generation attempt (before actual generation to ensure correct count)
588
+ record_generation_attempt(client_ip, current_phase)
589
+ updated_count = get_ip_generation_count(client_ip)
590
+
591
+ print(f"✅ Local editing started - IP: {client_ip}, phase: {current_phase}, total count: {updated_count}, prompt: {prompt.strip()}", flush=True)
592
+
593
+ # Call local image editing processing function
594
+ result_url, message, task_uuid = process_local_image_edit(base_image, layers, prompt.strip(), progress_callback)
595
+
596
+ if result_url:
597
+ print(f"✅ Local editing completed successfully - IP: {client_ip}, result_url: {result_url}, task_uuid: {task_uuid}", flush=True)
598
+
599
+ # Detect result image NSFW content (only in rate limit phases)
600
+ if nsfw_detector is not None and current_phase != 'free':
601
+ try:
602
+ if progress is not None:
603
+ progress(0.9, desc="Checking result image...")
604
+
605
+ is_nsfw, nsfw_error = download_and_check_result_nsfw(result_url, nsfw_detector)
606
+
607
+ if nsfw_error:
608
+ print(f"⚠️ Local edit result image NSFW detection error - IP: {client_ip}, error: {nsfw_error}")
609
+ elif is_nsfw:
610
+ is_nsfw_task = True # Mark task as NSFW
611
+ print(f"🔍 Local edit result image NSFW detected in {current_phase} phase: ❌❌❌ - IP: {client_ip} (will blur result)")
612
+ else:
613
+ print(f"🔍 Local edit result image NSFW check passed: ✅✅✅ - IP: {client_ip}")
614
+
615
+ except Exception as e:
616
+ print(f"⚠️ Local edit result image NSFW detection exception - IP: {client_ip}, error: {str(e)}")
617
+
618
+ # Apply blur if this is an NSFW task in rate limit phases
619
+ should_blur = False
620
+
621
+ if current_phase in ['rate_limit_1', 'rate_limit_2', 'rate_limit_3'] and is_nsfw_task:
622
+ should_blur = True
623
+
624
+ # Apply blur processing
625
+ if should_blur:
626
+ if progress is not None:
627
+ progress(0.95, desc="Applying content filter...")
628
+
629
+ blurred_image = apply_gaussian_blur_to_image_url(result_url)
630
+ if blurred_image is not None:
631
+ final_result = blurred_image # Return PIL Image object
632
+ final_message = f"⚠️ NSFW content detected, content filter applied. NSFW content is prohibited by Hugging Face, but you can generate unlimited content at our official website https://omnicreator.net/multi-image-edit#generator"
633
+ print(f"🔒 Local edit applied Gaussian blur for NSFW content - IP: {client_ip}")
634
+ else:
635
+ # Blur failed, return original URL with warning
636
+ final_result = result_url
637
+ final_message = f"⚠️ NSFW content detected, but content filter failed. Please visit https://omnicreator.net/multi-image-edit#generator for better experience"
638
+
639
+ # Generate NSFW button for blurred content
640
+ nsfw_action_buttons_html = f"""
641
+ <div style='display: flex; justify-content: center; gap: 15px; margin: 10px 0 5px 0; padding: 0px;'>
642
+ <a href='https://omnicreator.net/multi-image-edit#generator' target='_blank' style='
643
+ display: inline-flex;
644
+ align-items: center;
645
+ justify-content: center;
646
+ padding: 16px 32px;
647
+ background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%);
648
+ color: white;
649
+ text-decoration: none;
650
+ border-radius: 12px;
651
+ font-weight: 600;
652
+ font-size: 16px;
653
+ text-align: center;
654
+ min-width: 200px;
655
+ box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4);
656
+ transition: all 0.3s ease;
657
+ border: none;
658
+ '>🔥 Unlimited NSFW Generation</a>
659
+ </div>
660
+ """
661
+ return final_result, final_message, gr.update(value=nsfw_action_buttons_html, visible=True)
662
+ else:
663
+ final_result = result_url
664
+ final_message = "✅ " + message
665
+
666
+ try:
667
+ if progress is not None:
668
+ progress(1.0, desc="Processing completed")
669
+ except Exception as e:
670
+ print(f"⚠️ Local edit final progress update failed: {e}")
671
+
672
+ # Generate action buttons HTML like Trump AI Voice
673
+ action_buttons_html = ""
674
+ if task_uuid:
675
+ task_detail_url = f"https://omnicreator.net/my-creations/task/{task_uuid}"
676
+ action_buttons_html = f"""
677
+ <div style='display: flex; justify-content: center; gap: 15px; margin: 10px 0 5px 0; padding: 0px;'>
678
+ <a href='{task_detail_url}' target='_blank' style='
679
+ display: inline-flex;
680
+ align-items: center;
681
+ justify-content: center;
682
+ padding: 16px 32px;
683
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
684
+ color: white;
685
+ text-decoration: none;
686
+ border-radius: 12px;
687
+ font-weight: 600;
688
+ font-size: 16px;
689
+ text-align: center;
690
+ min-width: 160px;
691
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
692
+ transition: all 0.3s ease;
693
+ border: none;
694
+ '>🖼️ Download HD Image</a>
695
+ <a href='https://omnicreator.net/multi-image-edit#generator' target='_blank' style='
696
+ display: inline-flex;
697
+ align-items: center;
698
+ justify-content: center;
699
+ padding: 16px 32px;
700
+ background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
701
+ color: white;
702
+ text-decoration: none;
703
+ border-radius: 12px;
704
+ font-weight: 600;
705
+ font-size: 16px;
706
+ text-align: center;
707
+ min-width: 160px;
708
+ box-shadow: 0 4px 15px rgba(17, 153, 142, 0.4);
709
+ transition: all 0.3s ease;
710
+ border: none;
711
+ '>🚀 Unlimited Generation</a>
712
+ </div>
713
+ """
714
+
715
+ return final_result, final_message, gr.update(value=action_buttons_html, visible=True)
716
+ else:
717
+ print(f"❌ Local editing processing failed - IP: {client_ip}, error: {message}", flush=True)
718
+ return None, "❌ " + message, gr.update(visible=False)
719
+
720
+ except Exception as e:
721
+ print(f"❌ Local editing exception - IP: {client_ip}, error: {str(e)}")
722
+ return None, f"❌ Error occurred during processing: {str(e)}", gr.update(visible=False)
723
+
724
+ # Create Gradio interface
725
+ def create_app():
726
+ with gr.Blocks(
727
+ title="AI Image Editor",
728
+ theme=gr.themes.Soft(),
729
+ css="""
730
+ .main-container {
731
+ max-width: 1200px;
732
+ margin: 0 auto;
733
+ }
734
+ .upload-area {
735
+ border: 2px dashed #ccc;
736
+ border-radius: 10px;
737
+ padding: 20px;
738
+ text-align: center;
739
+ }
740
+ .result-area {
741
+ margin-top: 20px;
742
+ padding: 20px;
743
+ border-radius: 10px;
744
+ background-color: #f8f9fa;
745
+ }
746
+ .use-as-input-btn {
747
+ margin-top: 10px;
748
+ width: 100%;
749
+ }
750
+ """,
751
+ # Improve concurrency performance configuration
752
+ head="""
753
+ <script>
754
+ // Reduce client-side state update frequency, avoid excessive SSE connections
755
+ if (window.gradio) {
756
+ window.gradio.update_frequency = 2000; // Update every 2 seconds
757
+ }
758
+ </script>
759
+ """
760
+ ) as app:
761
+
762
+ # Main title - styled like Trump AI Voice
763
+ gr.HTML("""
764
+ <div style="text-align: center; margin: 5px auto 0px auto; max-width: 800px;">
765
+ <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);">
766
+ 🎨 AI Multi-Image Editor
767
+ </h1>
768
+ </div>
769
+ """, padding=False)
770
+
771
+ # Powered by line below title - styled like Trump AI Voice
772
+ gr.HTML("""
773
+ <div style="text-align: center; margin: 0px auto -5px auto;">
774
+ <p style="margin: 0; font-size: 16px; color: #999; font-weight: 400;">
775
+ powered by <a href="https://omnicreator.net/multi-image-edit#generator" target="_blank" style="color: #667eea; text-decoration: none;">omnicreator.net</a>
776
+ </p>
777
+ </div>
778
+ """, padding=False)
779
+
780
+ with gr.Tabs():
781
+ # Multi-image editing tab
782
+ with gr.Tab("🖼️ Multi-Image Editing"):
783
+ with gr.Row():
784
+ with gr.Column(scale=1):
785
+ gr.Markdown("### 📸 Upload Images (2-3 images)")
786
+
787
+ # Multiple image inputs
788
+ with gr.Row():
789
+ input_image1 = gr.Image(
790
+ label="Image 1 *",
791
+ type="pil",
792
+ height=200,
793
+ elem_classes=["upload-area"]
794
+ )
795
+ input_image2 = gr.Image(
796
+ label="Image 2 *",
797
+ type="pil",
798
+ height=200,
799
+ elem_classes=["upload-area"]
800
+ )
801
+ input_image3 = gr.Image(
802
+ label="Image 3 (optional)",
803
+ type="pil",
804
+ height=200,
805
+ elem_classes=["upload-area"]
806
+ )
807
+
808
+ gr.Markdown("### 📐 Output Size")
809
+ aspect_ratio_selector = gr.Radio(
810
+ choices=["Auto", "16:9", "4:3", "1:1", "3:4", "9:16"],
811
+ value="Auto",
812
+ label="Select aspect ratio",
813
+ info="Choose output dimensions or Auto for original sizing"
814
+ )
815
+
816
+ gr.Markdown("### ✍️ Multi-Image Editing Instructions")
817
+ prompt_input = gr.Textbox(
818
+ label="Enter multi-image editing prompt",
819
+ placeholder="For example: combine these images into a single scene, merge the best parts from each image, create a collage with artistic transitions...",
820
+ lines=4,
821
+ max_lines=6
822
+ )
823
+
824
+ edit_button = gr.Button(
825
+ "🚀 Start Multi-Image Editing",
826
+ variant="primary",
827
+ size="lg"
828
+ )
829
+
830
+ with gr.Column(scale=1):
831
+ gr.Markdown("### 🎯 Multi-Image Editing Result")
832
+ output_image = gr.Image(
833
+ label="Multi-image edited result",
834
+ height=400,
835
+ elem_classes=["result-area"]
836
+ )
837
+
838
+ # Add "Use as Input" button
839
+ use_as_input_btn = gr.Button(
840
+ "🔄 Use as Input",
841
+ variant="secondary",
842
+ size="sm",
843
+ elem_classes=["use-as-input-btn"]
844
+ )
845
+
846
+ status_output = gr.Textbox(
847
+ label="Processing status",
848
+ lines=2,
849
+ max_lines=3,
850
+ interactive=False
851
+ )
852
+
853
+ # Action buttons that will show after task completion
854
+ action_buttons = gr.HTML(visible=False)
855
+
856
+ # Example area
857
+ gr.Markdown("### 💡 Multi-Image Editing Examples")
858
+
859
+ # Helper function to load example images
860
+ def load_example_1():
861
+ """Load dancing cats example"""
862
+ try:
863
+ from PIL import Image
864
+ img1 = Image.open("datas/data01/hei_cat01.webp")
865
+ img2 = Image.open("datas/data01/tom.webp")
866
+ return img1, img2, None, "Let the 2 cats dance together", "16:9"
867
+ except Exception as e:
868
+ print(f"Failed to load example 1 images: {e}")
869
+ return None, None, None, "Let the 2 cats dance together", "16:9"
870
+
871
+ def load_example_2():
872
+ """Load fashion try-on example"""
873
+ try:
874
+ from PIL import Image
875
+ img1 = Image.open("datas/data02/girl.jpg")
876
+ img2 = Image.open("datas/data02/cloth.jpeg")
877
+ return img1, img2, None, "Let the girl in first image, wear the dress in second image", "9:16"
878
+ except Exception as e:
879
+ print(f"Failed to load example 2 images: {e}")
880
+ return None, None, None, "Let the girl in first image, wear the dress in second image", "9:16"
881
+
882
+ def load_example_3():
883
+ """Load beach bikini example"""
884
+ try:
885
+ from PIL import Image
886
+ img1 = Image.open("datas/data03/girl.webp")
887
+ img2 = Image.open("datas/data03/cloth.jpg")
888
+ return img1, img2, None, "Let the girl in first image, wear the bikini in second image, lying on the beach", "1:1"
889
+ except Exception as e:
890
+ print(f"Failed to load example 3 images: {e}")
891
+ return None, None, None, "Let the girl in first image, wear the bikini in second image, lying on the beach", "1:1"
892
+
893
+ # Example 1: Cats dancing
894
+ gr.Markdown("#### 🐱 Example 1: Dancing Cats (2 images)")
895
+ with gr.Row():
896
+ with gr.Column(scale=2):
897
+ # Preview images for example 1
898
+ with gr.Row():
899
+ try:
900
+ gr.Image("datas/data01/hei_cat01.webp", label="Cat 1", height=100, width=100, show_label=False, interactive=False)
901
+ gr.Image("datas/data01/tom.webp", label="Cat 2", height=100, width=100, show_label=False, interactive=False)
902
+
903
+ except:
904
+ gr.Markdown("*Preview images not available*")
905
+ gr.Markdown("**Prompt**: Let the 2 cats dance together \n**Size**: 16:9")
906
+ with gr.Column(scale=1):
907
+ gr.Button(
908
+ "🎭 Load Dancing Cats Example",
909
+ size="lg",
910
+ variant="secondary"
911
+ ).click(
912
+ fn=load_example_1,
913
+ outputs=[input_image1, input_image2, input_image3, prompt_input, aspect_ratio_selector]
914
+ )
915
+
916
+ # Example 2: Girl wearing dress
917
+ gr.Markdown("#### 👗 Example 2: Fashion Try-on (2 images)")
918
+ with gr.Row():
919
+ with gr.Column(scale=2):
920
+ # Preview images for example 2
921
+ with gr.Row():
922
+ try:
923
+ gr.Image("datas/data02/girl.jpg", label="Girl", height=100, width=100, show_label=False, interactive=False)
924
+ gr.Image("datas/data02/cloth.jpeg", label="Dress", height=100, width=100, show_label=False, interactive=False)
925
+ except:
926
+ gr.Markdown("*Preview images not available*")
927
+ gr.Markdown("**Prompt**: Let the girl in first image, wear the dress in second image \n**Size**: 9:16")
928
+ with gr.Column(scale=1):
929
+ gr.Button(
930
+ "👗 Load Fashion Try-on Example",
931
+ size="lg",
932
+ variant="secondary"
933
+ ).click(
934
+ fn=load_example_2,
935
+ outputs=[input_image1, input_image2, input_image3, prompt_input, aspect_ratio_selector]
936
+ )
937
+
938
+ # Example 3: Beach bikini
939
+ gr.Markdown("#### 🏖️ Example 3: Beach Style (2 images)")
940
+ with gr.Row():
941
+ with gr.Column(scale=2):
942
+ # Preview images for example 3
943
+ with gr.Row():
944
+ try:
945
+ gr.Image("datas/data03/girl.webp", label="Girl", height=100, width=100, show_label=False, interactive=False)
946
+ gr.Image("datas/data03/cloth.jpg", label="Bikini", height=100, width=100, show_label=False, interactive=False)
947
+ except:
948
+ gr.Markdown("*Preview images not available*")
949
+ gr.Markdown("**Prompt**: Let the girl in first image, wear the bikini in second image, lying on the beach \n**Size**: 1:1")
950
+ with gr.Column(scale=1):
951
+ gr.Button(
952
+ "🏖️ Load Beach Style Example",
953
+ size="lg",
954
+ variant="secondary"
955
+ ).click(
956
+ fn=load_example_3,
957
+ outputs=[input_image1, input_image2, input_image3, prompt_input, aspect_ratio_selector]
958
+ )
959
+
960
+ # Additional quick prompt examples
961
+ gr.Markdown("#### ✨ Quick Prompts")
962
+ with gr.Row():
963
+ additional_prompts = [
964
+ "Combine these images into a single artistic scene",
965
+ "Create a collage with artistic transitions between images",
966
+ "Blend these images into a cohesive composition"
967
+ ]
968
+
969
+ for prompt in additional_prompts:
970
+ gr.Button(
971
+ prompt,
972
+ size="sm"
973
+ ).click(
974
+ lambda p=prompt: p,
975
+ outputs=prompt_input
976
+ )
977
+
978
+ # Bind button click events - simplified, remove state management
979
+ edit_button.click(
980
+ fn=edit_image_interface,
981
+ inputs=[input_image1, input_image2, input_image3, prompt_input, aspect_ratio_selector],
982
+ outputs=[output_image, status_output, action_buttons],
983
+ show_progress=True,
984
+ # Increase concurrency settings
985
+ concurrency_limit=8, # Limit concurrent requests for multi-image processing
986
+ api_name="multi_image_edit"
987
+ )
988
+
989
+ # Simplify "Use as Input" button, use result as first image
990
+ def simple_use_as_input(output_img):
991
+ if output_img is not None:
992
+ return output_img, None, None # Set as first image, clear others
993
+ return None, None, None
994
+
995
+ use_as_input_btn.click(
996
+ fn=simple_use_as_input,
997
+ inputs=[output_image],
998
+ outputs=[input_image1, input_image2, input_image3]
999
+ )
1000
+
1001
+ # # Local editing tab
1002
+ # with gr.Tab("🖌️ Local Editing"):
1003
+ # with gr.Row():
1004
+ # with gr.Column(scale=1):
1005
+ # gr.Markdown("### 📸 Upload Image and Draw Edit Area")
1006
+ # local_input_image = gr.ImageEditor(
1007
+ # label="Upload image and draw mask",
1008
+ # type="pil",
1009
+ # height=512,
1010
+ # brush=gr.Brush(colors=["#ff0000"], default_size=180),
1011
+ # elem_classes=["upload-area"]
1012
+ # )
1013
+
1014
+ # gr.Markdown("### ✍️ Editing Instructions")
1015
+ # local_prompt_input = gr.Textbox(
1016
+ # label="Enter local editing prompt",
1017
+ # placeholder="For example: change selected area hair to golden, add patterns to selected object, change selected area color, etc...",
1018
+ # lines=3,
1019
+ # max_lines=5
1020
+ # )
1021
+
1022
+ # local_edit_button = gr.Button(
1023
+ # "🎯 Start Local Editing",
1024
+ # variant="primary",
1025
+ # size="lg"
1026
+ # )
1027
+
1028
+ # with gr.Column(scale=1):
1029
+ # gr.Markdown("### 🎯 Editing Result")
1030
+ # local_output_image = gr.Image(
1031
+ # label="Local edited image",
1032
+ # height=320,
1033
+ # elem_classes=["result-area"]
1034
+ # )
1035
+
1036
+ # # Add "Use as Input" button
1037
+ # local_use_as_input_btn = gr.Button(
1038
+ # "🔄 Use as Input",
1039
+ # variant="secondary",
1040
+ # size="sm",
1041
+ # elem_classes=["use-as-input-btn"]
1042
+ # )
1043
+
1044
+ # local_status_output = gr.Textbox(
1045
+ # label="Processing status",
1046
+ # lines=2,
1047
+ # max_lines=3,
1048
+ # interactive=False
1049
+ # )
1050
+
1051
+ # Action buttons that will show after task completion
1052
+ # local_action_buttons = gr.HTML(visible=False)
1053
+
1054
+ # # Local editing examples
1055
+ # gr.Markdown("### 💡 Local Editing Prompt Examples")
1056
+ # with gr.Row():
1057
+ # local_example_prompts = [
1058
+ # "Change selected area hair to golden",
1059
+ # "Add pattern designs to selected clothing",
1060
+ # "Change selected area to different material",
1061
+ # "Add decorations to selected object",
1062
+ # "Change selected area color and style"
1063
+ # ]
1064
+
1065
+ # for prompt in local_example_prompts:
1066
+ # gr.Button(
1067
+ # prompt,
1068
+ # size="sm"
1069
+ # ).click(
1070
+ # lambda p=prompt: p,
1071
+ # outputs=local_prompt_input
1072
+ # )
1073
+
1074
+ # # Bind local edit button click events - simplified, remove state management
1075
+ # local_edit_button.click(
1076
+ # fn=local_edit_interface,
1077
+ # inputs=[local_input_image, local_prompt_input],
1078
+ # outputs=[local_output_image, local_status_output, local_action_buttons],
1079
+ # show_progress=True,
1080
+ # # Increase concurrency settings
1081
+ # concurrency_limit=8, # Local editing is more complex, allow fewer concurrent requests
1082
+ # api_name="local_edit"
1083
+ # )
1084
+
1085
+ # # Simplify local edit "Use as Input" button
1086
+ # def simple_local_use_as_input(output_img):
1087
+ # if output_img is not None:
1088
+ # # Create simple ImageEditor format
1089
+ # editor_data = {
1090
+ # "background": output_img,
1091
+ # "layers": [],
1092
+ # "composite": output_img
1093
+ # }
1094
+ # return editor_data
1095
+ # return None
1096
+
1097
+ # local_use_as_input_btn.click(
1098
+ # fn=simple_local_use_as_input,
1099
+ # inputs=[local_output_image],
1100
+ # outputs=[local_input_image]
1101
+ # )
1102
+
1103
+ # SEO Content Section
1104
+ gr.HTML("""
1105
+ <div style="width: 100%; margin: 50px 0; padding: 0 20px;">
1106
+
1107
+ <!-- Showcase Section -->
1108
+ <div style="text-align: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 40px; border-radius: 20px; margin: 40px 0;">
1109
+ <h2 style="margin: 0 0 20px 0; font-size: 2.2em; font-weight: 700;">
1110
+ 🎨 Unlimited AI Image Generation & Editing
1111
+ </h2>
1112
+ <p style="margin: 0 0 25px 0; font-size: 1.2em; opacity: 0.95; line-height: 1.6;">
1113
+ Experience the ultimate freedom in AI image creation! Generate and edit unlimited images without restrictions,
1114
+ including NSFW content, with our premium AI image editing platform.
1115
+ </p>
1116
+
1117
+ <div style="display: flex; justify-content: center; gap: 25px; flex-wrap: wrap; margin: 30px 0;">
1118
+ <a href="https://omnicreator.net/multi-image-edit#generator" target="_blank" style="
1119
+ display: inline-flex;
1120
+ align-items: center;
1121
+ justify-content: center;
1122
+ padding: 20px 40px;
1123
+ background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%);
1124
+ color: white;
1125
+ text-decoration: none;
1126
+ border-radius: 15px;
1127
+ font-weight: 700;
1128
+ font-size: 18px;
1129
+ text-align: center;
1130
+ min-width: 250px;
1131
+ box-shadow: 0 8px 25px rgba(255, 107, 107, 0.4);
1132
+ transition: all 0.3s ease;
1133
+ border: none;
1134
+ transform: scale(1);
1135
+ " onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
1136
+ 🚀 Get Unlimited Access Now
1137
+ </a>
1138
+
1139
+ </div>
1140
+
1141
+ <p style="color: rgba(255,255,255,0.9); font-size: 1em; margin: 20px 0 0 0;">
1142
+ Join thousands of creators who trust Omni Creator for unrestricted AI image generation!
1143
+ </p>
1144
+ </div>
1145
+
1146
+ <!-- Hero Description -->
1147
+ <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);">
1148
+ <h2 style="color: #2c3e50; margin: 0 0 20px 0; font-size: 1.9em; font-weight: 700;">
1149
+ 🌟 Professional AI Multi-Image Editor - No Restrictions
1150
+ </h2>
1151
+ <p style="color: #555; font-size: 1.1em; line-height: 1.6; margin: 0 0 20px 0; padding: 0 20px;">
1152
+ Transform multiple images into stunning compositions with our advanced AI multi-image editing platform. Combine,
1153
+ merge, and blend 2-3 images to create artistic masterpieces, collages, and seamless compositions with complete
1154
+ creative freedom and professional quality results.
1155
+ </p>
1156
+ </div>
1157
+
1158
+ <!-- Features Grid -->
1159
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 25px; margin: 40px 0;">
1160
+
1161
+ <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;">
1162
+ <h3 style="color: #e74c3c; margin: 0 0 15px 0; font-size: 1.4em; font-weight: 600;">
1163
+ 🎯 Unlimited Generation
1164
+ </h3>
1165
+ <p style="color: #666; margin: 0; line-height: 1.6; font-size: 1em;">
1166
+ Premium users enjoy unlimited image generation without daily limits, rate restrictions, or content barriers.
1167
+ Create as many images as you need, whenever you need them.
1168
+ </p>
1169
+ </div>
1170
+
1171
+ <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;">
1172
+ <h3 style="color: #3498db; margin: 0 0 15px 0; font-size: 1.4em; font-weight: 600;">
1173
+ 🔓 No Content Restrictions
1174
+ </h3>
1175
+ <p style="color: #666; margin: 0; line-height: 1.6; font-size: 1em;">
1176
+ Generate and edit any type of content without NSFW filters or content limitations. Complete creative
1177
+ freedom for artists, designers, and content creators.
1178
+ </p>
1179
+ </div>
1180
+
1181
+ <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;">
1182
+ <h3 style="color: #27ae60; margin: 0 0 15px 0; font-size: 1.4em; font-weight: 600;">
1183
+ ⚡ Lightning Fast Processing
1184
+ </h3>
1185
+ <p style="color: #666; margin: 0; line-height: 1.6; font-size: 1em;">
1186
+ Advanced AI infrastructure delivers high-quality results in seconds. No waiting in queues,
1187
+ no processing delays - just instant, professional-grade image editing.
1188
+ </p>
1189
+ </div>
1190
+
1191
+ <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;">
1192
+ <h3 style="color: #9b59b6; margin: 0 0 15px 0; font-size: 1.4em; font-weight: 600;">
1193
+ 🎨 Advanced Editing Tools
1194
+ </h3>
1195
+ <p style="color: #666; margin: 0; line-height: 1.6; font-size: 1em;">
1196
+ Global transformations, precision local editing, style transfer, object removal, background replacement,
1197
+ and dozens of other professional editing capabilities.
1198
+ </p>
1199
+ </div>
1200
+
1201
+ <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;">
1202
+ <h3 style="color: #f39c12; margin: 0 0 15px 0; font-size: 1.4em; font-weight: 600;">
1203
+ 💎 Premium Quality
1204
+ </h3>
1205
+ <p style="color: #666; margin: 0; line-height: 1.6; font-size: 1em;">
1206
+ State-of-the-art AI models trained on millions of images deliver exceptional quality and realism.
1207
+ Professional results suitable for commercial use and high-end projects.
1208
+ </p>
1209
+ </div>
1210
+
1211
+ <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;">
1212
+ <h3 style="color: #34495e; margin: 0 0 15px 0; font-size: 1.4em; font-weight: 600;">
1213
+ 🌍 Multi-Modal Support
1214
+ </h3>
1215
+ <p style="color: #666; margin: 0; line-height: 1.6; font-size: 1em;">
1216
+ Support for all image formats, styles, and use cases. From photorealistic portraits to artistic creations,
1217
+ product photography to digital art - we handle everything.
1218
+ </p>
1219
+ </div>
1220
+
1221
+ </div>
1222
+
1223
+ <!-- Premium Benefits Section -->
1224
+ <div style="background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); color: white; padding: 40px; border-radius: 20px; margin: 40px 0; text-align: center;">
1225
+ <h2 style="margin: 0 0 25px 0; font-size: 1.8em; font-weight: 700;">
1226
+ 💎 Why Choose Omni Creator Premium?
1227
+ </h2>
1228
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin: 30px 0;">
1229
+
1230
+ <div style="background: rgba(255,255,255,0.15); padding: 20px; border-radius: 12px;">
1231
+ <h4 style="margin: 0 0 10px 0; font-size: 1.2em;">🚫 No Rate Limits</h4>
1232
+ <p style="margin: 0; opacity: 0.9; font-size: 0.95em;">Generate unlimited images without waiting periods or daily restrictions</p>
1233
+ </div>
1234
+
1235
+ <div style="background: rgba(255,255,255,0.15); padding: 20px; border-radius: 12px;">
1236
+ <h4 style="margin: 0 0 10px 0; font-size: 1.2em;">🎭 Unrestricted Content</h4>
1237
+ <p style="margin: 0; opacity: 0.9; font-size: 0.95em;">Create any type of content without NSFW filters or censorship</p>
1238
+ </div>
1239
+
1240
+ <div style="background: rgba(255,255,255,0.15); padding: 20px; border-radius: 12px;">
1241
+ <h4 style="margin: 0 0 10px 0; font-size: 1.2em;">⚡ Priority Processing</h4>
1242
+ <p style="margin: 0; opacity: 0.9; font-size: 0.95em;">Skip queues and get instant results with dedicated processing power</p>
1243
+ </div>
1244
+
1245
+ <div style="background: rgba(255,255,255,0.15); padding: 20px; border-radius: 12px;">
1246
+ <h4 style="margin: 0 0 10px 0; font-size: 1.2em;">🎨 Advanced Features</h4>
1247
+ <p style="margin: 0; opacity: 0.9; font-size: 0.95em;">Access to latest AI models and cutting-edge editing capabilities</p>
1248
+ </div>
1249
+
1250
+ </div>
1251
+ <div style="display: flex; justify-content: center; margin: 25px 0 0 0;">
1252
+ <a href="https://omnicreator.net/multi-image-edit#generator" target="_blank" style="
1253
+ display: inline-flex;
1254
+ align-items: center;
1255
+ justify-content: center;
1256
+ padding: 18px 35px;
1257
+ background: rgba(255,255,255,0.9);
1258
+ color: #333;
1259
+ text-decoration: none;
1260
+ border-radius: 15px;
1261
+ font-weight: 700;
1262
+ font-size: 16px;
1263
+ text-align: center;
1264
+ min-width: 200px;
1265
+ box-shadow: 0 6px 20px rgba(0,0,0,0.3);
1266
+ transition: all 0.3s ease;
1267
+ border: none;
1268
+ ">🌟 Start Creating Now</a>
1269
+ </div>
1270
+ </div>
1271
+
1272
+ <!-- Tips Section -->
1273
+ <div style="background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 50%, #fecfef 100%); padding: 30px; border-radius: 15px; margin: 40px 0;">
1274
+ <h3 style="color: #8b5cf6; text-align: center; margin: 0 0 25px 0; font-size: 1.5em; font-weight: 700;">
1275
+ 💡 Pro Tips for Best Results
1276
+ </h3>
1277
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 18px;">
1278
+
1279
+ <div style="background: rgba(255,255,255,0.85); padding: 18px; border-radius: 12px;">
1280
+ <strong style="color: #8b5cf6; font-size: 1.1em;">📝 Clear Descriptions:</strong>
1281
+ <p style="color: #555; margin: 5px 0 0 0; line-height: 1.5;">Use detailed, specific prompts for better results. Describe colors, styles, lighting, and composition clearly.</p>
1282
+ </div>
1283
+
1284
+ <div style="background: rgba(255,255,255,0.85); padding: 18px; border-radius: 12px;">
1285
+ <strong style="color: #8b5cf6; font-size: 1.1em;">🎯 Local Editing:</strong>
1286
+ <p style="color: #555; margin: 5px 0 0 0; line-height: 1.5;">Use precise brush strokes to select areas for local editing. Smaller, focused edits often yield better results.</p>
1287
+ </div>
1288
+
1289
+ <div style="background: rgba(255,255,255,0.85); padding: 18px; border-radius: 12px;">
1290
+ <strong style="color: #8b5cf6; font-size: 1.1em;">⚡ Iterative Process:</strong>
1291
+ <p style="color: #555; margin: 5px 0 0 0; line-height: 1.5;">Use "Use as Input" feature to refine results. Multiple iterations can achieve complex transformations.</p>
1292
+ </div>
1293
+
1294
+ <div style="background: rgba(255,255,255,0.85); padding: 18px; border-radius: 12px;">
1295
+ <strong style="color: #8b5cf6; font-size: 1.1em;">🖼️ Image Quality:</strong>
1296
+ <p style="color: #555; margin: 5px 0 0 0; line-height: 1.5;">Higher resolution input images (up to 10MB) generally produce better editing results and finer details.</p>
1297
+ </div>
1298
+
1299
+ </div>
1300
+ </div>
1301
+
1302
+ <!-- Use Cases Section -->
1303
+ <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);">
1304
+ <h2 style="color: #2c3e50; margin: 0 0 20px 0; font-size: 1.8em; font-weight: 700;">
1305
+ 🚀 Perfect For Every Creative Need
1306
+ </h2>
1307
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 25px 0; text-align: left;">
1308
+
1309
+ <div style="background: rgba(255,255,255,0.8); padding: 20px; border-radius: 12px;">
1310
+ <h4 style="color: #e74c3c; margin: 0 0 10px 0;">🎨 Digital Art</h4>
1311
+ <ul style="color: #555; margin: 0; padding-left: 18px; line-height: 1.6;">
1312
+ <li>Character design</li>
1313
+ <li>Concept art</li>
1314
+ <li>Style transfer</li>
1315
+ <li>Artistic effects</li>
1316
+ </ul>
1317
+ </div>
1318
+
1319
+ <div style="background: rgba(255,255,255,0.8); padding: 20px; border-radius: 12px;">
1320
+ <h4 style="color: #3498db; margin: 0 0 10px 0;">📸 Photography</h4>
1321
+ <ul style="color: #555; margin: 0; padding-left: 18px; line-height: 1.6;">
1322
+ <li>Background replacement</li>
1323
+ <li>Object removal</li>
1324
+ <li>Lighting adjustment</li>
1325
+ <li>Portrait enhancement</li>
1326
+ </ul>
1327
+ </div>
1328
+
1329
+ <div style="background: rgba(255,255,255,0.8); padding: 20px; border-radius: 12px;">
1330
+ <h4 style="color: #27ae60; margin: 0 0 10px 0;">🛍️ E-commerce</h4>
1331
+ <ul style="color: #555; margin: 0; padding-left: 18px; line-height: 1.6;">
1332
+ <li>Product photography</li>
1333
+ <li>Lifestyle shots</li>
1334
+ <li>Color variations</li>
1335
+ <li>Context placement</li>
1336
+ </ul>
1337
+ </div>
1338
+
1339
+ <div style="background: rgba(255,255,255,0.8); padding: 20px; border-radius: 12px;">
1340
+ <h4 style="color: #9b59b6; margin: 0 0 10px 0;">📱 Social Media</h4>
1341
+ <ul style="color: #555; margin: 0; padding-left: 18px; line-height: 1.6;">
1342
+ <li>Content creation</li>
1343
+ <li>Meme generation</li>
1344
+ <li>Brand visuals</li>
1345
+ <li>Viral content</li>
1346
+ </ul>
1347
+ </div>
1348
+
1349
+ </div>
1350
+ <div style="text-align: center; margin: 25px 0 0 0;">
1351
+ <a href="https://omnicreator.net/multi-image-edit#generator" target="_blank" style="
1352
+ display: inline-flex;
1353
+ align-items: center;
1354
+ justify-content: center;
1355
+ padding: 18px 35px;
1356
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1357
+ color: white;
1358
+ text-decoration: none;
1359
+ border-radius: 15px;
1360
+ font-weight: 700;
1361
+ font-size: 16px;
1362
+ text-align: center;
1363
+ min-width: 220px;
1364
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
1365
+ transition: all 0.3s ease;
1366
+ border: none;
1367
+ ">🎯 Start Your Project Now</a>
1368
+ </div>
1369
+ </div>
1370
+
1371
+ </div>
1372
+
1373
+ <!-- Powered by footer -->
1374
+ <div style="text-align: center; margin: 30px auto 20px auto; padding: 20px;">
1375
+ <p style="margin: 0 0 10px 0; font-size: 18px; color: #333; font-weight: 500;">
1376
+ Powered by <a href="https://omnicreator.net/multi-image-edit#generator" target="_blank" style="color: #667eea; text-decoration: none; font-weight: bold;">Omni Creator</a>
1377
+ </p>
1378
+ <p style="margin: 0; font-size: 14px; color: #999; font-weight: 400;">
1379
+ The ultimate AI image generation and editing platform • Unlimited creativity, zero restrictions
1380
+ </p>
1381
+ </div>
1382
+ """, padding=False)
1383
+
1384
+ return app
1385
+
1386
+ if __name__ == "__main__":
1387
+ app = create_app()
1388
+ # Improve queue configuration to handle high concurrency and prevent SSE connection issues
1389
+ app.queue(
1390
+ default_concurrency_limit=20, # Default concurrency limit
1391
+ max_size=50, # Maximum queue size
1392
+ api_open=False # Close API access to reduce resource consumption
1393
+ )
1394
+ app.launch(
1395
+ server_name="0.0.0.0",
1396
+ show_error=True, # Show detailed error information
1397
+ quiet=False, # Keep log output
1398
+ max_threads=40, # Increase thread pool size
1399
+ height=800,
1400
+ favicon_path=None # Reduce resource loading
1401
+ )
datas/.DS_Store ADDED
Binary file (6.15 kB). View file
 
datas/data01/hei_cat01.webp ADDED
datas/data01/tom.webp ADDED
datas/data01/tom02.webp ADDED
datas/data02/.DS_Store ADDED
Binary file (6.15 kB). View file
 
datas/data02/cloth.jpeg ADDED
datas/data03/.DS_Store ADDED
Binary file (6.15 kB). View file
 
datas/data03/girl.webp ADDED
labels.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "0": "normal",
3
+ "1": "nsfw"
4
+ }
nfsw.py ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from PIL import Image
3
+ import numpy as np
4
+ import onnxruntime as ort
5
+ import json
6
+ from huggingface_hub import hf_hub_download
7
+
8
+
9
+ class NSFWDetector:
10
+ """
11
+ NSFW检测器类,使用YOLOv9模型进行图像分类
12
+ """
13
+
14
+ def __init__(self, repo_id="Falconsai/nsfw_image_detection",
15
+ model_filename="falconsai_yolov9_nsfw_model_quantized.pt",
16
+ labels_filename="labels.json",
17
+ input_size=(224, 224)):
18
+ """
19
+ 初始化NSFW检测器
20
+
21
+ Args:
22
+ repo_id (str): Hugging Face仓库ID
23
+ model_filename (str): 模型文件名
24
+ labels_filename (str): 标签文件名
25
+ input_size (tuple): 模型输入尺寸 (height, width)
26
+ """
27
+ self.repo_id = repo_id
28
+ self.model_filename = model_filename
29
+ self.labels_filename = labels_filename
30
+ self.input_size = input_size
31
+
32
+ # 从Hugging Face下载文件
33
+ self.model_path = self._download_model()
34
+ self.labels_path = self._download_labels()
35
+
36
+ # 加载标签
37
+ self.labels = self._load_labels()
38
+
39
+ # 加载模型
40
+ self.session = self._load_model()
41
+ self.input_name = self.session.get_inputs()[0].name
42
+ self.output_name = self.session.get_outputs()[0].name
43
+
44
+ def _download_model(self):
45
+ """
46
+ 从Hugging Face下载模型文件
47
+
48
+ Returns:
49
+ str: 下载的模型文件路径
50
+ """
51
+ try:
52
+ print(f"正在从 {self.repo_id} 下载模型文件: {self.model_filename}")
53
+ model_path = hf_hub_download(
54
+ repo_id=self.repo_id,
55
+ filename=self.model_filename,
56
+ cache_dir="./hf_cache"
57
+ )
58
+ print(f"✅ 模型下载成功: {model_path}")
59
+ return model_path
60
+ except Exception as e:
61
+ raise RuntimeError(f"模型下载失败: {e}")
62
+
63
+ def _download_labels(self):
64
+ """
65
+ 从Hugging Face下载标签文件
66
+
67
+ Returns:
68
+ str: 下载的标签文件路径
69
+ """
70
+ try:
71
+ print(f"正在从 {self.repo_id} 下载标签文件: {self.labels_filename}")
72
+ labels_path = hf_hub_download(
73
+ repo_id=self.repo_id,
74
+ filename=self.labels_filename,
75
+ cache_dir="./hf_cache"
76
+ )
77
+ print(f"✅ 标签文件下载成功: {labels_path}")
78
+ return labels_path
79
+ except Exception as e:
80
+ raise RuntimeError(f"标签文件下载失败: {e}")
81
+
82
+ def _load_labels(self):
83
+ """
84
+ 加载类别标签
85
+
86
+ Returns:
87
+ dict: 标签字典
88
+ """
89
+ try:
90
+ with open(self.labels_path, "r") as f:
91
+ return json.load(f)
92
+ except FileNotFoundError:
93
+ raise FileNotFoundError(f"标签文件未找到: {self.labels_path}")
94
+ except json.JSONDecodeError:
95
+ raise ValueError(f"标签文件格式错误: {self.labels_path}")
96
+
97
+ def _load_model(self):
98
+ """
99
+ 加载ONNX模型
100
+
101
+ Returns:
102
+ onnxruntime.InferenceSession: 模型会话
103
+ """
104
+ try:
105
+ return ort.InferenceSession(self.model_path)
106
+ except Exception as e:
107
+ raise RuntimeError(f"模型加载失败: {self.model_path}, 错误: {e}")
108
+
109
+ def _preprocess_image(self, image_path):
110
+ """
111
+ 图像预处理
112
+
113
+ Args:
114
+ image_path (str): 图像文件路径
115
+
116
+ Returns:
117
+ tuple: (预处理后的张量, 原始图像)
118
+ """
119
+ try:
120
+ # 加载并转换图像
121
+ original_image = Image.open(image_path).convert("RGB")
122
+
123
+ # 调整尺寸
124
+ image_resized = original_image.resize(self.input_size, Image.Resampling.BILINEAR)
125
+
126
+ # 转换为numpy数组并归一化
127
+ image_np = np.array(image_resized, dtype=np.float32) / 255.0
128
+
129
+ # 调整维度顺序 [H, W, C] -> [C, H, W]
130
+ image_np = np.transpose(image_np, (2, 0, 1))
131
+
132
+ # 添加批次维度 [C, H, W] -> [1, C, H, W]
133
+ input_tensor = np.expand_dims(image_np, axis=0).astype(np.float32)
134
+
135
+ return input_tensor, original_image
136
+
137
+ except FileNotFoundError:
138
+ raise FileNotFoundError(f"图像文件未找到: {image_path}")
139
+ except Exception as e:
140
+ raise RuntimeError(f"图像预处理失败: {e}")
141
+
142
+ def _postprocess_predictions(self, predictions):
143
+ """
144
+ 后处理预测结果
145
+
146
+ Args:
147
+ predictions: 模型预测输出
148
+
149
+ Returns:
150
+ str: 预测的类别标签
151
+ """
152
+ predicted_index = np.argmax(predictions)
153
+ predicted_label = self.labels[str(predicted_index)]
154
+ return predicted_label
155
+
156
+ def predict(self, image_path):
157
+ """
158
+ 对单张图像进行NSFW检测
159
+
160
+ Args:
161
+ image_path (str): 图像文件路径
162
+
163
+ Returns:
164
+ tuple: (预测标签, 原始图像)
165
+ """
166
+ # 预处理图像
167
+ input_tensor, original_image = self._preprocess_image(image_path)
168
+
169
+ # 运行推理
170
+ outputs = self.session.run([self.output_name], {self.input_name: input_tensor})
171
+ predictions = outputs[0]
172
+
173
+ # 后处理结果
174
+ predicted_label = self._postprocess_predictions(predictions)
175
+
176
+ return predicted_label, original_image
177
+
178
+ def predict_label_only(self, image_path):
179
+ """
180
+ 只返回预测标签(不返回图像)
181
+
182
+ Args:
183
+ image_path (str): 图像文件路径
184
+
185
+ Returns:
186
+ str: 预测的类别标签
187
+ """
188
+ predicted_label, _ = self.predict(image_path)
189
+ return predicted_label
190
+
191
+ def predict_from_pil(self, pil_image):
192
+ """
193
+ 直接从PIL Image对象进行NSFW检测
194
+
195
+ Args:
196
+ pil_image (PIL.Image): PIL图像对象
197
+
198
+ Returns:
199
+ tuple: (预测标签, 原始图像)
200
+ """
201
+ try:
202
+ # 确保是RGB格式
203
+ if pil_image.mode != "RGB":
204
+ pil_image = pil_image.convert("RGB")
205
+
206
+ # 调整尺寸
207
+ image_resized = pil_image.resize(self.input_size, Image.Resampling.BILINEAR)
208
+
209
+ # 转换为numpy数组并归一化
210
+ image_np = np.array(image_resized, dtype=np.float32) / 255.0
211
+
212
+ # 调整维度顺序 [H, W, C] -> [C, H, W]
213
+ image_np = np.transpose(image_np, (2, 0, 1))
214
+
215
+ # 添加批次维度 [C, H, W] -> [1, C, H, W]
216
+ input_tensor = np.expand_dims(image_np, axis=0).astype(np.float32)
217
+
218
+ # 运行推理
219
+ outputs = self.session.run([self.output_name], {self.input_name: input_tensor})
220
+ predictions = outputs[0]
221
+
222
+ # 后处理结果
223
+ predicted_label = self._postprocess_predictions(predictions)
224
+
225
+ return predicted_label, pil_image
226
+
227
+ except Exception as e:
228
+ raise RuntimeError(f"PIL图像预测失败: {e}")
229
+
230
+ def predict_pil_label_only(self, pil_image):
231
+ """
232
+ 从PIL Image对象只返回预测标签
233
+
234
+ Args:
235
+ pil_image (PIL.Image): PIL图像对象
236
+
237
+ Returns:
238
+ str: 预测的类别标签
239
+ """
240
+ predicted_label, _ = self.predict_from_pil(pil_image)
241
+ return predicted_label
242
+
243
+ # --- 使用示例 ---
244
+ if __name__ == "__main__":
245
+ # 配置参数
246
+ single_image_path = "datas/bad01.jpg"
247
+
248
+ try:
249
+ # 创建检测器实例(自动从Hugging Face下载)
250
+ detector = NSFWDetector()
251
+
252
+ # 检查图像文件是否存在
253
+ if os.path.exists(single_image_path):
254
+ # 进行预测
255
+ predicted_label = detector.predict_label_only(single_image_path)
256
+ print(f"图像文件: {single_image_path}")
257
+ print(f"预测结果: {predicted_label}")
258
+ else:
259
+ print(f"错误: 指定的图像文件不存在: {single_image_path}")
260
+
261
+ except Exception as e:
262
+ print(f"初始化检测器时发生错误: {e}")
push.sh ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # 设置仓库级别用户名
3
+ git config user.name "selfitcamera"
4
+ git config user.email "ethan.blake@heybeauty.ai"
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,673 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
35
+
36
+
37
+ samples = [
38
+ {
39
+ "image": ["datas/data01/hei_cat01.webp","datas/data01/tom.webp"],
40
+ "prompt": "Let all cats dance together in the park",
41
+ "aspect_ratio": "16:9"
42
+ },
43
+ {
44
+ "image": ["datas/data02/girl.jpg","datas/data02/cloth.jpeg"],
45
+ "prompt": "Let the girl in first image, wear the dress in second image",
46
+ "aspect_ratio": "9:16"
47
+ },
48
+ {
49
+ "image": ["datas/data03/girl.webp","datas/data03/cloth.jpg"],
50
+ "prompt": "Let the girl in first image, wear the bikini in second image, lying on the beach",
51
+ "aspect_ratio": "1:1"
52
+ }
53
+ ]
54
+
55
+
56
+
57
+ class R2Api:
58
+
59
+ def __init__(self, session=None):
60
+ super().__init__()
61
+ self.R2_BUCKET = "omni-creator"
62
+ self.domain = "https://www.omnicreator.net/"
63
+ self.R2_ACCESS_KEY = R2_ACCESS_KEY
64
+ self.R2_SECRET_KEY = R2_SECRET_KEY
65
+ self.R2_ENDPOINT = R2_ENDPOINT
66
+
67
+ self.client = boto3.client(
68
+ "s3",
69
+ endpoint_url=self.R2_ENDPOINT,
70
+ aws_access_key_id=self.R2_ACCESS_KEY,
71
+ aws_secret_access_key=self.R2_SECRET_KEY,
72
+ config=Config(signature_version="s3v4")
73
+ )
74
+
75
+ self.session = requests.Session() if session is None else session
76
+
77
+ def upload_from_memory(self, image_data, filename, content_type='image/jpeg'):
78
+ """
79
+ Upload image data directly from memory to R2
80
+
81
+ Args:
82
+ image_data (bytes): Image data in bytes
83
+ filename (str): Filename for the uploaded file
84
+ content_type (str): MIME type of the image
85
+
86
+ Returns:
87
+ str: URL of the uploaded file
88
+ """
89
+ t1 = time.time()
90
+ headers = {"Content-Type": content_type}
91
+
92
+ cloud_path = f"QwenImageEdit/Uploads/{str(datetime.date.today())}/{filename}"
93
+ url = self.client.generate_presigned_url(
94
+ "put_object",
95
+ Params={"Bucket": self.R2_BUCKET, "Key": cloud_path, "ContentType": content_type},
96
+ ExpiresIn=604800
97
+ )
98
+
99
+ retry_count = 0
100
+ while retry_count < 3:
101
+ try:
102
+ response = self.session.put(url, data=image_data, headers=headers, timeout=15)
103
+ if response.status_code == 200:
104
+ break
105
+ else:
106
+ print(f"⚠️ Upload failed with status code: {response.status_code}")
107
+ retry_count += 1
108
+ except (requests.exceptions.Timeout, requests.exceptions.RequestException) as e:
109
+ print(f"⚠️ Upload retry {retry_count + 1}/3 failed: {e}")
110
+ retry_count += 1
111
+ if retry_count == 3:
112
+ raise Exception(f'Failed to upload file to R2 after 3 retries! Last error: {str(e)}')
113
+ time.sleep(1) # 等待1秒后重试
114
+ continue
115
+ print("upload_from_memory time is ====>", time.time() - t1)
116
+ return f"{self.domain}{cloud_path}"
117
+
118
+ def upload_file(self, local_path, cloud_path):
119
+ t1 = time.time()
120
+ head_dict = {
121
+ 'jpg': 'image/jpeg',
122
+ 'jpeg': 'image/jpeg',
123
+ 'png': 'image/png',
124
+ 'gif': 'image/gif',
125
+ 'bmp': 'image/bmp',
126
+ 'webp': 'image/webp',
127
+ 'ico': 'image/x-icon'
128
+ }
129
+ ftype = os.path.basename(local_path).split(".")[-1].lower()
130
+ ctype = head_dict.get(ftype, 'application/octet-stream')
131
+ headers = {"Content-Type": ctype}
132
+
133
+
134
+ cloud_path = f"QwenImageEdit/Uploads/{str(datetime.date.today())}/{os.path.basename(local_path)}"
135
+ url = self.client.generate_presigned_url(
136
+ "put_object",
137
+ Params={"Bucket": self.R2_BUCKET, "Key": cloud_path, "ContentType": ctype},
138
+ ExpiresIn=604800
139
+ )
140
+
141
+ retry_count = 0
142
+ while retry_count < 3:
143
+ try:
144
+ with open(local_path, 'rb') as f:
145
+ self.session.put(url, data=f.read(), headers=headers, timeout=8)
146
+ break
147
+ except (requests.exceptions.Timeout, requests.exceptions.RequestException):
148
+ retry_count += 1
149
+ if retry_count == 3:
150
+ raise Exception('Failed to upload file to R2 after 3 retries!')
151
+ continue
152
+ print("upload_file time is ====>", time.time() - t1)
153
+ return f"{self.domain}{cloud_path}"
154
+
155
+ def upload_user_img_r2(clientIp, timeId, pil_image):
156
+ """
157
+ Upload PIL Image directly to R2 without saving to local file
158
+
159
+ Args:
160
+ clientIp (str): Client IP address
161
+ timeId (int): Timestamp
162
+ pil_image (PIL.Image): PIL Image object
163
+
164
+ Returns:
165
+ str: Uploaded URL
166
+ """
167
+ # Generate unique filename using UUID to prevent file conflicts in concurrent environment
168
+ unique_id = str(uuid.uuid4())
169
+ fileName = f"user_img_{unique_id}_{timeId}.jpg"
170
+
171
+ # Convert PIL Image to bytes
172
+ img_buffer = io.BytesIO()
173
+ if pil_image.mode != 'RGB':
174
+ pil_image = pil_image.convert('RGB')
175
+ pil_image.save(img_buffer, format='JPEG', quality=95)
176
+ img_data = img_buffer.getvalue()
177
+
178
+ # Upload directly from memory
179
+ res = R2Api().upload_from_memory(img_data, fileName, 'image/jpeg')
180
+ return res
181
+
182
+
183
+
184
+ def create_mask_from_layers(base_image, layers):
185
+ """
186
+ Create mask image from ImageEditor layers
187
+
188
+ Args:
189
+ base_image (PIL.Image): Original image
190
+ layers (list): ImageEditor layer data
191
+
192
+ Returns:
193
+ PIL.Image: Black and white mask image
194
+ """
195
+ from PIL import Image, ImageDraw
196
+ import numpy as np
197
+
198
+ # Create blank mask with same size as original image
199
+ mask = Image.new('L', base_image.size, 0) # 'L' mode is grayscale, 0 is black
200
+
201
+ if not layers:
202
+ return mask
203
+
204
+ # Iterate through all layers, set drawn areas to white
205
+ for layer in layers:
206
+ if layer is not None:
207
+ # Convert layer to numpy array
208
+ layer_array = np.array(layer)
209
+
210
+ # Check layer format
211
+ if len(layer_array.shape) == 3: # RGB/RGBA format
212
+ # If RGBA, check alpha channel
213
+ if layer_array.shape[2] == 4:
214
+ # Use alpha channel as mask
215
+ alpha_channel = layer_array[:, :, 3]
216
+ # Set non-transparent areas (alpha > 0) to white
217
+ mask_array = np.where(alpha_channel > 0, 255, 0).astype(np.uint8)
218
+ else:
219
+ # RGB format, check if not pure black (0,0,0)
220
+ # Assume drawn areas are non-black
221
+ non_black = np.any(layer_array > 0, axis=2)
222
+ mask_array = np.where(non_black, 255, 0).astype(np.uint8)
223
+ elif len(layer_array.shape) == 2: # Grayscale
224
+ # Use grayscale values directly, set non-zero areas to white
225
+ mask_array = np.where(layer_array > 0, 255, 0).astype(np.uint8)
226
+ else:
227
+ continue
228
+
229
+ # Convert mask_array to PIL image and merge into total mask
230
+ layer_mask = Image.fromarray(mask_array, mode='L')
231
+ # Resize to match original image
232
+ if layer_mask.size != base_image.size:
233
+ layer_mask = layer_mask.resize(base_image.size, Image.LANCZOS)
234
+
235
+ # Merge masks (use maximum value to ensure all drawn areas are included)
236
+ mask_array_current = np.array(mask)
237
+ layer_mask_array = np.array(layer_mask)
238
+ combined_mask_array = np.maximum(mask_array_current, layer_mask_array)
239
+ mask = Image.fromarray(combined_mask_array, mode='L')
240
+
241
+ return mask
242
+
243
+
244
+ def upload_mask_image_r2(client_ip, time_id, mask_image):
245
+ """
246
+ Upload mask image to R2 directly from memory
247
+
248
+ Args:
249
+ client_ip (str): Client IP
250
+ time_id (int): Timestamp
251
+ mask_image (PIL.Image): Mask image
252
+
253
+ Returns:
254
+ str: Uploaded URL
255
+ """
256
+ # Generate unique filename using UUID to prevent file conflicts in concurrent environment
257
+ unique_id = str(uuid.uuid4())
258
+ file_name = f"mask_img_{unique_id}_{time_id}.png"
259
+
260
+ try:
261
+ # Convert mask image to bytes
262
+ img_buffer = io.BytesIO()
263
+ mask_image.save(img_buffer, format='PNG')
264
+ img_data = img_buffer.getvalue()
265
+
266
+ # Upload directly from memory
267
+ res = R2Api().upload_from_memory(img_data, file_name, 'image/png')
268
+
269
+ return res
270
+ except Exception as e:
271
+ print(f"Failed to upload mask image: {e}")
272
+ return None
273
+
274
+
275
+
276
+ def submit_image_edit_task(user_image_url, prompt, task_type="80", mask_image_url="", user_image2_url="", user_image3_url="", width=0, height=0):
277
+ """
278
+ Submit image editing task with improved error handling using API v2
279
+ Supports multi-image editing with user_image2, user_image3, width, height parameters
280
+ """
281
+ headers = {
282
+ 'Content-Type': 'application/json',
283
+ 'Authorization': f'Bearer {APIKEY}'
284
+ }
285
+
286
+ data = {
287
+ "user_image": user_image_url,
288
+ "user_image2": user_image2_url,
289
+ "user_image3": user_image3_url,
290
+ "user_mask": mask_image_url,
291
+ "type": task_type,
292
+ "width": width,
293
+ "height": height,
294
+ "text": prompt,
295
+ "user_uuid": APIKEY,
296
+ "priority": 0,
297
+ "secret_key": "219ngu"
298
+ }
299
+
300
+ retry_count = 0
301
+ max_retries = 3
302
+
303
+ while retry_count < max_retries:
304
+ try:
305
+ response = requests.post(
306
+ f'{UKAPIURL}/public_image_edit_v2',
307
+ headers=headers,
308
+ json=data,
309
+ timeout=30 # 增加超时时间
310
+ )
311
+
312
+ if response.status_code == 200:
313
+ result = response.json()
314
+ if result.get('code') == 0:
315
+ return result['data']['task_id'], None
316
+ else:
317
+ return None, f"API Error: {result.get('message', 'Unknown error')}"
318
+ elif response.status_code in [502, 503, 504]: # 服务器错误,可以重试
319
+ retry_count += 1
320
+ if retry_count < max_retries:
321
+ print(f"⚠️ Server error {response.status_code}, retrying {retry_count}/{max_retries}")
322
+ time.sleep(2) # 等待2秒后重试
323
+ continue
324
+ else:
325
+ return None, f"HTTP Error after {max_retries} retries: {response.status_code}"
326
+ else:
327
+ return None, f"HTTP Error: {response.status_code}"
328
+
329
+ except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
330
+ retry_count += 1
331
+ if retry_count < max_retries:
332
+ print(f"⚠️ Network error, retrying {retry_count}/{max_retries}: {e}")
333
+ time.sleep(2)
334
+ continue
335
+ else:
336
+ return None, f"Network error after {max_retries} retries: {str(e)}"
337
+ except Exception as e:
338
+ return None, f"Request Exception: {str(e)}"
339
+
340
+ return None, f"Failed after {max_retries} retries"
341
+
342
+
343
+ def check_task_status(task_id):
344
+ """
345
+ Query task status with improved error handling using API v2
346
+ """
347
+ headers = {
348
+ 'Content-Type': 'application/json',
349
+ 'Authorization': f'Bearer {APIKEY}'
350
+ }
351
+
352
+ data = {
353
+ "task_id": task_id
354
+ }
355
+
356
+ retry_count = 0
357
+ max_retries = 2 # 状态查询重试次数少一些
358
+
359
+ while retry_count < max_retries:
360
+ try:
361
+ response = requests.post(
362
+ f'{UKAPIURL}/status_image_edit_v2',
363
+ headers=headers,
364
+ json=data,
365
+ timeout=15 # 状态查询超时时间短一些
366
+ )
367
+
368
+ if response.status_code == 200:
369
+ result = response.json()
370
+ if result.get('code') == 0:
371
+ task_data = result['data']
372
+ status = task_data['status']
373
+ image_url = task_data.get('image_url')
374
+
375
+ # Extract and log queue information
376
+ queue_info = task_data.get('queue_info', {})
377
+ if queue_info:
378
+ tasks_ahead = queue_info.get('tasks_ahead', 0)
379
+ current_priority = queue_info.get('current_priority', 0)
380
+ print(f"📊 Queue Info - Tasks ahead: {tasks_ahead}, Priority: {current_priority}")
381
+
382
+ return status, image_url, task_data
383
+ else:
384
+ return 'error', None, result.get('message', 'Unknown error')
385
+ elif response.status_code in [502, 503, 504]: # 服务器错误,可以重试
386
+ retry_count += 1
387
+ if retry_count < max_retries:
388
+ print(f"⚠️ Status check server error {response.status_code}, retrying {retry_count}/{max_retries}")
389
+ time.sleep(1) # 状态查询重试间隔短一些
390
+ continue
391
+ else:
392
+ return 'error', None, f"HTTP Error after {max_retries} retries: {response.status_code}"
393
+ else:
394
+ return 'error', None, f"HTTP Error: {response.status_code}"
395
+
396
+ except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
397
+ retry_count += 1
398
+ if retry_count < max_retries:
399
+ print(f"⚠️ Status check network error, retrying {retry_count}/{max_retries}: {e}")
400
+ time.sleep(1)
401
+ continue
402
+ else:
403
+ return 'error', None, f"Network error after {max_retries} retries: {str(e)}"
404
+ except Exception as e:
405
+ return 'error', None, f"Request Exception: {str(e)}"
406
+
407
+ return 'error', None, f"Failed after {max_retries} retries"
408
+
409
+
410
+ def process_multi_image_edit(img_inputs, prompt, width=0, height=0, progress_callback=None):
411
+ """
412
+ Complete process for multi-image editing
413
+
414
+ Args:
415
+ img_inputs: List of image inputs (PIL Image objects or file paths), 2-3 images
416
+ prompt: Editing instructions
417
+ width: Output width (0 for auto)
418
+ height: Output height (0 for auto)
419
+ progress_callback: Progress callback function
420
+ """
421
+ try:
422
+ # Generate client IP and timestamp
423
+ client_ip = "127.0.0.1" # Default IP
424
+ time_id = int(time.time())
425
+
426
+ # Validate input images
427
+ if not img_inputs or len(img_inputs) < 2:
428
+ return None, "Please upload at least 2 images", None
429
+
430
+ if len(img_inputs) > 3:
431
+ return None, "Maximum 3 images allowed", None
432
+
433
+ # Process input images - supports PIL Image and file path
434
+ uploaded_urls = []
435
+
436
+ if progress_callback:
437
+ progress_callback("uploading images...")
438
+
439
+ for i, img_input in enumerate(img_inputs):
440
+ if img_input is None:
441
+ continue
442
+
443
+ if hasattr(img_input, 'save'): # PIL Image object
444
+ pil_image = img_input
445
+ print(f"💾 Using PIL Image {i+1} directly from memory")
446
+ else:
447
+ # Load from file path
448
+ pil_image = Image.open(img_input)
449
+ print(f"📁 Loaded image {i+1} from file: {img_input}")
450
+
451
+ # Upload user image directly from memory
452
+ uploaded_url = upload_user_img_r2(client_ip, time_id + i, pil_image)
453
+ if not uploaded_url:
454
+ return None, f"Image {i+1} upload failed", None
455
+
456
+ # Extract actual image URL from upload URL
457
+ if "?" in uploaded_url:
458
+ uploaded_url = uploaded_url.split("?")[0]
459
+
460
+ uploaded_urls.append(uploaded_url)
461
+
462
+ # Ensure we have the required URLs
463
+ user_image_url = uploaded_urls[0] if len(uploaded_urls) > 0 else ""
464
+ user_image2_url = uploaded_urls[1] if len(uploaded_urls) > 1 else ""
465
+ user_image3_url = uploaded_urls[2] if len(uploaded_urls) > 2 else ""
466
+
467
+ if progress_callback:
468
+ progress_callback("submitting multi-image edit task...")
469
+
470
+ # Submit multi-image editing task
471
+ task_id, error = submit_image_edit_task(
472
+ user_image_url,
473
+ prompt,
474
+ task_type="80",
475
+ mask_image_url="",
476
+ user_image2_url=user_image2_url,
477
+ user_image3_url=user_image3_url,
478
+ width=width,
479
+ height=height
480
+ )
481
+ if error:
482
+ return None, error, None
483
+
484
+ if progress_callback:
485
+ progress_callback(f"task submitted, ID: {task_id}, processing...")
486
+
487
+ # Wait for task completion
488
+ max_attempts = 60 # Wait up to 10 minutes
489
+ task_uuid = None
490
+ for attempt in range(max_attempts):
491
+ status, output_url, task_data = check_task_status(task_id)
492
+
493
+ # Extract task_uuid from task_data
494
+ if task_data and isinstance(task_data, dict):
495
+ task_uuid = task_data.get('uuid', None)
496
+
497
+ if status == 'completed':
498
+ if output_url:
499
+ return output_url, "multi-image edit completed", task_uuid
500
+ else:
501
+ return None, "Task completed but no result image returned", task_uuid
502
+ elif status == 'error' or status == 'failed':
503
+ return None, f"task processing failed: {task_data}", task_uuid
504
+ elif status in ['queued', 'processing', 'running', 'created', 'working']:
505
+ if progress_callback:
506
+ progress_callback(f"multi-image processing... (status: {status})")
507
+ time.sleep(1)
508
+ else:
509
+ if progress_callback:
510
+ progress_callback(f"unknown status: {status}")
511
+ time.sleep(1)
512
+
513
+ return None, "task processing timeout", task_uuid
514
+
515
+ except Exception as e:
516
+ return None, f"error occurred during processing: {str(e)}", None
517
+
518
+
519
+ def process_image_edit(img_input, prompt, progress_callback=None):
520
+ """
521
+ Complete process for single image editing (backward compatibility)
522
+
523
+ Args:
524
+ img_input: Can be file path (str) or PIL Image object
525
+ prompt: Editing instructions
526
+ progress_callback: Progress callback function
527
+ """
528
+ return process_multi_image_edit([img_input], prompt, 0, 0, progress_callback)
529
+
530
+
531
+ def process_local_image_edit(base_image, layers, prompt, progress_callback=None):
532
+ """
533
+ 处理局部图片编辑的完整流程
534
+
535
+ Args:
536
+ base_image (PIL.Image): 原始图片
537
+ layers (list): ImageEditor的层数据
538
+ prompt (str): 编辑指令
539
+ progress_callback: 进度回调函数
540
+ """
541
+ try:
542
+ # Generate client IP and timestamp
543
+ client_ip = "127.0.0.1" # Default IP
544
+ time_id = int(time.time())
545
+
546
+ if progress_callback:
547
+ progress_callback("creating mask image...")
548
+
549
+ # 从layers创建mask图片
550
+ mask_image = create_mask_from_layers(base_image, layers)
551
+
552
+ # 检查mask是否有内容
553
+ mask_array = np.array(mask_image)
554
+ if np.max(mask_array) == 0:
555
+ return None, "please draw mask", None
556
+
557
+ print(f"📝 创建mask图片成功,绘制区域像素数: {np.sum(mask_array > 0)}")
558
+
559
+ if progress_callback:
560
+ progress_callback("uploading original image...")
561
+
562
+ # 直接从内存上传原始图片
563
+ uploaded_url = upload_user_img_r2(client_ip, time_id, base_image)
564
+ if not uploaded_url:
565
+ return None, "original image upload failed", None
566
+
567
+ # 从上传 URL 中提取实际的图片 URL
568
+ if "?" in uploaded_url:
569
+ uploaded_url = uploaded_url.split("?")[0]
570
+
571
+ if progress_callback:
572
+ progress_callback("uploading mask image...")
573
+
574
+ # 直接从内存上传mask图片
575
+ mask_url = upload_mask_image_r2(client_ip, time_id, mask_image)
576
+ if not mask_url:
577
+ return None, "mask image upload failed", None
578
+
579
+ # 从上传 URL 中提取实际的图片 URL
580
+ if "?" in mask_url:
581
+ mask_url = mask_url.split("?")[0]
582
+
583
+ print(f"📤 图片上传成功:")
584
+ print(f" 原始图片: {uploaded_url}")
585
+ print(f" Mask图片: {mask_url}")
586
+
587
+ if progress_callback:
588
+ progress_callback("submitting local edit task...")
589
+
590
+ # 提交局部图片编辑任务 (task_type=81)
591
+ task_id, error = submit_image_edit_task(uploaded_url, prompt, task_type="81", mask_image_url=mask_url)
592
+ if error:
593
+ return None, error, None
594
+
595
+ if progress_callback:
596
+ progress_callback(f"task submitted, ID: {task_id}, processing...")
597
+
598
+ print(f"🚀 局部编辑任务已提交,任务ID: {task_id}")
599
+
600
+ # Wait for task completion
601
+ max_attempts = 60 # Wait up to 10 minutes
602
+ task_uuid = None
603
+ for attempt in range(max_attempts):
604
+ status, output_url, task_data = check_task_status(task_id)
605
+
606
+ # Extract task_uuid from task_data
607
+ if task_data and isinstance(task_data, dict):
608
+ task_uuid = task_data.get('uuid', None)
609
+
610
+ if status == 'completed':
611
+ if output_url:
612
+ print(f"✅ 局部编辑任务完成,结果: {output_url}")
613
+ return output_url, "local image edit completed", task_uuid
614
+ else:
615
+ return None, "task completed but no result image returned", task_uuid
616
+ elif status == 'error' or status == 'failed':
617
+ return None, f"task processing failed: {task_data}", task_uuid
618
+ elif status in ['queued', 'processing', 'running', 'created', 'working']:
619
+ if progress_callback:
620
+ progress_callback(f"processing... (status: {status})")
621
+ time.sleep(1) # Wait 1 second before retry
622
+ else:
623
+ if progress_callback:
624
+ progress_callback(f"unknown status: {status}")
625
+ time.sleep(1)
626
+
627
+ return None, "task processing timeout", task_uuid
628
+
629
+ except Exception as e:
630
+ print(f"❌ 局部编辑处理异常: {str(e)}")
631
+ return None, f"error occurred during processing: {str(e)}", None
632
+
633
+
634
+ def download_and_check_result_nsfw(image_url, nsfw_detector=None):
635
+ """
636
+ 下载结果图片并进行NSFW检测
637
+
638
+ Args:
639
+ image_url (str): 结果图片URL
640
+ nsfw_detector: NSFW检测器实例
641
+
642
+ Returns:
643
+ tuple: (is_nsfw, error_message)
644
+ """
645
+ if nsfw_detector is None:
646
+ return False, None
647
+
648
+ try:
649
+ # 下载图片
650
+ response = requests.get(image_url, timeout=30)
651
+ if response.status_code != 200:
652
+ return False, f"Failed to download result image: HTTP {response.status_code}"
653
+
654
+ # 将图片数据转换为PIL Image
655
+ image_data = io.BytesIO(response.content)
656
+ result_image = Image.open(image_data)
657
+
658
+ # 进行NSFW检测
659
+ nsfw_result = nsfw_detector.predict_pil_label_only(result_image)
660
+
661
+ is_nsfw = nsfw_result.lower() == "nsfw"
662
+ print(f"🔍 结果图片NSFW检测: {'❌❌❌ ' + nsfw_result if is_nsfw else '✅✅✅ ' + nsfw_result}")
663
+
664
+ return is_nsfw, None
665
+
666
+ except Exception as e:
667
+ print(f"⚠️ 结果图片NSFW检测失败: {e}")
668
+ return False, f"Failed to check result image: {str(e)}"
669
+
670
+
671
+ if __name__ == "__main__":
672
+
673
+ pass