Opera8 commited on
Commit
72d32a7
·
verified ·
1 Parent(s): 5c68bd2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +386 -536
app.py CHANGED
@@ -10,8 +10,12 @@ from gradio.themes import Soft
10
  from gradio.themes.utils import colors, fonts, sizes
11
  from deep_translator import GoogleTranslator
12
  from transformers import pipeline
 
 
13
 
14
- # --- تنظیمات تم و رنگ ---
 
 
15
  colors.orange_red = colors.Color(
16
  name="orange_red",
17
  c50="#FFF0E5",
@@ -27,15 +31,114 @@ colors.orange_red = colors.Color(
27
  c950="#802200",
28
  )
29
 
30
- # --- بارگذاری مدل تشخیص محتوای نامناسب (NSFW) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  print("Loading Safety Checker...")
32
- try:
33
- safety_classifier = pipeline("image-classification", model="Falconsai/nsfw_image_detection", device=-1)
34
- except:
35
- safety_classifier = None
36
 
37
  def is_image_nsfw(image):
38
- if image is None or safety_classifier is None: return False
39
  try:
40
  results = safety_classifier(image)
41
  for result in results:
@@ -46,24 +149,34 @@ def is_image_nsfw(image):
46
  print(f"Safety check error: {e}")
47
  return False
48
 
49
- # --- توابع کمکی متن ---
50
  BANNED_WORDS = [
51
- "nude", "naked", "sex", "porn", "undressed", "nsfw", "erotic", "xxx",
52
- "breast", "nipple", "genital", "vagina", "penis", "ass", "butt", "sexual",
53
- "lingerie", "bikini", "swimwear", "underwear", "fetish", "topless",
54
- "exhibitionism", "hentai", "ecchi", "18+"
 
 
 
 
 
 
 
 
55
  ]
56
 
57
  def check_text_safety(text):
 
58
  text_lower = text.lower()
 
 
 
59
  for word in BANNED_WORDS:
60
- if f" {word} " in f" {text_lower} ":
61
  return False
62
  return True
63
 
64
  def translate_prompt(text):
65
- if not text:
66
- return ""
67
  try:
68
  translated = GoogleTranslator(source='auto', target='en').translate(text)
69
  return translated
@@ -71,442 +184,17 @@ def translate_prompt(text):
71
  print(f"Translation Error: {e}")
72
  return text
73
 
74
- # --- تنظیمات HTML/JS برای مدیریت خطا (نسخه اصلاح شده و قوی) ---
75
- # این بخش دقیقاً همان لاجیک برنامه قبلی برای حذف خطا و نمایش پنجره راهنماست
76
- js_global_content = """
77
- <script>
78
- document.addEventListener('DOMContentLoaded', () => {
79
- // 1. Force Light Mode
80
- const forceLight = () => {
81
- const body = document.querySelector('body');
82
- if (body) {
83
- body.classList.remove('dark');
84
- body.style.backgroundColor = '#f5f7fa';
85
- body.style.color = '#333333';
86
- }
87
- document.querySelectorAll('.dark').forEach(el => el.classList.remove('dark'));
88
- };
89
- forceLight();
90
- setInterval(forceLight, 1000);
91
-
92
- // 2. RETRY FUNCTION
93
- window.retryGeneration = function() {
94
- const modal = document.getElementById('custom-quota-modal');
95
- if (modal) modal.remove();
96
-
97
- const runBtn = document.getElementById('run-btn');
98
- if(runBtn) runBtn.click();
99
- };
100
-
101
- // Close function
102
- window.closeErrorModal = function() {
103
- const modal = document.getElementById('custom-quota-modal');
104
- if (modal) modal.remove();
105
- };
106
-
107
- // 3. SHOW MODAL FUNCTION
108
- const showQuotaModal = () => {
109
- if (document.getElementById('custom-quota-modal')) return;
110
-
111
- const modalHtml = `
112
- <div id="custom-quota-modal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); backdrop-filter: blur(5px); z-index: 99999; display: flex; align-items: center; justify-content: center; font-family: 'Vazirmatn', sans-serif;">
113
- <div class="ip-reset-guide-container" style="direction: rtl; text-align: right; max-width: 450px; width: 90%; background: white; border-radius: 20px; padding: 25px; box-shadow: 0 10px 40px rgba(0,0,0,0.3); animation: slideInUp 0.4s ease;">
114
- <div class="guide-header">
115
- <svg class="guide-header-icon" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
116
- <defs><lineargradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color: #667eea; stop-opacity: 1;"></stop><stop offset="100%" style="stop-color: #764ba2; stop-opacity: 1;"></stop></lineargradient></defs>
117
- <circle cx="50" cy="50" r="45" fill="url(#grad1)" opacity="0.1"></circle>
118
- <circle cx="50" cy="50" r="35" fill="none" stroke="url(#grad1)" stroke-width="2" opacity="0.3"></circle>
119
- <path d="M35 50 L45 60 L65 40" stroke="url(#grad1)" stroke-width="4" fill="none" stroke-linecap="round" stroke-linejoin="round"></path>
120
- <circle cx="65" cy="35" r="8" fill="#fee140"></circle>
121
- <path d="M62 35 L68 35 M65 32 L65 38" stroke="white" stroke-width="2" stroke-linecap="round"></path>
122
- </svg>
123
- <div>
124
- <h2>یک قدم تا ساخت تصاویر جدید</h2>
125
- <p>نیازمند تغییر نقطه دستیابی</p>
126
- </div>
127
- </div>
128
-
129
- <div class="guide-content">
130
- <div class="info-card">
131
- <div class="info-card-header">
132
- <svg class="info-card-icon" viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" fill="#667eea" opacity="0.2"></path><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" stroke="#667eea" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
133
- <span class="info-card-title">راه حل سریع</span>
134
- </div>
135
- <p>طبق ویدیو آموزشی پایین بین نقطه دستیابی جابجا شوید تلاش مجدد بزنید تا تصاویر مجدداً تولید بشه.</p>
136
- </div>
137
-
138
- <div class="summary-section">
139
- <div class="summary-header">
140
- <svg class="summary-icon" viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="10" fill="#56ab2f" opacity="0.2"></circle><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z" stroke="#56ab2f" stroke-width="2"></path><path d="M9 12l2 2 4-4" stroke="#56ab2f" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
141
- <span class="summary-title">خلاصه راهنما</span>
142
- </div>
143
- <div class="summary-text">هربار که این صفحه را مشاهده کردید: از اینترنت سیم‌کارت استفاده کنید، VPN را خاموش کرده و طبق ویدیو آموزشی پایین نقطه دستیابی رو تغییر دهید. «تلاش مجدد» کلیک کنید. با این روش ساده می‌توانید به صورت نامحدود تصویر بسازید! ☘️</div>
144
- </div>
145
-
146
- <div class="video-button-container">
147
- <button onclick="parent.postMessage({ type: 'NAVIGATE_TO_URL', url: '#/nav/online/news/getSingle/1149635/eyJpdiI6IjhHVGhPQWJwb3E0cjRXbnFWTW5BaUE9PSIsInZhbHVlIjoiS1V0dTdvT21wbXAwSXZaK1RCTG1pVXZqdlFJa1hXV1RKa2FLem9zU3pXMjd5MmlVOGc2YWY0NVdNR3h3Smp1aSIsIm1hYyI6IjY1NTA5ZDYzMjAzMTJhMGQyMWQ4NjA4ZDgyNGZjZDVlY2MyNjdiMjA2NWYzOWRjY2M4ZmVjYWRlMWNlMWQ3ODEiLCJ0YWciOiIifQ==/21135210' }, '*')" class="elegant-video-button">
148
- <svg class="elegant-video-button-icon" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M8 5v14l11-7z"></path></svg>
149
- <span>دیدن ویدیو آموزشی استفاده نامحدود</span>
150
- </button>
151
- </div>
152
- </div>
153
-
154
- <div class="guide-actions">
155
- <button class="action-button back-button" onclick="window.closeErrorModal()">
156
- <svg class="action-button-icon" viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19 12H5M12 19l-7-7 7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
157
- <span>بازگشت</span>
158
- </button>
159
- <button class="action-button retry-button" onclick="window.retryGeneration()">
160
- <svg class="action-button-icon" viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M23 4v6h-6M1 20v-6h6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
161
- <span>تلاش مجدد</span>
162
- </button>
163
- </div>
164
- </div>
165
- </div>
166
- `;
167
-
168
- document.body.insertAdjacentHTML('beforeend', modalHtml);
169
- };
170
-
171
- // 4. SCANNER
172
- setInterval(() => {
173
- const potentialErrors = document.querySelectorAll('.toast-body, .error, .toast-wrap, .eta-bar, div[class*="error"]');
174
-
175
- potentialErrors.forEach(el => {
176
- const text = el.innerText || "";
177
- if (text.toLowerCase().includes('quota') || text.toLowerCase().includes('exceeded')) {
178
-
179
- showQuotaModal();
180
-
181
- // Immediately hide the Gradio error
182
- el.style.display = 'none';
183
- el.style.opacity = '0';
184
- el.innerText = '';
185
-
186
- const parentWrap = el.closest('.toast-wrap');
187
- if(parentWrap) parentWrap.style.display = 'none';
188
- }
189
- });
190
- }, 100);
191
- });
192
- </script>
193
- """
194
-
195
- # --- CSS Updated ---
196
- css_code = """
197
- <style>
198
- @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700&display=swap');
199
-
200
- :root, .dark, body, .gradio-container {
201
- --body-background-fill: #f5f7fa !important;
202
- --body-text-color: #1f2937 !important;
203
- --background-fill-primary: #ffffff !important;
204
- --background-fill-secondary: #f3f4f6 !important;
205
- --border-color-primary: #e5e7eb !important;
206
- --block-background-fill: #ffffff !important;
207
- --block-label-text-color: #374151 !important;
208
- --block-title-text-color: #111827 !important;
209
- --input-background-fill: #ffffff !important;
210
- color-scheme: light !important;
211
- }
212
-
213
- /* --- IP Reset Guide CSS --- */
214
- :root {
215
- --guide-bg: rgba(255, 255, 255, 0.98);
216
- --guide-border: rgba(102, 126, 234, 0.2);
217
- --guide-text-title: #2d3748;
218
- --guide-text-body: #4a5568;
219
- --guide-accent: #667eea;
220
- --primary-gradient-guide: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
221
- --success-gradient-guide: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%);
222
- --radius-md-guide: 12px;
223
- --radius-lg-guide: 20px;
224
- --shadow-sm: 0 1px 2px 0 rgba(26, 32, 44, 0.03);
225
- --shadow-md: 0 4px 6px -1px rgba(26, 32, 44, 0.05), 0 2px 4px -2px rgba(26, 32, 44, 0.04);
226
- --shadow-xl: 0 20px 25px -5px rgba(26, 32, 44, 0.07), 0 8px 10px -6px rgba(26, 32, 44, 0.05);
227
- }
228
-
229
- @keyframes float {
230
- 0%, 100% { transform: translateY(0px); }
231
- 50% { transform: translateY(-10px); }
232
- }
233
- @keyframes slideInUp {
234
- from { opacity: 0; transform: translateY(30px); }
235
- to { opacity: 1; transform: translateY(0); }
236
- }
237
-
238
- .guide-header { display: flex; align-items: center; margin-bottom: 20px; }
239
- .guide-header-icon { width: 50px; height: 50px; margin-left: 15px; animation: float 3s ease-in-out infinite; flex-shrink: 0; }
240
- .guide-header h2 { font-size: 1.3rem; color: var(--guide-text-title); font-weight: 700; margin: 0; }
241
- .guide-header p { color: var(--guide-text-body); font-size: 0.8rem; margin-top: 5px; margin-bottom: 0; }
242
-
243
- .guide-content { font-size: 0.9rem; color: var(--guide-text-body); line-height: 1.6; }
244
-
245
- .info-card { background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%); border: 1px solid rgba(102, 126, 234, 0.2); border-radius: var(--radius-md-guide); padding: 15px; margin: 15px 0; position: relative; overflow: hidden; }
246
- .info-card p { font-size: 0.85rem; line-height: 1.6; margin: 0; }
247
- .info-card::before { content: ''; position: absolute; top: 0; right: 0; width: 4px; height: 100%; background: var(--primary-gradient-guide); }
248
- .info-card-header { display: flex; align-items: center; margin-bottom: 10px; }
249
- .info-card-icon { width: 20px; height: 20px; margin-left: 10px; }
250
- .info-card-title { font-weight: 600; color: var(--guide-text-title); font-size: 0.95rem; }
251
-
252
- .summary-section { margin-top: 15px; padding: 15px; border-radius: var(--radius-md-guide); background: linear-gradient(135deg, #56ab2f15 0%, #a8e06315 100%); border: 1px solid rgba(86, 171, 47, 0.2); position: relative; overflow: hidden; }
253
- .summary-section::before { content: ''; position: absolute; top: 0; right: 0; width: 4px; height: 100%; background: var(--success-gradient-guide); }
254
- .summary-header { display: flex; align-items: center; margin-bottom: 8px; }
255
- .summary-icon { width: 20px; height: 20px; margin-left: 10px; }
256
- .summary-title { font-weight: 600; color: #2f5a33; font-size: 0.95rem; }
257
- .summary-text { color: #2f5a33; font-size: 0.85rem; line-height: 1.6; }
258
-
259
- /* Tutorial Button */
260
- .video-button-container { text-align: center; margin: 25px 0 15px 0; width: 100%; }
261
- .elegant-video-button {
262
- display: inline-flex !important;
263
- align-items: center;
264
- justify-content: center;
265
- padding: 10px 24px !important;
266
- background-color: #fff !important;
267
- color: var(--guide-accent) !important;
268
- border: 1px solid #e2e8f0 !important;
269
- text-decoration: none;
270
- border-radius: 50px !important;
271
- font-weight: 600 !important;
272
- font-size: 0.9rem !important;
273
- cursor: pointer !important;
274
- font-family: inherit;
275
- transition: all 0.3s ease !important;
276
- box-shadow: 0 2px 10px rgba(0,0,0,0.05) !important;
277
- width: auto !important;
278
- }
279
- .elegant-video-button:hover {
280
- background: var(--primary-gradient-guide) !important;
281
- color: white !important;
282
- border-color: transparent !important;
283
- transform: translateY(-2px);
284
- box-shadow: 0 6px 16px rgba(102, 126, 234, 0.3) !important;
285
- }
286
- .elegant-video-button-icon { width: 18px; height: 18px; margin-left: 8px; fill: currentColor; }
287
-
288
- /* Action Buttons */
289
- .guide-actions {
290
- display: flex !important;
291
- gap: 12px !important;
292
- margin-top: 20px;
293
- padding-top: 20px;
294
- border-top: 1px solid #e2e8f0;
295
- width: 100% !important;
296
- }
297
- .action-button {
298
- padding: 12px 15px !important;
299
- border: none !important;
300
- border-radius: 12px !important;
301
- font-size: 0.95rem !important;
302
- font-weight: 600 !important;
303
- cursor: pointer !important;
304
- flex: 1 !important;
305
- transition: all 0.3s ease !important;
306
- display: flex !important;
307
- align-items: center;
308
- justify-content: center;
309
- font-family: inherit;
310
- height: 48px !important;
311
- }
312
- .action-button-icon { width: 20px; height: 20px; margin-right: 0; margin-left: 8px; }
313
-
314
- .back-button {
315
- background: white !important;
316
- color: var(--guide-text-body) !important;
317
- border: 2px solid #e2e8f0 !important;
318
- }
319
- .back-button:hover {
320
- background: #f7fafc !important;
321
- border-color: var(--guide-accent) !important;
322
- transform: translateY(-2px);
323
- box-shadow: var(--shadow-md) !important;
324
- }
325
-
326
- .retry-button {
327
- background: var(--primary-gradient-guide) !important;
328
- color: white !important;
329
- box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3) !important;
330
- }
331
- .retry-button:hover {
332
- transform: translateY(-2px);
333
- box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4) !important;
334
- }
335
-
336
- /* --- Main App CSS --- */
337
- body {
338
- font-family: 'Vazirmatn', sans-serif !important;
339
- background-color: #f5f7fa !important;
340
- margin: 0;
341
- padding: 10px;
342
- }
343
-
344
- #col-container {
345
- margin: 0 auto;
346
- max-width: 980px;
347
- direction: rtl;
348
- text-align: right;
349
- padding: 30px;
350
- background: #ffffff !important;
351
- border-radius: 24px;
352
- box-shadow: 0 10px 40px -10px rgba(0,0,0,0.08);
353
- border: 1px solid rgba(255,255,255,0.8);
354
- }
355
-
356
- #main-title h1 {
357
- font-size: 2.4em !important;
358
- text-align: center;
359
- color: #1a202c !important;
360
- margin-bottom: 15px;
361
- font-weight: 800;
362
- background: -webkit-linear-gradient(45deg, #2563eb, #1e40af);
363
- -webkit-background-clip: text;
364
- -webkit-text-fill-color: transparent;
365
- }
366
-
367
- #main-description {
368
- text-align: center;
369
- font-size: 1.15em;
370
- color: #4b5563 !important;
371
- margin-bottom: 40px;
372
- line-height: 1.6;
373
- }
374
-
375
- .gr-input-label, span.label-wrap, label span {
376
- font-weight: 700 !important;
377
- color: #374151 !important;
378
- font-size: 0.95em !important;
379
- margin-bottom: 8px !important;
380
- }
381
-
382
- textarea, input[type="text"] {
383
- border: 2px solid #e2e8f0 !important;
384
- border-radius: 12px !important;
385
- background-color: #ffffff !important;
386
- color: #111827 !important;
387
- padding: 12px !important;
388
- transition: all 0.3s ease;
389
- font-family: 'Vazirmatn', sans-serif !important;
390
- }
391
-
392
- textarea:focus, input[type="text"]:focus {
393
- border-color: #3b82f6 !important;
394
- box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1) !important;
395
- outline: none;
396
- }
397
-
398
- .gr-dropdown {
399
- background: #ffffff !important;
400
- border-radius: 12px !important;
401
- }
402
-
403
- .primary-btn, button.primary {
404
- background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important;
405
- border: none !important;
406
- color: white !important;
407
- font-weight: 700 !important;
408
- font-size: 1.1em !important;
409
- padding: 14px 28px !important;
410
- border-radius: 14px !important;
411
- box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3) !important;
412
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
413
- cursor: pointer !important;
414
- width: 100%;
415
- margin-top: 15px;
416
- }
417
-
418
- .primary-btn:hover, button.primary:hover {
419
- transform: translateY(-2px);
420
- box-shadow: 0 8px 25px rgba(16, 185, 129, 0.45) !important;
421
- }
422
-
423
- .primary-btn:active, button.primary:active {
424
- transform: translateY(1px);
425
- }
426
-
427
- #download-btn {
428
- background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
429
- box-shadow: 0 4px 15px rgba(59, 130, 246, 0.3) !important;
430
- }
431
- #download-btn:hover {
432
- box-shadow: 0 8px 25px rgba(59, 130, 246, 0.45) !important;
433
- }
434
-
435
- .gradio-container .prose table,
436
- .gradio-container table {
437
- background-color: #ffffff !important;
438
- color: #111827 !important;
439
- border: 1px solid #e5e7eb !important;
440
- border-radius: 12px !important;
441
- overflow: hidden !important;
442
- width: 100% !important;
443
- margin-top: 20px !important;
444
- }
445
-
446
- .gradio-container thead th {
447
- background-color: #f3f4f6 !important;
448
- color: #374151 !important;
449
- font-weight: 700 !important;
450
- border-bottom: 2px solid #e5e7eb !important;
451
- padding: 12px !important;
452
- text-align: right !important;
453
- }
454
-
455
- .gradio-container tbody tr {
456
- background-color: #ffffff !important;
457
- border-bottom: 1px solid #f3f4f6 !important;
458
- }
459
-
460
- .gradio-container tbody tr:hover {
461
- background-color: #f9fafb !important;
462
- }
463
-
464
- .gradio-container tbody td {
465
- background-color: #ffffff !important;
466
- color: #374151 !important;
467
- padding: 10px !important;
468
- }
469
-
470
- .gradio-container tbody td span,
471
- .gradio-container tbody td p {
472
- color: #374151 !important;
473
- }
474
-
475
- footer { display: none !important; }
476
- .flagging { display: none !important; }
477
-
478
- /* Force toast transparency */
479
- .toast-body {
480
- direction: rtl !important;
481
- text-align: right !important;
482
- background: transparent !important;
483
- box-shadow: none !important;
484
- border: none !important;
485
- padding: 0 !important;
486
- max-width: 100% !important;
487
- width: auto !important;
488
- }
489
- .toast-wrap {
490
- background: transparent !important;
491
- border: none !important;
492
- box-shadow: none !important;
493
- }
494
-
495
- @media (prefers-color-scheme: dark) {
496
- body, .gradio-container, .prose, table, tr, td, th {
497
- background-color: #ffffff !important;
498
- color: #333333 !important;
499
- }
500
- }
501
- </style>
502
- """
503
-
504
- # ادغام CSS و JS
505
- combined_html = css_code + js_global_content
506
-
507
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
508
  dtype = torch.bfloat16
509
 
 
 
 
 
 
510
  print("Loading Qwen Image Edit Pipeline...")
511
  pipe = QwenImageEditPlusPipeline.from_pretrained(
512
  "Qwen/Qwen-Image-Edit-2509",
@@ -535,17 +223,17 @@ try:
535
  pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
536
  print("Flash Attention 3 Processor set successfully.")
537
  except Exception as e:
538
- print(f"Could not set FA3 processor (likely hardware mismatch): {e}. using default attention.")
539
 
540
  MAX_SEED = np.iinfo(np.int32).max
541
 
542
  LORA_MAPPING_PERSIAN = {
543
- "ویرایش بافت (Texture)": "Texture Edit",
544
- "طراحی لباس (Shirt Design)": "Cloth-Design-Fuse",
545
- "ترکیب اشیاء (Fusion)": "Fuse-Objects",
546
- "ترکیب پیشرفته (Super Fusion)": "Super-Fusion",
547
- "انتقال نور (Light Migration)": "Light-Migration",
548
- "انتقال متریال (Material Transfer)": "Material-Transfer"
549
  }
550
 
551
  def update_dimensions_on_upload(image):
@@ -563,21 +251,34 @@ def update_dimensions_on_upload(image):
563
  new_height = (new_height // 16) * 16
564
  return new_width, new_height
565
 
 
 
 
566
  def get_error_html(message):
567
- return f"""
568
- <div style="background-color: #fee2e2; border: 1px solid #ef4444; color: #b91c1c; padding: 12px; border-radius: 8px; text-align: center; margin-bottom: 10px; font-weight: bold; display: flex; align-items: center; justify-content: center; gap: 8px; direction: rtl;">
569
- <span style="font-size: 1.2em;">⛔</span>
570
- {message}
571
- </div>
572
- """
573
 
574
  def get_success_html(message):
575
- return f"""
576
- <div style="background-color: #dcfce7; border: 1px solid #22c55e; color: #15803d; padding: 12px; border-radius: 8px; text-align: center; margin-bottom: 10px; font-weight: bold; display: flex; align-items: center; justify-content: center; gap: 8px; direction: rtl;">
577
- <span style="font-size: 1.2em;">✅</span>
578
- {message}
579
- </div>
580
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
581
 
582
  @spaces.GPU(duration=30)
583
  def infer(
@@ -589,19 +290,33 @@ def infer(
589
  randomize_seed,
590
  guidance_scale,
591
  steps,
 
 
592
  progress=gr.Progress(track_tqdm=True)
593
  ):
 
 
 
 
 
 
 
594
  if image_1 is None or image_2 is None:
595
- return None, seed, get_error_html("لطفاً هر دو تصویر (تصویر پایه و تصویر مرجع) را بارگذاری کنید.")
596
 
 
597
  if is_image_nsfw(image_1) or is_image_nsfw(image_2):
598
- return None, seed, get_error_html("تصویر ورودی دارای محتوای نامناسب است و پردازش نمی‌شود.")
599
 
 
600
  lora_adapter = LORA_MAPPING_PERSIAN.get(lora_adapter_persian, "Texture Edit")
601
 
602
  english_prompt = translate_prompt(prompt)
 
 
 
 
603
  if not english_prompt:
604
- # Default prompts based on internal logic
605
  if lora_adapter == "Cloth-Design-Fuse": english_prompt = "Put this design on their shirt."
606
  elif lora_adapter == "Texture Edit": english_prompt = "Apply texture to object."
607
  elif lora_adapter == "Fuse-Objects": english_prompt = "Fuse object into background."
@@ -609,9 +324,11 @@ def infer(
609
  elif lora_adapter == "Material-Transfer": english_prompt = "change materials of image1 to match the reference in image2"
610
  elif lora_adapter == "Light-Migration": english_prompt = "Refer to the color tone, remove the original lighting from Image 1, and relight Image 1 based on the lighting and color tone of Image 2."
611
 
612
- if not check_text_safety(english_prompt):
613
- return None, seed, get_error_html("متن درخواست شامل کلمات غیرمجاز است.")
 
614
 
 
615
  adapters_map = {
616
  "Texture Edit": "texture",
617
  "Fuse-Objects": "fusion",
@@ -620,7 +337,6 @@ def infer(
620
  "Material-Transfer": "material-transfer",
621
  "Light-Migration": "light-migration",
622
  }
623
-
624
  active_adapter = adapters_map.get(lora_adapter)
625
  if active_adapter:
626
  pipe.set_adapters([active_adapter], adapter_weights=[1.0])
@@ -650,116 +366,250 @@ def infer(
650
  true_cfg_scale=guidance_scale,
651
  ).images[0]
652
 
 
653
  if is_image_nsfw(result):
654
- return None, seed, get_error_html("تصویر خروجی حاوی محتوای نامناسب بود و حذف شد.")
 
 
 
 
 
 
 
 
 
 
655
 
656
- return result, seed, get_success_html("تصویر با موفقیت ترکیب و ویرایش شد.")
657
 
658
  except Exception as e:
659
  error_str = str(e)
660
  if "quota" in error_str.lower() or "exceeded" in error_str.lower():
661
- raise e
662
- return None, seed, get_error_html(f"خطا در پردازش: {error_str}")
663
 
664
  @spaces.GPU(duration=30)
665
- def infer_example(image_1, image_2, prompt, lora_adapter_persian):
666
- res, s, status = infer(image_1, image_2, prompt, lora_adapter_persian, 0, True, 1.0, 4)
667
- return res, s, status
668
 
669
- # --- رابط کاربری ---
 
 
670
  js_download_func = """
671
  async (image) => {
672
- if (!image) {
673
- alert("لطفاً ابتدا تصویر را تولید کنید.");
674
- return;
675
- }
676
  let fileUrl = image.url;
677
- if (fileUrl && !fileUrl.startsWith('http')) {
678
- fileUrl = window.location.origin + fileUrl;
679
- } else if (!fileUrl && image.path) {
680
- fileUrl = window.location.origin + "/file=" + image.path;
681
- }
682
- window.parent.postMessage({
683
- type: 'DOWNLOAD_REQUEST',
684
- url: fileUrl
685
- }, '*');
686
  }
687
  """
688
 
689
- with gr.Blocks() as demo:
690
- gr.HTML(combined_html)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
691
  with gr.Column(elem_id="col-container"):
692
- gr.Markdown("# **ویرایشگر هوشمند آلفا (ترکیبی)**", elem_id="main-title")
693
- gr.Markdown("با هوش مصنوعی آلفا تصاویر خود را با متد‌های مختلف (بافت، ترکیب، نورپردازی) ویرایش کنید.", elem_id="main-description")
694
-
 
695
  with gr.Row(equal_height=True):
696
  with gr.Column(scale=1):
697
  with gr.Row():
698
- image_1 = gr.Image(label="۱. تصویر پایه (اصلی)", type="pil", height=290)
699
- image_2 = gr.Image(label="۲. تصویر مرجع (استایل/بافت)", type="pil", height=290)
700
 
701
  prompt = gr.Text(
702
- label="دستور ویرایش (به فارسی)",
703
  show_label=True,
704
- placeholder="مثال: بافت چوب را روی لیوان اعمال کن...",
705
- rtl=True,
706
- lines=3
707
  )
708
 
709
  status_box = gr.HTML(label="وضعیت")
710
 
711
- # IMPORTANT: elem_id="run-btn" for JS
712
- run_button = gr.Button(" شروع پردازش و ترکیب تصاویر", variant="primary", elem_classes="primary-btn", elem_id="run-btn")
713
 
714
  with gr.Accordion("تنظیمات پیشرفته", open=False, visible=True):
715
- seed = gr.Slider(label="دانه تصادفی (Seed)", minimum=0, maximum=MAX_SEED, step=1, value=0)
716
- randomize_seed = gr.Checkbox(label="استفاده از Seed تصادفی", value=True)
717
- guidance_scale = gr.Slider(label="میزان وفاداری (Guidance Scale)", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
718
  steps = gr.Slider(label="تعداد مراحل (Steps)", minimum=1, maximum=50, step=1, value=4)
719
 
720
  with gr.Column(scale=1):
721
  output_image = gr.Image(label="تصویر نهایی", interactive=False, format="png", height=350)
722
- download_button = gr.Button("📥 دانلود و ذخیره تصویر", variant="secondary", elem_id="download-btn", elem_classes="primary-btn")
723
 
724
  with gr.Row():
725
  lora_adapter = gr.Dropdown(
726
- label="انتخاب سبک ویرایش",
727
  choices=list(LORA_MAPPING_PERSIAN.keys()),
728
- value="ویرایش بافت (Texture)",
729
  )
730
 
731
  gr.Examples(
732
  examples=[
733
- ["examples/M1.jpg", "examples/M2.jpg", "با توجه به تن رنگ، نورپردازی اصلی تصویر ۱ را حذف کن و بر اساس نور و رنگ تصویر ۲ مجدداً نورپردازی کن.", "انتقال نور (Light Migration)"],
734
- ["examples/Cloth2.jpg", "examples/Design2.png", "این طرح را روی پیراهن قرار بده.", "طراحی لباس (Shirt Design)"],
735
- ["examples/Cup1.png", "examples/Wood1.png", "بافت چوب را روی لیوان اعمال کن.", "ویرایش بافت (Texture)"],
736
- ["examples/Cloth1.jpg", "examples/Design1.png", "این طرح را روی پیراهن قرار بده.", "طراحی لباس (Shirt Design)"],
737
- ["examples/F3.jpg", "examples/F4.jpg", "عینک او را با عینک جدید از تصویر ۱ جایگزین کن.", "ترکیب پیشرفته (Super Fusion)"],
738
- ["examples/Chair.jpg", "examples/Material.jpg", "متریال تصویر ۱ را تغییر بده تا با مرجع تصویر ۲ مطابقت داشته باشد.", "انتقال متریال (Material Transfer)"],
739
- ["examples/F1.jpg", "examples/F2.jpg", "بطری کوچک را روی میز قرار بده.", "ترکیب پیشرفته (Super Fusion)"],
740
- ["examples/Mug1.jpg", "examples/Texture1.jpg", "طرح تصویر ۲ را روی لیوان اعمال کن.", "ویرایش بافت (Texture)"],
741
- ["examples/Cat1.jpg", "examples/Glass1.webp", "یک گربه که عینک زده است.", "ترکیب اشیاء (Fusion)"],
742
  ],
743
  inputs=[image_1, image_2, prompt, lora_adapter],
744
- outputs=[output_image, seed, status_box],
745
  fn=infer_example,
746
  cache_examples=False,
747
- label="نمونه‌ها (برای تست کلیک کنید)"
748
  )
749
 
 
 
 
750
  run_button.click(
751
  fn=infer,
752
- inputs=[image_1, image_2, prompt, lora_adapter, seed, randomize_seed, guidance_scale, steps],
753
- outputs=[output_image, seed, status_box],
754
- api_name="predict"
755
  )
756
-
757
- download_button.click(
758
- fn=None,
759
- inputs=[output_image],
760
- outputs=None,
761
- js=js_download_func
762
- )
763
-
764
  if __name__ == "__main__":
765
- demo.queue(max_size=50).launch(show_error=True)
 
10
  from gradio.themes.utils import colors, fonts, sizes
11
  from deep_translator import GoogleTranslator
12
  from transformers import pipeline
13
+ from datetime import date
14
+ import json
15
 
16
+ # ==========================================
17
+ # 1. تنظیمات تم (Orange Red Theme)
18
+ # ==========================================
19
  colors.orange_red = colors.Color(
20
  name="orange_red",
21
  c50="#FFF0E5",
 
31
  c950="#802200",
32
  )
33
 
34
+ class OrangeRedTheme(Soft):
35
+ def __init__(
36
+ self,
37
+ *,
38
+ primary_hue: colors.Color | str = colors.gray,
39
+ secondary_hue: colors.Color | str = colors.orange_red,
40
+ neutral_hue: colors.Color | str = colors.slate,
41
+ text_size: sizes.Size | str = sizes.text_lg,
42
+ font: fonts.Font | str | Iterable[fonts.Font | str] = (
43
+ fonts.GoogleFont("Outfit"), "Arial", "sans-serif",
44
+ ),
45
+ font_mono: fonts.Font | str | Iterable[fonts.Font | str] = (
46
+ fonts.GoogleFont("IBM Plex Mono"), "ui-monospace", "monospace",
47
+ ),
48
+ ):
49
+ super().__init__(
50
+ primary_hue=primary_hue,
51
+ secondary_hue=secondary_hue,
52
+ neutral_hue=neutral_hue,
53
+ text_size=text_size,
54
+ font=font,
55
+ font_mono=font_mono,
56
+ )
57
+ super().set(
58
+ background_fill_primary="*primary_50",
59
+ background_fill_primary_dark="*primary_900",
60
+ body_background_fill="linear-gradient(135deg, *primary_200, *primary_100)",
61
+ body_background_fill_dark="linear-gradient(135deg, *primary_900, *primary_800)",
62
+ button_primary_text_color="white",
63
+ button_primary_text_color_hover="white",
64
+ button_primary_background_fill="linear-gradient(90deg, *secondary_500, *secondary_600)",
65
+ button_primary_background_fill_hover="linear-gradient(90deg, *secondary_600, *secondary_700)",
66
+ button_primary_background_fill_dark="linear-gradient(90deg, *secondary_600, *secondary_700)",
67
+ button_primary_background_fill_hover_dark="linear-gradient(90deg, *secondary_500, *secondary_600)",
68
+ button_secondary_text_color="black",
69
+ button_secondary_text_color_hover="white",
70
+ button_secondary_background_fill="linear-gradient(90deg, *primary_300, *primary_300)",
71
+ button_secondary_background_fill_hover="linear-gradient(90deg, *primary_400, *primary_400)",
72
+ button_secondary_background_fill_dark="linear-gradient(90deg, *primary_500, *primary_600)",
73
+ button_secondary_background_fill_hover_dark="linear-gradient(90deg, *primary_500, *primary_500)",
74
+ slider_color="*secondary_500",
75
+ slider_color_dark="*secondary_600",
76
+ block_title_text_weight="600",
77
+ block_border_width="3px",
78
+ block_shadow="*shadow_drop_lg",
79
+ button_primary_shadow="*shadow_drop_lg",
80
+ button_large_padding="11px",
81
+ color_accent_soft="*primary_100",
82
+ block_label_background_fill="*primary_200",
83
+ )
84
+
85
+ orange_red_theme = OrangeRedTheme()
86
+
87
+ # ==========================================
88
+ # 2. تنظیمات سیستم اعتبار (Limit = 3)
89
+ # ==========================================
90
+ USAGE_LIMIT = 3
91
+ DATA_FILE = "usage_data_fusion.json"
92
+ PREMIUM_PAGE_ID = '1149636'
93
+
94
+ def load_usage_data():
95
+ if os.path.exists(DATA_FILE):
96
+ try:
97
+ with open(DATA_FILE, 'r') as f:
98
+ return json.load(f)
99
+ except:
100
+ return {}
101
+ return {}
102
+
103
+ def save_usage_data(data):
104
+ try:
105
+ with open(DATA_FILE, 'w') as f:
106
+ json.dump(data, f)
107
+ except Exception as e:
108
+ print(f"Error saving data: {e}")
109
+
110
+ usage_data_cache = load_usage_data()
111
+
112
+ def get_user_record(fingerprint):
113
+ global usage_data_cache
114
+ if not fingerprint: return None
115
+ usage_data_cache = load_usage_data()
116
+ today_str = date.today().isoformat()
117
+ user_record = usage_data_cache.get(fingerprint)
118
+ if not user_record or user_record.get("last_reset") != today_str:
119
+ return {"count": 0, "last_reset": today_str}
120
+ return user_record
121
+
122
+ def consume_quota(fingerprint):
123
+ global usage_data_cache
124
+ today_str = date.today().isoformat()
125
+ usage_data_cache = load_usage_data()
126
+ user_record = usage_data_cache.get(fingerprint)
127
+ if not user_record or user_record.get("last_reset") != today_str:
128
+ user_record = {"count": 0, "last_reset": today_str}
129
+ user_record["count"] += 1
130
+ usage_data_cache[fingerprint] = user_record
131
+ save_usage_data(usage_data_cache)
132
+ return user_record["count"]
133
+
134
+ # ==========================================
135
+ # 3. سیستم ایمنی (Safety)
136
+ # ==========================================
137
  print("Loading Safety Checker...")
138
+ safety_classifier = pipeline("image-classification", model="Falconsai/nsfw_image_detection", device=-1)
 
 
 
139
 
140
  def is_image_nsfw(image):
141
+ if image is None: return False
142
  try:
143
  results = safety_classifier(image)
144
  for result in results:
 
149
  print(f"Safety check error: {e}")
150
  return False
151
 
 
152
  BANNED_WORDS = [
153
+ "nsfw", "nude", "naked", "sex", "porn", "erotic", "xxx", "18+", "adult",
154
+ "explicit", "uncensored", "sexual", "lewd", "sensual", "lust", "horny",
155
+ "breast", "breasts", "nipple", "nipples", "vagina", "pussy", "cunt",
156
+ "penis", "dick", "cock", "genital", "genitals", "groin", "pubic",
157
+ "ass", "butt", "buttocks", "anus", "anal", "rectum",
158
+ "intercourse", "masturbation", "orgasm", "blowjob", "bj", "cum", "sperm",
159
+ "ejaculation", "penetration", "fucking", "sucking", "licking",
160
+ "lingerie", "bikini", "swimwear", "underwear", "panties", "bra", "thong",
161
+ "topless", "bottomless", "undressed", "unclothed", "skimpy", "transparent",
162
+ "see-through", "fetish", "bdsm", "bondage", "latex", "hentai", "ecchi", "ahegao",
163
+ "exhibitionism", "voyeur", "harem", "gore", "bloody", "blood", "kill", "murder",
164
+ "dead", "torture", "abuse"
165
  ]
166
 
167
  def check_text_safety(text):
168
+ if not text: return True
169
  text_lower = text.lower()
170
+ padded_text = f" {text_lower} "
171
+ for char in [".", ",", "!", "?", "-", "_", "(", ")", "[", "]", "{", "}"]:
172
+ padded_text = padded_text.replace(char, " ")
173
  for word in BANNED_WORDS:
174
+ if f" {word} " in padded_text:
175
  return False
176
  return True
177
 
178
  def translate_prompt(text):
179
+ if not text: return ""
 
180
  try:
181
  translated = GoogleTranslator(source='auto', target='en').translate(text)
182
  return translated
 
184
  print(f"Translation Error: {e}")
185
  return text
186
 
187
+ # ==========================================
188
+ # 4. بارگذاری مدل و LoRA ها
189
+ # ==========================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
191
  dtype = torch.bfloat16
192
 
193
+ from diffusers import FlowMatchEulerDiscreteScheduler
194
+ from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
195
+ from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
196
+ from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
197
+
198
  print("Loading Qwen Image Edit Pipeline...")
199
  pipe = QwenImageEditPlusPipeline.from_pretrained(
200
  "Qwen/Qwen-Image-Edit-2509",
 
223
  pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
224
  print("Flash Attention 3 Processor set successfully.")
225
  except Exception as e:
226
+ print(f"Could not set FA3 processor: {e}. using default attention.")
227
 
228
  MAX_SEED = np.iinfo(np.int32).max
229
 
230
  LORA_MAPPING_PERSIAN = {
231
+ "ویرایش بافت (Texture Edit)": "Texture Edit",
232
+ "طراحی روی لباس (Cloth-Design)": "Cloth-Design-Fuse",
233
+ "ترکیب اشیاء (Fuse-Objects)": "Fuse-Objects",
234
+ "ترکیب پیشرفته (Super-Fusion)": "Super-Fusion",
235
+ "انتقال نور (Light-Migration)": "Light-Migration",
236
+ "انتقال متریال (Material-Transfer)": "Material-Transfer"
237
  }
238
 
239
  def update_dimensions_on_upload(image):
 
251
  new_height = (new_height // 16) * 16
252
  return new_width, new_height
253
 
254
+ # ==========================================
255
+ # 5. HTML Helpers
256
+ # ==========================================
257
  def get_error_html(message):
258
+ return f"""<div style="background-color: #fee2e2; border: 1px solid #ef4444; color: #b91c1c; padding: 12px; border-radius: 8px; text-align: center; margin-bottom: 10px; font-weight: bold; display: flex; align-items: center; justify-content: center; gap: 8px;"><span style="font-size: 1.2em;">⛔</span>{message}</div>"""
 
 
 
 
 
259
 
260
  def get_success_html(message):
261
+ return f"""<div style="background-color: #dcfce7; border: 1px solid #22c55e; color: #15803d; padding: 12px; border-radius: 8px; text-align: center; margin-bottom: 10px; font-weight: bold; display: flex; align-items: center; justify-content: center; gap: 8px;"><span style="font-size: 1.2em;">✅</span>{message}</div>"""
262
+
263
+ def get_quota_exceeded_html():
264
+ return """<div style="background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%); border: 2px solid #f59e0b; padding: 20px; border-radius: 16px; text-align: center; box-shadow: 0 4px 15px rgba(245, 158, 11, 0.1);"><div style="font-size: 3rem; margin-bottom: 10px;">💎</div><h3 style="color: #92400e; margin: 0 0 10px 0; font-weight: 800;">اعتبار رایگان امروز تمام شد</h3><p style="color: #b45309; margin: 0; font-size: 0.95em;">شما از ۳ تصویر رایگان امروز استفاده کرده‌اید.<br>برای ساخت تصاویر نامحدود و حرفه‌ای، لطفا نسخه خود را ارتقا دهید.</p></div>"""
265
+
266
+ # ==========================================
267
+ # 6. منطق اصلی پردازش (Infer)
268
+ # ==========================================
269
+ def check_initial_quota(fingerprint, subscription_status):
270
+ if not fingerprint:
271
+ return gr.update(visible=True), gr.update(visible=False), None
272
+ if subscription_status == 'paid':
273
+ return gr.update(visible=True), gr.update(visible=False), None
274
+
275
+ user_record = get_user_record(fingerprint)
276
+ current_usage = user_record["count"] if user_record else 0
277
+
278
+ if current_usage >= USAGE_LIMIT:
279
+ return gr.update(visible=False), gr.update(visible=True), get_quota_exceeded_html()
280
+ else:
281
+ return gr.update(visible=True), gr.update(visible=False), None
282
 
283
  @spaces.GPU(duration=30)
284
  def infer(
 
290
  randomize_seed,
291
  guidance_scale,
292
  steps,
293
+ fingerprint,
294
+ subscription_status,
295
  progress=gr.Progress(track_tqdm=True)
296
  ):
297
+ # 1. بررسی اعتبار
298
+ if subscription_status != 'paid':
299
+ user_record = get_user_record(fingerprint)
300
+ if user_record and user_record["count"] >= USAGE_LIMIT:
301
+ return None, seed, get_quota_exceeded_html(), gr.update(visible=False), gr.update(visible=True)
302
+
303
+ # 2. بررسی ورودی‌ها
304
  if image_1 is None or image_2 is None:
305
+ return None, seed, get_error_html("لطفاً هر دو تصویر پایه و مرجع را بارگذاری کنید."), gr.update(visible=True), gr.update(visible=False)
306
 
307
+ # 3. بررسی ایمنی تصاویر ورودی
308
  if is_image_nsfw(image_1) or is_image_nsfw(image_2):
309
+ return None, seed, get_error_html("یکی از تصاویر ورودی حاوی محتوای نامناسب است."), gr.update(visible=True), gr.update(visible=False)
310
 
311
+ # 4. ترجمه و بررسی ایمنی متن
312
  lora_adapter = LORA_MAPPING_PERSIAN.get(lora_adapter_persian, "Texture Edit")
313
 
314
  english_prompt = translate_prompt(prompt)
315
+ if not check_text_safety(english_prompt):
316
+ return None, seed, get_error_html("متن درخواست شامل کلمات غیرمجاز است."), gr.update(visible=True), gr.update(visible=False)
317
+
318
+ # اگر پرامپت خالی بود، پرامپت پیش‌فرض انگلیسی تنظیم می‌شود (نیاز به ترجمه ندارد)
319
  if not english_prompt:
 
320
  if lora_adapter == "Cloth-Design-Fuse": english_prompt = "Put this design on their shirt."
321
  elif lora_adapter == "Texture Edit": english_prompt = "Apply texture to object."
322
  elif lora_adapter == "Fuse-Objects": english_prompt = "Fuse object into background."
 
324
  elif lora_adapter == "Material-Transfer": english_prompt = "change materials of image1 to match the reference in image2"
325
  elif lora_adapter == "Light-Migration": english_prompt = "Refer to the color tone, remove the original lighting from Image 1, and relight Image 1 based on the lighting and color tone of Image 2."
326
 
327
+ # 5. کسر اعتبار (اگر کاربر رایگان است)
328
+ if subscription_status != 'paid':
329
+ consume_quota(fingerprint)
330
 
331
+ # 6. تنظیم آداپتر
332
  adapters_map = {
333
  "Texture Edit": "texture",
334
  "Fuse-Objects": "fusion",
 
337
  "Material-Transfer": "material-transfer",
338
  "Light-Migration": "light-migration",
339
  }
 
340
  active_adapter = adapters_map.get(lora_adapter)
341
  if active_adapter:
342
  pipe.set_adapters([active_adapter], adapter_weights=[1.0])
 
366
  true_cfg_scale=guidance_scale,
367
  ).images[0]
368
 
369
+ # 7. بررسی ایمنی خروجی
370
  if is_image_nsfw(result):
371
+ return None, seed, get_error_html("تصویر خروجی حاوی محتوای نامناسب بود."), gr.update(visible=True), gr.update(visible=False)
372
+
373
+ # 8. پیام موفقیت
374
+ user_record = get_user_record(fingerprint)
375
+ remaining = USAGE_LIMIT - user_record["count"] if user_record else 0
376
+ success_msg = "تصویر با موفقیت ترکیب شد."
377
+ if subscription_status != 'paid':
378
+ success_msg += f" (اعتبار باقی‌مانده: {remaining})"
379
+
380
+ if subscription_status != 'paid' and remaining <= 0:
381
+ return result, seed, get_success_html(success_msg), gr.update(visible=False), gr.update(visible=True)
382
 
383
+ return result, seed, get_success_html(success_msg), gr.update(visible=True), gr.update(visible=False)
384
 
385
  except Exception as e:
386
  error_str = str(e)
387
  if "quota" in error_str.lower() or "exceeded" in error_str.lower():
388
+ raise e # Raise for JS Scanner
389
+ return None, seed, get_error_html(f"خطا در پردازش: {error_str}"), gr.update(visible=True), gr.update(visible=False)
390
 
391
  @spaces.GPU(duration=30)
392
+ def infer_example(image_1, image_2, prompt, lora_adapter):
393
+ res, s, status, btn1, btn2 = infer(image_1, image_2, prompt, lora_adapter, 0, True, 1.0, 4, "example", "paid")
394
+ return res, s
395
 
396
+ # ==========================================
397
+ # 7. JS & CSS
398
+ # ==========================================
399
  js_download_func = """
400
  async (image) => {
401
+ if (!image) { alert("لطفاً ابتدا تصویر را تولید کنید."); return; }
 
 
 
402
  let fileUrl = image.url;
403
+ if (fileUrl && !fileUrl.startsWith('http')) { fileUrl = window.location.origin + fileUrl; }
404
+ else if (!fileUrl && image.path) { fileUrl = window.location.origin + "/file=" + image.path; }
405
+ window.parent.postMessage({ type: 'DOWNLOAD_REQUEST', url: fileUrl }, '*');
 
 
 
 
 
 
406
  }
407
  """
408
 
409
+ js_upgrade_func = """() => { window.parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM' }, '*'); }"""
410
+
411
+ js_global = """
412
+ <script>
413
+ document.addEventListener('DOMContentLoaded', () => {
414
+ async function getBrowserFingerprint() {
415
+ const components = [navigator.userAgent, navigator.language, screen.colorDepth, screen.width + 'x' + screen.height, new Date().getTimezoneOffset()];
416
+ try {
417
+ const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d');
418
+ ctx.textBaseline = "top"; ctx.font = "14px 'Arial'"; ctx.textBaseline = "alphabetic";
419
+ ctx.fillStyle = "#f60"; ctx.fillRect(125, 1, 62, 20); ctx.fillStyle = "#069"; ctx.fillText("Fusion_FP_v1", 2, 15);
420
+ components.push(canvas.toDataURL());
421
+ } catch (e) { components.push("err"); }
422
+ const str = components.join('~~~');
423
+ let hash = 0; for (let i = 0; i < str.length; i++) { hash = ((hash << 5) - hash) + str.charCodeAt(i); hash |= 0; }
424
+ return 'fp_' + Math.abs(hash).toString(16);
425
+ }
426
+
427
+ function isUserPaid(userObject) {
428
+ const PREMIUM_PAGE_ID = '1149636';
429
+ if (userObject && userObject.isLogin && userObject.accessible_pages) {
430
+ if (Array.isArray(userObject.accessible_pages)) { return userObject.accessible_pages.some(page => String(page) === String(PREMIUM_PAGE_ID)); }
431
+ }
432
+ return false;
433
+ }
434
+
435
+ function updateHiddenInputs(fingerprint, status) {
436
+ const fpInput = document.querySelector('#fingerprint_storage textarea');
437
+ const stInput = document.querySelector('#status_storage textarea');
438
+ if(fpInput && fingerprint && fpInput.value !== fingerprint) { fpInput.value = fingerprint; fpInput.dispatchEvent(new Event('input', { bubbles: true })); }
439
+ if(stInput && status && stInput.value !== status) { stInput.value = status; stInput.dispatchEvent(new Event('input', { bubbles: true })); }
440
+ }
441
+
442
+ function updateBadge(status) {
443
+ const badge = document.getElementById('user-sub-badge');
444
+ if (!badge) return;
445
+ if (status === 'paid') {
446
+ badge.innerHTML = '✨ اشتراک: <span style="color: #FFD700; font-weight: bold;">نامحدود (PRO)</span>';
447
+ badge.style.background = 'linear-gradient(45deg, #FF4500, #FF8533)';
448
+ } else {
449
+ badge.innerHTML = '👤 اشتراک: <span style="color: #fff; font-weight: bold;">رایگان (۳ تصویر روزانه)</span>';
450
+ badge.style.background = 'linear-gradient(45deg, #4b5563, #6b7280)';
451
+ }
452
+ badge.style.display = 'inline-block';
453
+ }
454
+
455
+ async function init() {
456
+ window.userFingerprint = await getBrowserFingerprint();
457
+ window.userStatus = 'free';
458
+ window.parent.postMessage({ type: 'REQUEST_USER_STATUS' }, '*');
459
+ updateBadge('free');
460
+ updateHiddenInputs(window.userFingerprint, window.userStatus);
461
+ setInterval(() => { if(window.userFingerprint) updateHiddenInputs(window.userFingerprint, window.userStatus || 'free'); }, 1500);
462
+ }
463
+
464
+ window.addEventListener('message', (event) => {
465
+ if (event.data && event.data.type === 'USER_STATUS_RESPONSE') {
466
+ try {
467
+ const userObject = typeof event.data.payload === 'string' ? JSON.parse(event.data.payload) : event.data.payload;
468
+ const status = isUserPaid(userObject) ? 'paid' : 'free';
469
+ window.userStatus = status;
470
+ updateBadge(status);
471
+ updateHiddenInputs(window.userFingerprint, status);
472
+ } catch (e) { console.error(e); }
473
+ }
474
+ });
475
+
476
+ init();
477
+
478
+ // GPU QUOTA MODAL
479
+ window.retryGeneration = function() { document.getElementById('custom-quota-modal')?.remove(); document.getElementById('run-btn')?.click(); };
480
+ window.closeErrorModal = function() { document.getElementById('custom-quota-modal')?.remove(); };
481
+
482
+ const showQuotaModal = () => {
483
+ if (document.getElementById('custom-quota-modal')) return;
484
+ const modalHtml = `
485
+ <div id="custom-quota-modal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); backdrop-filter: blur(5px); z-index: 99999; display: flex; align-items: center; justify-content: center; font-family: 'Vazirmatn', sans-serif;">
486
+ <div class="ip-reset-guide-container" style="background: rgba(255, 255, 255, 0.98); padding: 20px; border-radius: 16px; width: 90%; max-width: 420px; text-align: right; direction: rtl;">
487
+ <h2 style="color: #2d3748; margin-bottom: 10px;">یک قدم تا ساخت تصاویر جدید</h2>
488
+ <p style="color: #4a5568; font-size: 0.9rem;">نیازمند تغییر نقطه دستیابی (IP) هستید.</p>
489
+ <div style="background: #FFF0E5; padding: 12px; border-radius: 8px; margin: 10px 0; border: 1px solid #FF8533;">
490
+ <p style="margin:0; font-size: 0.85rem; color: #992900;">از اینترنت سیم‌کارت استفاده کنید، VPN را خاموش کرده، نقطه دستیابی را طبق ویدیو تغییر دهید و «تلاش مجدد» بزنید.</p>
491
+ </div>
492
+ <div style="text-align:center; margin: 15px 0;">
493
+ <button onclick="parent.postMessage({ type: 'NAVIGATE_TO_URL', url: '#/nav/online/news/getSingle/1149635/eyJpdiI6IjhHVGhPQWJwb3E0cjRXbnFWTW5BaUE9PSIsInZhbHVlIjoiS1V0dTdvT21wbXAwSXZaK1RCTG1pVXZqdlFJa1hXV1RKa2FLem9zU3pXMjd5MmlVOGc2YWY0NVdNR3h3Smp1aSIsIm1hYyI6IjY1NTA5ZDYzMjAzMTJhMGQyMWQ4NjA4ZDgyNGZjZDVlY2MyNjdiMjA2NWYzOWRjY2M4ZmVjYWRlMWNlMWQ3ODEiLCJ0YWciOiIifQ==/21135210' }, '*')" style="padding: 8px 16px; background: white; border: 1px solid #ccc; border-radius: 20px; cursor: pointer;">ویدیو آموزشی</button>
494
+ </div>
495
+ <div style="display: flex; gap: 10px;">
496
+ <button onclick="window.closeErrorModal()" style="flex:1; padding: 10px; background: white; border: 1px solid #ccc; border-radius: 8px; cursor: pointer;">بازگشت</button>
497
+ <button onclick="window.retryGeneration()" style="flex:1; padding: 10px; background: linear-gradient(135deg, #FF4500, #FF8533); color: white; border: none; border-radius: 8px; cursor: pointer;">تلاش مجدد</button>
498
+ </div>
499
+ </div>
500
+ </div>`;
501
+ document.body.insertAdjacentHTML('beforeend', modalHtml);
502
+ setTimeout(() => window.closeErrorModal(), 10000);
503
+ };
504
+
505
+ setInterval(() => {
506
+ document.querySelectorAll('.toast-body, .error, div[class*="error"]').forEach(el => {
507
+ if ((el.innerText||"").toLowerCase().includes('quota')) {
508
+ showQuotaModal();
509
+ el.style.display = 'none';
510
+ }
511
+ });
512
+ }, 100);
513
+
514
+ // Force Light Mode
515
+ setInterval(() => {
516
+ document.body.classList.remove('dark');
517
+ document.body.style.backgroundColor = '#FFF0E5';
518
+ }, 1000);
519
+ });
520
+ </script>
521
+ """
522
+
523
+ css_code = """
524
+ <style>
525
+ @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700&display=swap');
526
+ :root, body, .gradio-container { font-family: 'Vazirmatn', sans-serif !important; }
527
+ #fingerprint_storage, #status_storage { display: none !important; }
528
+ #col-container { margin: 0 auto; max-width: 1100px; direction: rtl; text-align: right; background: white; padding: 30px; border-radius: 20px; }
529
+ #badge-container { text-align: center; margin-bottom: 20px; height: 30px; }
530
+ #user-sub-badge { padding: 6px 16px; border-radius: 20px; font-size: 0.9em; color: white; display: none; }
531
+ .primary-btn { width: 100%; margin-top: 15px; background: linear-gradient(135deg, #FF4500, #FF8533); color: white; border: none; }
532
+ .upgrade-btn { width: 100%; margin-top: 15px; background: linear-gradient(135deg, #f59e0b, #d97706); color: white; border: none; animation: pulse 2s infinite; }
533
+ @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.02); } 100% { transform: scale(1); } }
534
+ footer { display: none !important; }
535
+ </style>
536
+ """
537
+
538
+ # ==========================================
539
+ # 8. رابط کاربری (Gradio Blocks)
540
+ # ==========================================
541
+ with gr.Blocks(theme=orange_red_theme) as demo:
542
+ gr.HTML(css_code + js_global)
543
+ fingerprint_box = gr.Textbox(elem_id="fingerprint_storage", visible=True)
544
+ status_box_input = gr.Textbox(elem_id="status_storage", visible=True)
545
+
546
  with gr.Column(elem_id="col-container"):
547
+ gr.Markdown("# **ویرایشگر ترکیبی هوشمند (Fusion)**", elem_id="main-title")
548
+ gr.Markdown("ترکیب، انتقال بافت و ویرایش دو تصویر با هوش مصنوعی آلفا.")
549
+ gr.HTML('<div id="badge-container"><span id="user-sub-badge"></span></div>')
550
+
551
  with gr.Row(equal_height=True):
552
  with gr.Column(scale=1):
553
  with gr.Row():
554
+ image_1 = gr.Image(label="تصویر پایه (Base)", type="pil", height=290)
555
+ image_2 = gr.Image(label="تصویر مرجع (Reference)", type="pil", height=290)
556
 
557
  prompt = gr.Text(
558
+ label="دستور ویرایش (فارسی)",
559
  show_label=True,
560
+ placeholder="مثال: بافت تصویر دوم را روی لیوان اعمال کن...",
561
+ rtl=True
 
562
  )
563
 
564
  status_box = gr.HTML(label="وضعیت")
565
 
566
+ run_button = gr.Button("🔥 شروع ترکیب تصاویر", variant="primary", elem_classes="primary-btn", elem_id="run-btn")
567
+ upgrade_button = gr.Button("💎 ارتقا به نسخه نامحدود", variant="primary", elem_classes="upgrade-btn", elem_id="upgrade-btn", visible=False)
568
 
569
  with gr.Accordion("تنظیمات پیشرفته", open=False, visible=True):
570
+ seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
571
+ randomize_seed = gr.Checkbox(label="تصادفی", value=True)
572
+ guidance_scale = gr.Slider(label="Guidance Scale", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
573
  steps = gr.Slider(label="تعداد مراحل (Steps)", minimum=1, maximum=50, step=1, value=4)
574
 
575
  with gr.Column(scale=1):
576
  output_image = gr.Image(label="تصویر نهایی", interactive=False, format="png", height=350)
577
+ download_button = gr.Button("📥 دانلود تصویر", variant="secondary", elem_classes="primary-btn")
578
 
579
  with gr.Row():
580
  lora_adapter = gr.Dropdown(
581
+ label="نوع عملیات (LoRA)",
582
  choices=list(LORA_MAPPING_PERSIAN.keys()),
583
+ value="ویرایش بافت (Texture Edit)",
584
  )
585
 
586
  gr.Examples(
587
  examples=[
588
+ ["examples/M1.jpg", "examples/M2.jpg", "نورپردازی تصویر اول را حذف کن و بر اساس نور و رنگ تصویر دوم آن را مجدداً نورپردازی کن.", "انتقال نور (Light-Migration)"],
589
+ ["examples/Cloth2.jpg", "examples/Design2.png", "این طرح را روی پیراهن قرار بده.", "طراحی روی لباس (Cloth-Design)"],
590
+ ["examples/Cup1.png", "examples/Wood1.png", "بافت چوب را روی ماگ اعمال کن.", "ویرایش بافت (Texture Edit)"],
591
+ ["examples/F3.jpg", "examples/F4.jpg", "عینک او را با عینک جدید از تصویر ۱ جایگزین کن.", "ترکیب پیشرفته (Super-Fusion)"],
592
+ ["examples/Chair.jpg", "examples/Material.jpg", "متریال تصویر ۱ را دقیقاً شبیه به مرجع تصویر ۲ کن.", "انتقال متریال (Material-Transfer)"],
593
+ ["examples/Cat1.jpg", "examples/Glass1.webp", "یک گربه که عینک تصویر دوم را زده است.", "ترکیب اشیاء (Fuse-Objects)"],
 
 
 
594
  ],
595
  inputs=[image_1, image_2, prompt, lora_adapter],
596
+ outputs=[output_image, seed],
597
  fn=infer_example,
598
  cache_examples=False,
599
+ label="نمونه‌ها"
600
  )
601
 
602
+ # اتصال رویدادها
603
+ fingerprint_box.change(fn=check_initial_quota, inputs=[fingerprint_box, status_box_input], outputs=[run_button, upgrade_button, status_box])
604
+
605
  run_button.click(
606
  fn=infer,
607
+ inputs=[image_1, image_2, prompt, lora_adapter, seed, randomize_seed, guidance_scale, steps, fingerprint_box, status_box_input],
608
+ outputs=[output_image, seed, status_box, run_button, upgrade_button]
 
609
  )
610
+
611
+ upgrade_button.click(fn=None, js=js_upgrade_func)
612
+ download_button.click(fn=None, inputs=[output_image], js=js_download_func)
613
+
 
 
 
 
614
  if __name__ == "__main__":
615
+ demo.queue(max_size=30).launch(show_error=True)