Opera8 commited on
Commit
89f5cac
·
verified ·
1 Parent(s): d788890

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +327 -620
app.py CHANGED
@@ -4,44 +4,14 @@ import numpy as np
4
  import spaces
5
  import torch
6
  import random
 
 
 
7
  from PIL import Image, ImageFilter
8
- from typing import Iterable
9
  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
- from datetime import datetime
14
-
15
- # --- متغیرهای سراسری برای مدیریت محدودیت ---
16
- USAGE_TRACKER = {} # ذخیره: {ip: {'count': 0, 'date': 'YYYY-MM-DD'}}
17
- DAILY_LIMIT = 5 # محدودیت تعداد عکس در روز برای کاربران رایگان
18
-
19
- # --- تعریف لیست کلمات ممنوعه (سخت‌گیرانه) ---
20
- # این کلمات در متن ترجمه شده (انگلیسی) جستجو می‌شوند
21
- BANNED_WORDS = [
22
- # Nudity & Sexual content
23
- "nsfw", "nude", "naked", "sex", "sexy", "sexual", "porn", "porno", "pornography",
24
- "erotic", "erotica", "xxx", "18+", "uncensored", "hentai", "ecchi", "r34", "rule34",
25
- "fetish", "bondage", "bdsm", "kink", "sadism", "masochism",
26
- "lingerie", "bikini", "swimwear", "underwear", "panties", "bra", "topless", "bottomless",
27
- "upskirt", "exhibitionism", "voyeur", "orgasm", "seductive", "sensual",
28
-
29
- # Body parts (Specific context)
30
- "breast", "boobs", "tits", "nipple", "areola", "cleavage",
31
- "genital", "vagina", "pussy", "cunt", "clitoris", "vulva",
32
- "penis", "dick", "cock", "phallus", "boner", "erection",
33
- "ass", "butt", "booty", "anal", "anus", "rectum", "glute",
34
- "groin", "crotch", "pubic",
35
-
36
- # Sexual Acts
37
- "blowjob", "bj", "handjob", "rimming", "fingering", "fucking", "screwing",
38
- "masturbation", "masturbating", "sucking", "licking", "penetration", "intercourse",
39
- "cum", "sperm", "ejaculation", "bukkake", "creampie", "gangbang", "orgy", "threesome",
40
-
41
- # Violence & Gore (Optional but recommended for strict safety)
42
- "gore", "blood", "bloody", "kill", "murder", "death", "torture", "suicide",
43
- "dead", "corpse", "mutilation", "decapitation", "amputee"
44
- ]
45
 
46
  # --- تعریف تم ---
47
  colors.steel_blue = colors.Color(
@@ -61,7 +31,7 @@ colors.steel_blue = colors.Color(
61
 
62
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
63
 
64
- # --- بارگذاری مدل تشخیص محتوای نامناسب (NSFW) برای تصاویر ---
65
  print("Loading Safety Checker...")
66
  safety_classifier = pipeline("image-classification", model="Falconsai/nsfw_image_detection", device=-1)
67
 
@@ -77,15 +47,13 @@ def is_image_nsfw(image):
77
  print(f"Safety check error: {e}")
78
  return False
79
 
80
- # --- بارگذاری مدل اصلی ---
81
  from diffusers import FlowMatchEulerDiscreteScheduler
82
  from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
83
  from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
84
  from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
85
 
86
  dtype = torch.bfloat16
87
- device = "cuda" if torch.cuda.is_available() else "cpu"
88
-
89
  print("Loading pipeline...")
90
  pipe = QwenImageEditPlusPipeline.from_pretrained(
91
  "Qwen/Qwen-Image-Edit-2509",
@@ -122,14 +90,6 @@ LORA_MAPPING = {
122
  "افزایش کیفیت (Upscale)": "upscale-image"
123
  }
124
 
125
- ASPECT_RATIOS_LIST = [
126
- "خودکار (پیش‌فرض)",
127
- "۱:۱ (مربع - 1024x1024)",
128
- "۱۶:۹ (افقی - 1344x768)",
129
- "۹:۱۶ (عمودی - 768x1344)",
130
- "شخصی‌سازی (Custom)"
131
- ]
132
-
133
  ASPECT_RATIOS_MAP = {
134
  "خودکار (پیش‌فرض)": "Auto",
135
  "۱:۱ (مربع - 1024x1024)": (1024, 1024),
@@ -138,32 +98,84 @@ ASPECT_RATIOS_MAP = {
138
  "شخصی‌سازی (Custom)": "Custom"
139
  }
140
 
141
- def check_text_safety(text):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  """
143
- بررسی سخت‌گیرانه متن برای کل��ات ممنوعه
 
 
144
  """
145
- if not text: return True
146
- text_lower = text.lower()
 
 
 
 
 
147
 
148
- # حذف علائم نگارشی برای بررسی دقیق‌تر
149
- import string
150
- translator = str.maketrans('', '', string.punctuation)
151
- clean_text = text_lower.translate(translator)
152
 
153
- words = clean_text.split()
 
154
 
155
- for banned in BANNED_WORDS:
156
- # بررسی دقیق کلمه (Exact Match)
157
- if banned in words:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  return False
159
- # بررسی وجود کلمه در داخل کلمات دیگر (Substring Match) برای کلمات خاص و خطرناک
160
- # مثلاً اگر کاربر بنویسد "supersexy" باید شناسایی شود
161
- if len(banned) > 3 and banned in text_lower:
162
- # برای جلوگیری از خطای مثبت (مثلاً "ass" در "class")
163
- # اینجا یک بررسی ساده انجام می‌دهیم، اما برای کلمات خیلی رکیک سخت‌گیر هستیم
164
- if banned in ["sex", "porn", "fuck", "nude", "naked", "xxx"]:
165
- return False
166
-
167
  return True
168
 
169
  def translate_prompt(text):
@@ -192,15 +204,9 @@ def update_dimensions_on_upload(image):
192
  new_height = (new_height // 8) * 8
193
  return new_width, new_height
194
 
195
- def update_sliders_visibility(choice):
196
- if choice == "شخصی‌سازی (Custom)":
197
- return gr.update(visible=True), gr.update(visible=True)
198
- else:
199
- return gr.update(visible=False), gr.update(visible=False)
200
-
201
  def get_error_html(message):
202
  return f"""
203
- <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;">
204
  <span style="font-size: 1.2em;">⛔</span>
205
  {message}
206
  </div>
@@ -208,58 +214,13 @@ def get_error_html(message):
208
 
209
  def get_success_html(message):
210
  return f"""
211
- <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;">
212
  <span style="font-size: 1.2em;">✅</span>
213
  {message}
214
  </div>
215
  """
216
 
217
- def check_and_update_quota(request: gr.Request, is_vip: bool):
218
- """
219
- بررسی محدودیت کاربر و بروزرسانی شمارنده.
220
- اگر VIP باشد، محدودیت ندارد.
221
- اگر رایگان باشد، بر اساس IP محدودیت ۵ بار در روز اعمال می‌شود.
222
- """
223
- if is_vip:
224
- return True, ""
225
-
226
- try:
227
- client_ip = request.client.host
228
- except:
229
- client_ip = "unknown"
230
-
231
- today = datetime.now().strftime("%Y-%m-%d")
232
-
233
- # مقداردهی اولیه برای IP جدید
234
- if client_ip not in USAGE_TRACKER:
235
- USAGE_TRACKER[client_ip] = {'count': 0, 'date': today}
236
-
237
- user_data = USAGE_TRACKER[client_ip]
238
-
239
- # ریست کردن اگر روز تغییر کرده باشد
240
- if user_data['date'] != today:
241
- user_data['count'] = 0
242
- user_data['date'] = today
243
-
244
- # بررسی سقف مجاز
245
- if user_data['count'] >= DAILY_LIMIT:
246
- return False, f"شما به سقف مجاز روزانه ({DAILY_LIMIT} تصویر) رسیده‌اید. فردا مجدداً تلاش کنید یا حساب خود را ارتقا دهید."
247
-
248
- return True, ""
249
-
250
- def increment_quota(request: gr.Request, is_vip: bool):
251
- """
252
- افزایش شمارنده پس از موفقیت آمیز بودن عملیات
253
- """
254
- if is_vip: return
255
- try:
256
- client_ip = request.client.host
257
- if client_ip in USAGE_TRACKER:
258
- USAGE_TRACKER[client_ip]['count'] += 1
259
- except:
260
- pass
261
-
262
- @spaces.GPU(duration=30)
263
  def infer(
264
  input_image,
265
  prompt,
@@ -271,26 +232,31 @@ def infer(
271
  aspect_ratio_selection,
272
  custom_width,
273
  custom_height,
274
- is_vip_state, # ورودی جدید برای وضعیت VIP
 
275
  request: gr.Request,
276
  progress=gr.Progress(track_tqdm=True)
277
  ):
278
- # --- مرحله ۱: بررسی سهمیه (Quota) ---
279
- allowed, message = check_and_update_quota(request, is_vip_state)
 
 
 
 
280
  if not allowed:
281
- return None, seed, get_error_html(message)
282
 
283
  if input_image is None:
284
- return None, seed, get_error_html("لطفاً ابتدا یک تصویر بارگذاری کنید.")
285
 
286
- # --- مرحله ۲: بررسی تصویر ورودی (Safety Check) ---
287
  if is_image_nsfw(input_image):
288
- return None, seed, get_error_html("تصویر ورودی دارای محتوای نامناسب است و پردازش نمی‌شود.")
289
 
290
- # --- مرحله ۳: ترجمه و بررسی متن (Safety Check) ---
291
  english_prompt = translate_prompt(prompt)
292
  if not check_text_safety(english_prompt):
293
- return None, seed, get_error_html("متن درخواست شامل کلمات غیرمجاز یا غیراخلاقی است.")
294
 
295
  adapter_internal_name = LORA_MAPPING.get(lora_adapter_persian)
296
  if adapter_internal_name:
@@ -318,7 +284,6 @@ def infer(
318
  width, height = selection_value
319
 
320
  try:
321
- # --- تولید تصویر ---
322
  result = pipe(
323
  image=original_image,
324
  prompt=english_prompt,
@@ -330,85 +295,109 @@ def infer(
330
  true_cfg_scale=guidance_scale,
331
  ).images[0]
332
 
333
- # --- مرحله ۴: بررسی تصویر خروجی (Safety Check) ---
334
  if is_image_nsfw(result):
335
- return None, seed, get_error_html("تصویر تولید شده حاوی محتوای نامناسب بود و حذف شد.")
 
 
 
336
 
337
- # افزایش شمارنده مصرف فقط در صورت موفقیت
338
- increment_quota(request, is_vip_state)
 
339
 
340
- return result, seed, get_success_html("تصویر با موفقیت ویرایش شد.")
341
 
342
  except Exception as e:
343
  error_str = str(e)
344
  if "quota" in error_str.lower() or "exceeded" in error_str.lower():
345
  raise e
346
- return None, seed, get_error_html(f"خطا در پردازش: {error_str}")
347
-
348
- @spaces.GPU(duration=30)
349
- def infer_example(input_image, prompt, lora_adapter):
350
- # برای مثال‌ها محدودیت VIP بررسی نمی‌شود و IP لوکال فرض می‌شود
351
- res, s, status = infer(input_image, prompt, lora_adapter, 0, True, 1.0, 4, "خودکار (پیش‌فرض)", 1024, 1024, True, None)
352
- return res, s, status
353
-
354
- # --- جاوااسکریپت برای دکمه دانلود ---
355
- js_download_func = """
356
- async (image) => {
357
- if (!image) {
358
- alert("لطفاً ابتدا تصویر را تولید کنید.");
359
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  }
361
- let fileUrl = image.url;
362
- if (fileUrl && !fileUrl.startsWith('http')) {
363
- fileUrl = window.location.origin + fileUrl;
364
- } else if (!fileUrl && image.path) {
365
- fileUrl = window.location.origin + "/file=" + image.path;
 
 
 
366
  }
367
- window.parent.postMessage({
368
- type: 'DOWNLOAD_REQUEST',
369
- url: fileUrl
370
- }, '*');
371
  }
372
- """
373
 
374
- # --- جاوااسکریپت سراسری (استراتژی موبایل + بستن خودکار ۱۰ ثانیه + تشخیص VIP) ---
375
- js_global_content = """
376
- <script>
377
- // Global variable to store VIP status
378
- window.userIsVip = false;
379
- const PREMIUM_PAGE_ID = '1149635'; // شناسه صفحه پرداخت برای بررسی دسترسی
380
 
381
- // Request User Status on Load
382
- setTimeout(() => {
383
- window.parent.postMessage({ type: 'REQUEST_USER_STATUS' }, '*');
384
- }, 1000);
385
-
386
- // Listener for User Status
387
- window.addEventListener('message', (event) => {
388
- if (event.data && event.data.type === 'USER_STATUS_RESPONSE') {
389
- try {
390
- const user = JSON.parse(event.data.payload);
391
- // بررسی دسترسی کاربر به صفحه پرمیوم
392
- if (user && user.isLogin && user.accessible_pages &&
393
- (user.accessible_pages.includes(PREMIUM_PAGE_ID) ||
394
- user.accessible_pages.includes(parseInt(PREMIUM_PAGE_ID)))) {
395
-
396
- window.userIsVip = true;
397
-
398
- // نمایش نشان VIP
399
- const badge = document.createElement('div');
400
- badge.innerHTML = '👑 نسخه ویژه (نامحدود)';
401
- badge.style.cssText = 'position:fixed;top:10px;left:10px;background:linear-gradient(45deg, #FFD700, #FFC107);color:#333;padding:6px 12px;border-radius:20px;z-index:9999;font-weight:bold;box-shadow:0 2px 5px rgba(0,0,0,0.2);font-family:"Vazirmatn",sans-serif;font-size:0.8rem;';
402
- document.body.appendChild(badge);
403
-
404
- // حذف مدال محدودیت اگر باز باشد
405
- window.closeErrorModal();
406
- }
407
- } catch (e) { console.log('Error parsing user status:', e); }
408
  }
409
- });
410
 
411
- document.addEventListener('DOMContentLoaded', () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
  // 1. Force Light Mode
413
  const forceLight = () => {
414
  const body = document.querySelector('body');
@@ -422,115 +411,65 @@ document.addEventListener('DOMContentLoaded', () => {
422
  forceLight();
423
  setInterval(forceLight, 1000);
424
 
425
- // 2. RETRY FUNCTION
426
- window.retryGeneration = function() {
427
- const modal = document.getElementById('custom-quota-modal');
428
- if (modal) modal.remove();
 
 
 
429
 
430
- const runBtn = document.getElementById('run-btn');
431
- if(runBtn) runBtn.click();
432
- };
433
 
434
- // Close function
435
- window.closeErrorModal = function() {
436
- const modal = document.getElementById('custom-quota-modal');
437
- if (modal) modal.remove();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
  };
439
-
440
- // 3. SHOW MODAL FUNCTION
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
  const showQuotaModal = () => {
442
  if (document.getElementById('custom-quota-modal')) return;
443
-
444
- const modalHtml = `
445
- <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;">
446
- <div class="ip-reset-guide-container">
447
- <div class="guide-header">
448
- <svg class="guide-header-icon" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
449
- <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>
450
- <circle cx="50" cy="50" r="45" fill="url(#grad1)" opacity="0.1"></circle>
451
- <circle cx="50" cy="50" r="35" fill="none" stroke="url(#grad1)" stroke-width="2" opacity="0.3"></circle>
452
- <path d="M35 50 L45 60 L65 40" stroke="url(#grad1)" stroke-width="4" fill="none" stroke-linecap="round" stroke-linejoin="round"></path>
453
- <circle cx="65" cy="35" r="8" fill="#fee140"></circle>
454
- <path d="M62 35 L68 35 M65 32 L65 38" stroke="white" stroke-width="2" stroke-linecap="round"></path>
455
- </svg>
456
- <div>
457
- <h2>محدودیت سرور گرافیکی</h2>
458
- <p>نیازمند تغییر نقطه دستیابی</p>
459
- </div>
460
- </div>
461
-
462
- <div class="guide-content">
463
- <div class="info-card">
464
- <div class="info-card-header">
465
- <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>
466
- <span class="info-card-title">راه حل سریع</span>
467
- </div>
468
- <p>طبق ویدیو آموزشی پایین بین نقطه دستیابی جابجا شوید تلاش مجدد بزنید تا تصاویر مجدداً تولید بشه.</p>
469
- </div>
470
-
471
- <div class="summary-section">
472
- <div class="summary-header">
473
- <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>
474
- <span class="summary-title">خلاصه راهنما</span>
475
- </div>
476
- <div class="summary-text">این پیام مربوط به سرور جهانی است. برای رفع آن: اینترنت سیم‌کارت استفاده کنید، VPN را خاموش کرده و طبق ویدیو آموزشی نقطه دستیابی رو تغییر دهید.</div>
477
- </div>
478
-
479
- <div class="video-button-container">
480
- <button onclick="parent.postMessage({ type: 'NAVIGATE_TO_URL', url: '#/nav/online/news/getSingle/1149635/eyJpdiI6IjhHVGhPQWJwb3E0cjRXbnFWTW5BaUE9PSIsInZhbHVlIjoiS1V0dTdvT21wbXAwSXZaK1RCTG1pVXZqdlFJa1hXV1RKa2FLem9zU3pXMjd5MmlVOGc2YWY0NVdNR3h3Smp1aSIsIm1hYyI6IjY1NTA5ZDYzMjAzMTJhMGQyMWQ4NjA4ZDgyNGZjZDVlY2MyNjdiMjA2NWYzOWRjY2M4ZmVjYWRlMWNlMWQ3ODEiLCJ0YWciOiIifQ==/21135210' }, '*')" class="elegant-video-button">
481
- <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>
482
- <span>دیدن ویدیو آموزشی استفاده نامحدود</span>
483
- </button>
484
- </div>
485
- </div>
486
-
487
- <div class="guide-actions">
488
- <button class="action-button back-button" onclick="window.closeErrorModal()">
489
- <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>
490
- <span>بازگشت</span>
491
- </button>
492
- <button class="action-button retry-button" onclick="window.retryGeneration()">
493
- <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>
494
- <span>تلاش مجدد</span>
495
- </button>
496
- </div>
497
- </div>
498
- </div>
499
- `;
500
-
501
- document.body.insertAdjacentHTML('beforeend', modalHtml);
502
-
503
- // Auto close after 10 seconds
504
- setTimeout(() => {
505
- window.closeErrorModal();
506
- }, 10000);
507
  };
508
-
509
- // 4. SCANNER (For HuggingFace GPU Quota)
510
- setInterval(() => {
511
- const potentialErrors = document.querySelectorAll('.toast-body, .error, .toast-wrap, .eta-bar, div[class*="error"]');
512
-
513
- potentialErrors.forEach(el => {
514
- const text = el.innerText || "";
515
- if (text.toLowerCase().includes('quota') || text.toLowerCase().includes('exceeded')) {
516
-
517
- showQuotaModal();
518
-
519
- // Immediately hide the Gradio error
520
- el.style.display = 'none';
521
- el.style.opacity = '0';
522
- el.innerText = '';
523
-
524
- const parentWrap = el.closest('.toast-wrap');
525
- if(parentWrap) parentWrap.style.display = 'none';
526
- }
527
- });
528
- }, 100);
529
  });
530
  </script>
531
  """
532
 
533
- # --- CSS Updated (Larger & Auto Scroll) ---
534
  css_code = """
535
  <style>
536
  @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700&display=swap');
@@ -538,168 +477,72 @@ css_code = """
538
  :root, .dark, body, .gradio-container {
539
  --body-background-fill: #f5f7fa !important;
540
  --body-text-color: #1f2937 !important;
541
- --background-fill-primary: #ffffff !important;
542
- --background-fill-secondary: #f3f4f6 !important;
543
- --border-color-primary: #e5e7eb !important;
544
- --block-background-fill: #ffffff !important;
545
- --block-label-text-color: #374151 !important;
546
- --block-title-text-color: #111827 !important;
547
- --input-background-fill: #ffffff !important;
548
  color-scheme: light !important;
549
  }
550
 
551
- /* --- IP Reset Guide CSS --- */
552
- :root {
553
- --guide-bg: rgba(255, 255, 255, 0.98);
554
- --guide-border: rgba(102, 126, 234, 0.2);
555
- --guide-text-title: #2d3748;
556
- --guide-text-body: #4a5568;
557
- --guide-accent: #667eea;
558
- --primary-gradient-guide: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
559
- --success-gradient-guide: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%);
560
- --radius-md-guide: 12px;
561
- --radius-lg-guide: 16px;
562
- --shadow-sm: 0 1px 2px 0 rgba(26, 32, 44, 0.03);
563
- --shadow-md: 0 4px 6px -1px rgba(26, 32, 44, 0.05), 0 2px 4px -2px rgba(26, 32, 44, 0.04);
564
- --shadow-xl: 0 20px 25px -5px rgba(26, 32, 44, 0.07), 0 8px 10px -6px rgba(26, 32, 44, 0.05);
565
- }
566
 
567
- @keyframes float {
568
- 0%, 100% { transform: translateY(0px); }
569
- 50% { transform: translateY(-10px); }
570
- }
571
- @keyframes slideInUp {
572
- from { opacity: 0; transform: translateY(30px); }
573
- to { opacity: 1; transform: translateY(0); }
574
- }
575
 
576
- .ip-reset-guide-container {
577
- text-align: right;
578
- direction: rtl;
579
- background: var(--guide-bg);
580
- backdrop-filter: blur(10px);
581
- padding: 20px; /* Increased Padding */
582
- border-radius: var(--radius-lg-guide);
583
- box-shadow: var(--shadow-xl);
584
- border: 1px solid var(--guide-border);
585
- animation: slideInUp 0.6s cubic-bezier(0.4, 0, 0.2, 1) both;
586
- width: 90%;
587
- max-width: 420px; /* Increased width slightly */
588
- max-height: 90vh; /* Prevent overflow on small screens */
589
- overflow-y: auto; /* Enable scroll if needed */
590
- position: relative;
591
- box-sizing: border-box;
592
- font-family: 'Vazirmatn', sans-serif !important;
593
- }
594
- .ip-reset-guide-container::before {
595
- content: ''; position: absolute; top: 0; left: 0; right: 0; height: 4px; background: var(--primary-gradient-guide);
596
- }
597
- .guide-header { display: flex; align-items: center; margin-bottom: 15px; }
598
- .guide-header-icon { width: 45px; height: 45px; margin-left: 15px; animation: float 3s ease-in-out infinite; flex-shrink: 0; }
599
- .guide-header h2 { font-size: 1.2rem; color: var(--guide-text-title); font-weight: 700; margin: 0; }
600
- .guide-header p { color: var(--guide-text-body); font-size: 0.8rem; margin-top: 3px; margin-bottom: 0; }
601
-
602
- .guide-content { font-size: 0.9rem; color: var(--guide-text-body); line-height: 1.6; }
603
-
604
- .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: 12px; margin: 12px 0; position: relative; overflow: hidden; }
605
- .info-card p { font-size: 0.85rem; line-height: 1.6; margin: 0; }
606
- .info-card::before { content: ''; position: absolute; top: 0; right: 0; width: 3px; height: 100%; background: var(--primary-gradient-guide); }
607
- .info-card-header { display: flex; align-items: center; margin-bottom: 8px; }
608
- .info-card-icon { width: 18px; height: 18px; margin-left: 8px; }
609
- .info-card-title { font-weight: 600; color: var(--guide-text-title); font-size: 0.95rem; }
610
-
611
- .summary-section { margin-top: 12px; padding: 12px; 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; }
612
- .summary-section::before { content: ''; position: absolute; top: 0; right: 0; width: 3px; height: 100%; background: var(--success-gradient-guide); }
613
- .summary-header { display: flex; align-items: center; margin-bottom: 8px; }
614
- .summary-icon { width: 18px; height: 18px; margin-left: 8px; }
615
- .summary-title { font-weight: 600; color: #2f5a33; font-size: 0.95rem; }
616
- .summary-text { color: #2f5a33; font-size: 0.85rem; line-height: 1.6; }
617
-
618
- /* Tutorial Button */
619
- .video-button-container { text-align: center; margin: 20px 0 15px 0; width: 100%; }
620
- .elegant-video-button {
621
- display: inline-flex !important;
622
- align-items: center;
623
- justify-content: center;
624
- padding: 10px 20px !important;
625
- background-color: #fff !important;
626
- color: var(--guide-accent) !important;
627
- border: 1px solid #e2e8f0 !important;
628
- text-decoration: none;
629
- border-radius: 50px !important;
630
- font-weight: 600 !important;
631
- font-size: 0.9rem !important;
632
- cursor: pointer !important;
633
- font-family: inherit;
634
- transition: all 0.3s ease !important;
635
- box-shadow: 0 2px 10px rgba(0,0,0,0.05) !important;
636
- width: auto !important;
637
  }
638
- .elegant-video-button:hover {
639
- background: var(--primary-gradient-guide) !important;
640
- color: white !important;
641
- border-color: transparent !important;
642
- transform: translateY(-2px);
643
- box-shadow: 0 6px 16px rgba(102, 126, 234, 0.3) !important;
644
- }
645
- .elegant-video-button-icon { width: 18px; height: 18px; margin-left: 8px; fill: currentColor; }
646
-
647
- /* Action Buttons */
648
- .guide-actions {
649
- display: flex !important;
650
- gap: 12px !important;
651
- margin-top: 20px;
652
- padding-top: 20px;
653
- border-top: 1px solid #e2e8f0;
654
- width: 100% !important;
655
- }
656
- .action-button {
657
- padding: 12px 15px !important;
658
- border: none !important;
659
- border-radius: 12px !important;
660
- font-size: 0.95rem !important;
661
- font-weight: 600 !important;
662
- cursor: pointer !important;
663
- flex: 1 !important;
664
- transition: all 0.3s ease !important;
665
- display: flex !important;
666
- align-items: center;
667
- justify-content: center;
668
- font-family: inherit;
669
- height: 48px !important;
670
- }
671
- .action-button-icon { width: 20px; height: 20px; margin-right: 0; margin-left: 8px; }
672
 
673
- .back-button {
674
- background: white !important;
675
- color: var(--guide-text-body) !important;
676
- border: 2px solid #e2e8f0 !important;
677
  }
678
- .back-button:hover {
679
- background: #f7fafc !important;
680
- border-color: var(--guide-accent) !important;
681
- transform: translateY(-2px);
682
- box-shadow: var(--shadow-md) !important;
683
  }
684
 
685
- .retry-button {
686
- background: var(--primary-gradient-guide) !important;
687
- color: white !important;
688
- box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3) !important;
689
  }
690
- .retry-button:hover {
691
- transform: translateY(-2px);
692
- box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
693
  }
694
 
695
- /* --- Main App CSS --- */
696
- body {
697
- font-family: 'Vazirmatn', sans-serif !important;
698
- background-color: #f5f7fa !important;
699
- margin: 0;
700
- padding: 10px;
701
  }
702
 
 
703
  #col-container {
704
  margin: 0 auto;
705
  max-width: 980px;
@@ -716,7 +559,7 @@ body {
716
  font-size: 2.4em !important;
717
  text-align: center;
718
  color: #1a202c !important;
719
- margin-bottom: 15px;
720
  font-weight: 800;
721
  background: -webkit-linear-gradient(45deg, #2563eb, #1e40af);
722
  -webkit-background-clip: text;
@@ -727,39 +570,11 @@ body {
727
  text-align: center;
728
  font-size: 1.15em;
729
  color: #4b5563 !important;
730
- margin-bottom: 20px;
731
- line-height: 1.6;
732
- }
733
-
734
- .gr-input-label, span.label-wrap, label span {
735
- font-weight: 700 !important;
736
- color: #374151 !important;
737
- font-size: 0.95em !important;
738
- margin-bottom: 8px !important;
739
- }
740
-
741
- textarea, input[type="text"] {
742
- border: 2px solid #e2e8f0 !important;
743
- border-radius: 12px !important;
744
- background-color: #ffffff !important;
745
- color: #111827 !important;
746
- padding: 12px !important;
747
- transition: all 0.3s ease;
748
- font-family: 'Vazirmatn', sans-serif !important;
749
- }
750
-
751
- textarea:focus, input[type="text"]:focus {
752
- border-color: #3b82f6 !important;
753
- box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1) !important;
754
- outline: none;
755
  }
756
 
757
- .gr-dropdown {
758
- background: #ffffff !important;
759
- border-radius: 12px !important;
760
- }
761
-
762
- .primary-btn, button.primary {
763
  background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important;
764
  border: none !important;
765
  color: white !important;
@@ -768,106 +583,43 @@ textarea:focus, input[type="text"]:focus {
768
  padding: 14px 28px !important;
769
  border-radius: 14px !important;
770
  box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3) !important;
771
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
772
  cursor: pointer !important;
773
  width: 100%;
774
- margin-top: 15px;
775
- }
776
-
777
- .primary-btn:hover, button.primary:hover {
778
- transform: translateY(-2px);
779
- box-shadow: 0 8px 25px rgba(16, 185, 129, 0.45) !important;
780
- }
781
-
782
- .primary-btn:active, button.primary:active {
783
- transform: translateY(1px);
784
  }
 
785
 
786
  #download-btn {
787
  background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
788
  box-shadow: 0 4px 15px rgba(59, 130, 246, 0.3) !important;
789
  }
790
- #download-btn:hover {
791
- box-shadow: 0 8px 25px rgba(59, 130, 246, 0.45) !important;
792
- }
793
 
794
- .gradio-container .prose table,
795
- .gradio-container table {
796
- background-color: #ffffff !important;
797
- color: #111827 !important;
798
- border: 1px solid #e5e7eb !important;
799
  border-radius: 12px !important;
800
- overflow: hidden !important;
801
- width: 100% !important;
802
- margin-top: 20px !important;
803
- }
804
-
805
- .gradio-container thead th {
806
- background-color: #f3f4f6 !important;
807
- color: #374151 !important;
808
- font-weight: 700 !important;
809
- border-bottom: 2px solid #e5e7eb !important;
810
- padding: 12px !important;
811
- text-align: right !important;
812
- }
813
-
814
- .gradio-container tbody tr {
815
- background-color: #ffffff !important;
816
- border-bottom: 1px solid #f3f4f6 !important;
817
- }
818
-
819
- .gradio-container tbody tr:hover {
820
- background-color: #f9fafb !important;
821
- }
822
-
823
- .gradio-container tbody td {
824
- background-color: #ffffff !important;
825
- color: #374151 !important;
826
- padding: 10px !important;
827
- }
828
-
829
- .gradio-container tbody td span,
830
- .gradio-container tbody td p {
831
- color: #374151 !important;
832
  }
833
 
834
  footer { display: none !important; }
835
  .flagging { display: none !important; }
836
 
837
- /* Force toast transparency */
838
- .toast-body {
839
- direction: rtl !important;
840
- text-align: right !important;
841
- background: transparent !important;
842
- box-shadow: none !important;
843
- border: none !important;
844
- padding: 0 !important;
845
- max-width: 100% !important;
846
- width: auto !important;
847
- }
848
- .toast-wrap {
849
- background: transparent !important;
850
- border: none !important;
851
- box-shadow: none !important;
852
- }
853
-
854
- @media (prefers-color-scheme: dark) {
855
- body, .gradio-container, .prose, table, tr, td, th {
856
- background-color: #ffffff !important;
857
- color: #333333 !important;
858
- }
859
  }
860
  </style>
861
  """
862
 
863
  # ادغام CSS و JS
864
- combined_html = css_code + js_global_content
865
 
866
- # استفاده از gr.Blocks
867
- with gr.Blocks() as demo:
868
- # تزریق کدها به عنوان HTML
869
  gr.HTML(combined_html)
870
 
 
 
 
 
871
  with gr.Column(elem_id="col-container"):
872
  gr.Markdown("# **ویرایشگر هوشمند آلفا**", elem_id="main-title")
873
  gr.Markdown(
@@ -875,15 +627,10 @@ with gr.Blocks() as demo:
875
  elem_id="main-description"
876
  )
877
 
878
- # --- بخش نمایش وضعیت اشتراک (مشابه پادکست) ---
879
  gr.HTML("""
880
- <div style="display: flex; gap: 10px; justify-content: center; margin-bottom: 20px; flex-wrap: wrap;">
881
- <span style="background: #e2e8f0; color: #475569; padding: 6px 16px; border-radius: 20px; font-size: 0.9em; font-weight: 700; display: flex; align-items: center; gap: 6px;">
882
- 👤 برای کاربران رایگان: ۵ تصویر در روز
883
- </span>
884
- <span style="background: linear-gradient(90deg, #ffd700, #ffb700); color: #333; padding: 6px 16px; border-radius: 20px; font-size: 0.9em; font-weight: 700; box-shadow: 0 4px 10px rgba(255, 193, 7, 0.3); display: flex; align-items: center; gap: 6px;">
885
- 👑 نسخه نامحدود: برای کاربران پولی
886
- </span>
887
  </div>
888
  """)
889
 
@@ -899,13 +646,17 @@ with gr.Blocks() as demo:
899
  lines=3
900
  )
901
 
 
902
  status_box = gr.HTML(label="وضعیت")
903
 
904
- # Checkbox مخفی برای انتقال وضعیت VIP از جاوااسکریپت به پایتون
905
- is_vip_state = gr.Checkbox(visible=False, value=False, label="VIP State")
906
-
907
- # IMPORTANT: Added elem_id="run-btn" here for JS targeting
908
  run_button = gr.Button("✨ شروع پردازش و ساخت تصویر", variant="primary", elem_classes="primary-btn", elem_id="run-btn")
 
 
 
 
 
 
 
909
 
910
  with gr.Column():
911
  output_image = gr.Image(label="تصویر نهایی", interactive=False, format="png", height=380)
@@ -922,81 +673,37 @@ with gr.Blocks() as demo:
922
  with gr.Accordion("تنظیمات پیشرفته", open=False, visible=True):
923
  aspect_ratio_selection = gr.Dropdown(
924
  label="ابعاد تصویر خروجی",
925
- choices=ASPECT_RATIOS_LIST,
926
  value="خودکار (پیش‌فرض)",
927
  interactive=True
928
  )
929
 
930
  with gr.Row(visible=False) as custom_dims_row:
931
- custom_width = gr.Slider(
932
- label="عرض دلخواه (Width)",
933
- minimum=256, maximum=2048, step=8, value=1024
934
- )
935
- custom_height = gr.Slider(
936
- label="ارتفاع دلخواه (Height)",
937
- minimum=256, maximum=2048, step=8, value=1024
938
- )
939
 
940
  seed = gr.Slider(label="دانه تصادفی (Seed)", minimum=0, maximum=MAX_SEED, step=1, value=0)
941
  randomize_seed = gr.Checkbox(label="استفاده از Seed تصادفی", value=True)
942
- guidance_scale = gr.Slider(label="میزان وفاداری به متن (Guidance Scale)", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
943
- steps = gr.Slider(label="تعداد مراحل پردازش (Steps)", minimum=1, maximum=50, step=1, value=4)
944
 
945
- # اصلاح تابع نمایش ردیف اسلایدرها
946
  def toggle_row(choice):
947
- if choice == "شخصی‌سازی (Custom)":
948
- return gr.update(visible=True)
949
- return gr.update(visible=False)
950
-
951
- aspect_ratio_selection.change(
952
- fn=toggle_row,
953
- inputs=aspect_ratio_selection,
954
- outputs=custom_dims_row
955
- )
956
 
957
- gr.Examples(
958
- examples=[
959
- ["examples/1.jpg", "تبدیل به انیمه کن.", "تبدیل عکس به انیمه"],
960
- ["examples/5.jpg", "سایه‌ها را حذف کن و نورپردازی نرم به تصویر بده.", "اصلاح نور و سایه"],
961
- ["examples/4.jpg", "از فیلتر ساعت طلایی با پخش نور ملایم استفاده کن.", "نورپردازی مجدد (Relight)"],
962
- ["examples/2.jpeg", "دوربین را ۴۵ درجه به سمت چپ بچرخان.", "تغییر زاویه دید"],
963
- ["examples/7.jpg", "منبع نور را از سمت راست عقب قرار بده.", "نورپردازی چند زاویه‌ای"],
964
- ["examples/10.jpeg", "کیفیت تصویر را افزایش بده (Upscale).", "افزایش کیفیت (Upscale)"],
965
- ["examples/7.jpg", "منبع نور را از پایین بتابان.", "نورپردازی چند زاویه‌ای"],
966
- ["examples/2.jpeg", "زاویه دوربین را به نمای بالا گوشه راست تغییر بده.", "تغییر زاویه دید"],
967
- ["examples/9.jpg", "دوربین کمی به جلو حرکت می‌کند در حالی که نور خورشید از میان ابرها می‌تابد و درخششی نرم اطراف شبح شخصیت در مه ایجاد می‌کند. سبک سینمایی واقعی.", "صحنه بعدی (سینمایی)"],
968
- ["examples/8.jpg", "جزئیات پوست سوژه را برجسته‌تر و طبیعی‌تر کن.", "روتوش پوست"],
969
- ["examples/6.jpg", "دوربین را به نمای پایین به بالا تغییر بده.", "تغییر زاویه دید"],
970
- ],
971
- inputs=[input_image, prompt, lora_adapter],
972
- outputs=[output_image, seed, status_box],
973
- fn=infer_example,
974
- cache_examples=False,
975
- label="نمونه‌ها (برای تست کلیک کنید)"
976
- )
977
-
978
- # جاوااسکریپت برای تزریق متغیر سراسری JS به ورودی Checkbox پایتون
979
- # این تابع مقدار window.userIsVip را می‌خواند و به عنوان آخرین آرگومان به تابع پایتون می‌فرستد
980
- inject_vip_js = """
981
- (img, p, lora, seed, rand, scale, steps, aspect, w, h, vip_dummy) => {
982
- return [img, p, lora, seed, rand, scale, steps, aspect, w, h, window.userIsVip];
983
- }
984
- """
985
 
 
986
  run_button.click(
987
  fn=infer,
988
- inputs=[input_image, prompt, lora_adapter, seed, randomize_seed, guidance_scale, steps, aspect_ratio_selection, custom_width, custom_height, is_vip_state],
989
- outputs=[output_image, seed, status_box],
990
- js=inject_vip_js,
991
- api_name="predict"
 
 
992
  )
993
 
994
- download_button.click(
995
- fn=None,
996
- inputs=[output_image],
997
- outputs=None,
998
- js=js_download_func
999
- )
1000
 
1001
  if __name__ == "__main__":
1002
  demo.queue(max_size=30).launch(show_error=True)
 
4
  import spaces
5
  import torch
6
  import random
7
+ import json
8
+ import hashlib
9
+ from datetime import datetime
10
  from PIL import Image, ImageFilter
 
11
  from gradio.themes import Soft
12
+ from gradio.themes.utils import colors
13
  from deep_translator import GoogleTranslator
14
  from transformers import pipeline
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  # --- تعریف تم ---
17
  colors.steel_blue = colors.Color(
 
31
 
32
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
33
 
34
+ # --- بارگذاری مدل تشخیص محتوای نامناسب (NSFW) ---
35
  print("Loading Safety Checker...")
36
  safety_classifier = pipeline("image-classification", model="Falconsai/nsfw_image_detection", device=-1)
37
 
 
47
  print(f"Safety check error: {e}")
48
  return False
49
 
50
+ # --- بارگذاری مدل اصلی Qwen ---
51
  from diffusers import FlowMatchEulerDiscreteScheduler
52
  from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
53
  from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
54
  from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
55
 
56
  dtype = torch.bfloat16
 
 
57
  print("Loading pipeline...")
58
  pipe = QwenImageEditPlusPipeline.from_pretrained(
59
  "Qwen/Qwen-Image-Edit-2509",
 
90
  "افزایش کیفیت (Upscale)": "upscale-image"
91
  }
92
 
 
 
 
 
 
 
 
 
93
  ASPECT_RATIOS_MAP = {
94
  "خودکار (پیش‌فرض)": "Auto",
95
  "۱:۱ (مربع - 1024x1024)": (1024, 1024),
 
98
  "شخصی‌سازی (Custom)": "Custom"
99
  }
100
 
101
+ # --- لیست کلمات ممنوعه (سختگیرانه و کامل) ---
102
+ # این لیست شامل کلمات جنسی، خشونت آمیز و نامناسب است
103
+ BANNED_WORDS = [
104
+ "nsfw", "nude", "naked", "sex", "sexy", "porn", "erotic", "xxx",
105
+ "breast", "nipple", "genital", "vagina", "penis", "ass", "butt", "sexual",
106
+ "lingerie", "bikini", "swimwear", "underwear", "fetish", "topless",
107
+ "exhibitionism", "hentai", "ecchi", "18+", "hot girl", "sensual",
108
+ "intercourse", "bdsm", "bondage", "anus", "pussy", "dick", "cock",
109
+ "cum", "orgasm", "masturbation", "slut", "whore", "milf", "orgy",
110
+ "penetration", "striptease", "gore", "blood", "violence", "suicide",
111
+ "kill", "death", "murder", "torture", "abuse", "rapist", "rape",
112
+ "cleavage", "areola", "pubic", "upskirt", "intimate"
113
+ ]
114
+
115
+ # --- سیستم مدیریت اعتبار (حافظه موقت) ---
116
+ # ساختار: { "ip_fingerprint_hash": { "date": "YYYY-MM-DD", "count": 0 } }
117
+ USAGE_DB = {}
118
+ MAX_FREE_DAILY_LIMIT = 5
119
+
120
+ def get_quota_status(fingerprint_hash, is_paid, request: gr.Request):
121
  """
122
+ بررسی وضعیت اعتبار کاربر.
123
+ اگر کاربر پولی باشد، همیشه مجاز است.
124
+ اگر رایگان باشد، بر اساس ترکیب IP و Fingerprint بررسی می‌شود.
125
  """
126
+ if is_paid:
127
+ return True, "نامحدود", 0
128
+
129
+ # ترکیب IP و Fingerprint برای جلوگیری از تغییر IP ساده
130
+ client_ip = request.client.host if request else "unknown"
131
+ # ساخت کلید یکتا برای کاربر
132
+ user_key = f"{client_ip}_{fingerprint_hash}"
133
 
134
+ current_date = datetime.now().strftime("%Y-%m-%d")
 
 
 
135
 
136
+ # دریافت یا ایجاد رکورد کاربر
137
+ user_data = USAGE_DB.get(user_key, {"date": current_date, "count": 0})
138
 
139
+ # ریست کردن در روز جدید
140
+ if user_data["date"] != current_date:
141
+ user_data = {"date": current_date, "count": 0}
142
+
143
+ # بررسی محدودیت
144
+ if user_data["count"] >= MAX_FREE_DAILY_LIMIT:
145
+ return False, "اتمام اعتبار", 0
146
+
147
+ remaining = MAX_FREE_DAILY_LIMIT - user_data["count"]
148
+ return True, "مجاز", remaining
149
+
150
+ def increment_usage(fingerprint_hash, is_paid, request: gr.Request):
151
+ if is_paid: return
152
+
153
+ client_ip = request.client.host if request else "unknown"
154
+ user_key = f"{client_ip}_{fingerprint_hash}"
155
+ current_date = datetime.now().strftime("%Y-%m-%d")
156
+
157
+ user_data = USAGE_DB.get(user_key, {"date": current_date, "count": 0})
158
+ if user_data["date"] != current_date:
159
+ user_data = {"date": current_date, "count": 0}
160
+
161
+ user_data["count"] += 1
162
+ USAGE_DB[user_key] = user_data
163
+
164
+ # --- توابع کمکی ---
165
+
166
+ def check_text_safety(text):
167
+ text_lower = text.lower()
168
+ # بررسی دقیق کلمات (با فاصله برای جلوگیری از تطابق اشتباه)
169
+ for word in BANNED_WORDS:
170
+ # چک کردن کلمه به صورت جداگانه
171
+ if f" {word} " in f" {text_lower} " or \
172
+ text_lower.startswith(f"{word} ") or \
173
+ text_lower.endswith(f" {word}") or \
174
+ text_lower == word:
175
  return False
176
+ # چک کردن کلمات ترکیبی خطرناک
177
+ if word in text_lower and len(word) > 4: # برای کلمات طولانی‌تر سختگیرانه‌تر
178
+ return False
 
 
 
 
 
179
  return True
180
 
181
  def translate_prompt(text):
 
204
  new_height = (new_height // 8) * 8
205
  return new_width, new_height
206
 
 
 
 
 
 
 
207
  def get_error_html(message):
208
  return f"""
209
+ <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;">
210
  <span style="font-size: 1.2em;">⛔</span>
211
  {message}
212
  </div>
 
214
 
215
  def get_success_html(message):
216
  return f"""
217
+ <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;">
218
  <span style="font-size: 1.2em;">✅</span>
219
  {message}
220
  </div>
221
  """
222
 
223
+ @spaces.GPU(duration=45)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  def infer(
225
  input_image,
226
  prompt,
 
232
  aspect_ratio_selection,
233
  custom_width,
234
  custom_height,
235
+ fingerprint_hash,
236
+ is_paid_user_str,
237
  request: gr.Request,
238
  progress=gr.Progress(track_tqdm=True)
239
  ):
240
+ # تبدیل وضعیت کاربر از رشته به بولین
241
+ is_paid = is_paid_user_str == "true"
242
+
243
+ # 1. بررسی اعتبار قبل از هر کاری
244
+ allowed, status_msg, remaining = get_quota_status(fingerprint_hash, is_paid, request)
245
+
246
  if not allowed:
247
+ return None, seed, get_error_html("اعتبار ساخت تصویر شما برای امروز تمام شده است. لطفاً برای استفاده نامحدود حساب خود را ارتقا دهید."), f"0 اعتبار"
248
 
249
  if input_image is None:
250
+ return None, seed, get_error_html("لطفاً ابتدا یک تصویر بارگذاری کنید."), f"{remaining} اعتبار"
251
 
252
+ # 2. بررسی ایمنی تصویر ورودی
253
  if is_image_nsfw(input_image):
254
+ return None, seed, get_error_html("تصویر ورودی دارای محتوای نامناسب است و پردازش نمی‌شود."), f"{remaining} اعتبار"
255
 
256
+ # 3. ترجمه و بررسی ایمنی متن
257
  english_prompt = translate_prompt(prompt)
258
  if not check_text_safety(english_prompt):
259
+ return None, seed, get_error_html("متن درخواست شامل کلمات غیرمجاز، غیراخلاقی یا جنسی است."), f"{remaining} اعتبار"
260
 
261
  adapter_internal_name = LORA_MAPPING.get(lora_adapter_persian)
262
  if adapter_internal_name:
 
284
  width, height = selection_value
285
 
286
  try:
 
287
  result = pipe(
288
  image=original_image,
289
  prompt=english_prompt,
 
295
  true_cfg_scale=guidance_scale,
296
  ).images[0]
297
 
298
+ # 4. بررسی ایمنی تصویر خروجی
299
  if is_image_nsfw(result):
300
+ return None, seed, get_error_html("تصویر تولید شده حاوی محتوای نامناسب بود و حذف شد."), f"{remaining} اعتبار"
301
+
302
+ # 5. کسر اعتبار (فقط اگر همه چیز موفق بود)
303
+ increment_usage(fingerprint_hash, is_paid, request)
304
 
305
+ # محاسبه مجدد باقی مانده برای نمایش دقیق
306
+ _, _, new_remaining = get_quota_status(fingerprint_hash, is_paid, request)
307
+ credit_text = "نامحدود" if is_paid else f"{new_remaining} اعتبار برای امروز"
308
 
309
+ return result, seed, get_success_html("تصویر با موفقیت ویرایش شد."), credit_text
310
 
311
  except Exception as e:
312
  error_str = str(e)
313
  if "quota" in error_str.lower() or "exceeded" in error_str.lower():
314
  raise e
315
+ return None, seed, get_error_html(f"خطا در پردازش: {error_str}"), f"{remaining} اعتبار"
316
+
317
+ # --- جاوااسکریپت برای دانلود و ارتباطات ---
318
+ js_func = """
319
+ <script>
320
+ // --- پیکربندی پرداخت ---
321
+ const PREMIUM_PAGE_ID = '1149636';
322
+ const PREMIUM_URL = '#/nav/online/news/getSingle/1149636/eyJpdiI6InZSVUdlLzBlR0FzOHZJdXFZeWhER0E9PSIsInZhbHVlIjoiWFhqRXBLc29vSFpHdk9nYmRjZGVuWHRHRHVSZHRlTG1BUENLaE5mNXBNVVRGWFg3ZWN0djJ5K1dIY1RqTHJGaCIsIm1hYyI6IjIzYzFlZTMwYmVmMTdkYjQ0YTQ4YWMxNmFhN2RmNWQ2OTc1NDIyNGVlZGI3ZjJjMjhkNmQxNjM4MDFlZTIxNmUiLCJ0YWciOiIifQ==/20934991';
323
+
324
+ // --- تولید Fingerprint مرورگر ---
325
+ async function getBrowserFingerprint() {
326
+ const components = [
327
+ navigator.userAgent,
328
+ navigator.language,
329
+ screen.width + 'x' + screen.height,
330
+ new Date().getTimezoneOffset()
331
+ ];
332
+ try {
333
+ const canvas = document.createElement('canvas');
334
+ const ctx = canvas.getContext('2d');
335
+ ctx.textBaseline = "top";
336
+ ctx.font = "14px 'Arial'";
337
+ ctx.textBaseline = "alphabetic";
338
+ ctx.fillStyle = "#f60";
339
+ ctx.fillRect(125, 1, 62, 20);
340
+ ctx.fillStyle = "#069";
341
+ ctx.fillText("alpha_img_edit_fp_v1", 2, 15);
342
+ components.push(canvas.toDataURL());
343
+ } catch (e) {
344
+ components.push("canvas-error");
345
  }
346
+ const fingerprintString = components.join('~~~');
347
+
348
+ // Simple hash function
349
+ let hash = 0;
350
+ for (let i = 0; i < fingerprintString.length; i++) {
351
+ const char = fingerprintString.charCodeAt(i);
352
+ hash = ((hash << 5) - hash) + char;
353
+ hash |= 0;
354
  }
355
+ return 'fp_' + Math.abs(hash).toString(16);
 
 
 
356
  }
 
357
 
358
+ // --- مدیریت ارتباط با والد (Android/Web) ---
359
+ function isUserPaid(userObject) {
360
+ return userObject && userObject.isLogin && userObject.accessible_pages &&
361
+ (userObject.accessible_pages.includes(PREMIUM_PAGE_ID) ||
362
+ userObject.accessible_pages.includes(parseInt(PREMIUM_PAGE_ID)));
363
+ }
364
 
365
+ function updateUI(isPaid) {
366
+ const badge = document.getElementById('subscription-status-badge');
367
+ const upgradeBtn = document.getElementById('upgrade-premium-btn');
368
+ const hiddenStatus = document.querySelector('#is-paid-user textarea');
369
+
370
+ // بروزرسانی مقدار مخفی برای پایتون
371
+ if(hiddenStatus) {
372
+ hiddenStatus.value = isPaid ? "true" : "false";
373
+ hiddenStatus.dispatchEvent(new Event('input'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  }
 
375
 
376
+ if (isPaid) {
377
+ if(badge) {
378
+ badge.textContent = 'نسخه نامحدود';
379
+ badge.className = 'paid-badge';
380
+ badge.style.display = 'inline-block';
381
+ }
382
+ if(upgradeBtn) upgradeBtn.style.display = 'none';
383
+
384
+ // Hide credits text if visible
385
+ const creditDisplay = document.getElementById('credit-display-text');
386
+ if(creditDisplay) creditDisplay.textContent = 'نامحدود';
387
+
388
+ } else {
389
+ if(badge) {
390
+ badge.textContent = 'نسخه رایگان';
391
+ badge.className = 'free-badge';
392
+ badge.style.display = 'inline-block';
393
+ }
394
+ if(upgradeBtn) upgradeBtn.style.display = 'block';
395
+ }
396
+ }
397
+
398
+ // --- مقداردهی اولیه ---
399
+ document.addEventListener('DOMContentLoaded', async () => {
400
+
401
  // 1. Force Light Mode
402
  const forceLight = () => {
403
  const body = document.querySelector('body');
 
411
  forceLight();
412
  setInterval(forceLight, 1000);
413
 
414
+ // 2. Fingerprint Setup
415
+ const fp = await getBrowserFingerprint();
416
+ const fpInput = document.querySelector('#fingerprint-input textarea');
417
+ if(fpInput) {
418
+ fpInput.value = fp;
419
+ fpInput.dispatchEvent(new Event('input'));
420
+ }
421
 
422
+ // 3. Request User Status
423
+ window.parent.postMessage({ type: 'REQUEST_USER_STATUS' }, '*');
 
424
 
425
+ // 4. Listen for Response
426
+ window.addEventListener('message', (event) => {
427
+ if (event.data && event.data.type === 'USER_STATUS_RESPONSE') {
428
+ try {
429
+ const userObject = JSON.parse(event.data.payload);
430
+ const isPaid = isUserPaid(userObject);
431
+ updateUI(isPaid);
432
+ } catch (e) {
433
+ console.error("Error parsing user status", e);
434
+ updateUI(false);
435
+ }
436
+ }
437
+ });
438
+
439
+ // 5. Upgrade Button Logic
440
+ window.triggerUpgrade = function() {
441
+ window.parent.postMessage({
442
+ type: 'NAVIGATE_TO_PREMIUM',
443
+ payload: { url: PREMIUM_URL }
444
+ }, '*');
445
  };
446
+
447
+ // 6. Download Logic
448
+ window.downloadImage = async (image) => {
449
+ if (!image) { alert("لطفاً ابتدا تصویر را تولید کنید."); return; }
450
+ let fileUrl = image.url;
451
+ if (fileUrl && !fileUrl.startsWith('http')) {
452
+ fileUrl = window.location.origin + fileUrl;
453
+ } else if (!fileUrl && image.path) {
454
+ fileUrl = window.location.origin + "/file=" + image.path;
455
+ }
456
+ window.parent.postMessage({
457
+ type: 'DOWNLOAD_REQUEST',
458
+ url: fileUrl
459
+ }, '*');
460
+ }
461
+
462
+ // 7. Error Modal Logic (Quota Exceeded from Gradio Backend)
463
  const showQuotaModal = () => {
464
  if (document.getElementById('custom-quota-modal')) return;
465
+ // ساخت مودال مشابه پادکست نیست، بلکه مودال IP Reset قبلی است
466
+ // اما اینجا ما سیستم کردیت داریم، پس ارور کردیت را مدیریت می‌کنیم
467
+ // اگر ارور سمت سرور "Quota Exceeded" بود یعنی کاربر رایگان محدودیتش تمام شده
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
  });
470
  </script>
471
  """
472
 
 
473
  css_code = """
474
  <style>
475
  @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700&display=swap');
 
477
  :root, .dark, body, .gradio-container {
478
  --body-background-fill: #f5f7fa !important;
479
  --body-text-color: #1f2937 !important;
480
+ --app-font: 'Vazirmatn', sans-serif !important;
 
 
 
 
 
 
481
  color-scheme: light !important;
482
  }
483
 
484
+ body { font-family: var(--app-font); direction: rtl; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
 
486
+ /* --- Badges & Buttons Styles (From Podcast Code) --- */
487
+ @keyframes badge-fade-in { from { opacity: 0; transform: translateY(-10px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } }
 
 
 
 
 
 
488
 
489
+ #subscription-status-badge {
490
+ display: none; /* Controlled by JS */
491
+ padding: 6px 16px;
492
+ border-radius: 20px;
493
+ font-size: 0.9em;
494
+ font-weight: 700;
495
+ margin-top: 1rem;
496
+ margin-bottom: 1rem;
497
+ letter-spacing: 0.5px;
498
+ text-shadow: 0 1px 2px rgba(0,0,0,0.1);
499
+ animation: badge-fade-in 0.6s 0.5s ease-out backwards;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
 
502
+ .free-badge {
503
+ background: linear-gradient(45deg, #6c757d, #495057);
504
+ color: white;
505
+ box-shadow: 0 4px 10px rgba(108, 117, 125, 0.3);
506
  }
507
+
508
+ .paid-badge {
509
+ background: linear-gradient(45deg, #FFC107, #ffca2c);
510
+ color: #333;
511
+ box-shadow: 0 4px 10px rgba(255, 193, 7, 0.3);
512
  }
513
 
514
+ @keyframes upgrade-btn-pulse {
515
+ 0% { transform: scale(1); box-shadow: 0 8px 20px -5px rgba(255, 193, 7, 0.3); }
516
+ 50% { transform: scale(1.02); box-shadow: 0 12px 25px -5px rgba(255, 193, 7, 0.3); }
517
+ 100% { transform: scale(1); box-shadow: 0 8px 20px -5px rgba(255, 193, 7, 0.3); }
518
  }
519
+
520
+ #upgrade-premium-btn {
521
+ display: none; /* Controlled by JS */
522
+ width: 100%;
523
+ margin-top: 1.5rem;
524
+ padding: 1rem;
525
+ font-family: var(--app-font);
526
+ font-size: 1.1em;
527
+ font-weight: 800;
528
+ color: #212529;
529
+ background: linear-gradient(95deg, #FFD54F, #FFC107 100%);
530
+ border: none;
531
+ border-radius: 14px;
532
+ cursor: pointer;
533
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
534
+ box-shadow: 0 8px 20px -5px rgba(255, 193, 7, 0.3);
535
+ animation: upgrade-btn-pulse 2.5s infinite;
536
+ text-align: center;
537
  }
538
 
539
+ #upgrade-premium-btn:hover {
540
+ transform: translateY(-3px);
541
+ box-shadow: 0 12px 25px -5px rgba(255, 193, 7, 0.4);
542
+ animation-play-state: paused;
 
 
543
  }
544
 
545
+ /* --- Layout --- */
546
  #col-container {
547
  margin: 0 auto;
548
  max-width: 980px;
 
559
  font-size: 2.4em !important;
560
  text-align: center;
561
  color: #1a202c !important;
562
+ margin-bottom: 5px;
563
  font-weight: 800;
564
  background: -webkit-linear-gradient(45deg, #2563eb, #1e40af);
565
  -webkit-background-clip: text;
 
570
  text-align: center;
571
  font-size: 1.15em;
572
  color: #4b5563 !important;
573
+ margin-bottom: 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
574
  }
575
 
576
+ /* --- Gradio Overrides --- */
577
+ .primary-btn {
 
 
 
 
578
  background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important;
579
  border: none !important;
580
  color: white !important;
 
583
  padding: 14px 28px !important;
584
  border-radius: 14px !important;
585
  box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3) !important;
 
586
  cursor: pointer !important;
587
  width: 100%;
 
 
 
 
 
 
 
 
 
 
588
  }
589
+ .primary-btn:hover { transform: translateY(-2px); }
590
 
591
  #download-btn {
592
  background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
593
  box-shadow: 0 4px 15px rgba(59, 130, 246, 0.3) !important;
594
  }
 
 
 
595
 
596
+ textarea, input[type="text"] {
597
+ font-family: var(--app-font) !important;
 
 
 
598
  border-radius: 12px !important;
599
+ border: 2px solid #e2e8f0 !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
600
  }
601
 
602
  footer { display: none !important; }
603
  .flagging { display: none !important; }
604
 
605
+ /* Toast/Error hiding */
606
+ .toast-body, .toast-wrap {
607
+ display: none !important;
608
+ opacity: 0 !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
609
  }
610
  </style>
611
  """
612
 
613
  # ادغام CSS و JS
614
+ combined_html = css_code + js_func
615
 
616
+ with gr.Blocks(title="Alpha Image Editor") as demo:
 
 
617
  gr.HTML(combined_html)
618
 
619
+ # متغیرهای مخفی برای ارتباط با JS
620
+ fingerprint_hash = gr.Textbox(elem_id="fingerprint-input", visible=False)
621
+ is_paid_user = gr.Textbox(elem_id="is-paid-user", value="false", visible=False)
622
+
623
  with gr.Column(elem_id="col-container"):
624
  gr.Markdown("# **ویرایشگر هوشمند آلفا**", elem_id="main-title")
625
  gr.Markdown(
 
627
  elem_id="main-description"
628
  )
629
 
630
+ # بخش نشان وضعیت کاربر
631
  gr.HTML("""
632
+ <div style="text-align: center;">
633
+ <div id="subscription-status-badge">در حال بررسی...</div>
 
 
 
 
 
634
  </div>
635
  """)
636
 
 
646
  lines=3
647
  )
648
 
649
+ credit_status = gr.Textbox(label="اعتبار باقی‌مانده", value="...", interactive=False, elem_id="credit-display-text")
650
  status_box = gr.HTML(label="وضعیت")
651
 
 
 
 
 
652
  run_button = gr.Button("✨ شروع پردازش و ساخت تصویر", variant="primary", elem_classes="primary-btn", elem_id="run-btn")
653
+
654
+ # دکمه ارتقا (توسط JS کنترل می‌شود)
655
+ gr.HTML("""
656
+ <button type="button" id="upgrade-premium-btn" onclick="window.triggerUpgrade()">
657
+ ⭐️ ارتقا به نسخه کامل و نامحدود
658
+ </button>
659
+ """)
660
 
661
  with gr.Column():
662
  output_image = gr.Image(label="تصویر نهایی", interactive=False, format="png", height=380)
 
673
  with gr.Accordion("تنظیمات پیشرفته", open=False, visible=True):
674
  aspect_ratio_selection = gr.Dropdown(
675
  label="ابعاد تصویر خروجی",
676
+ choices=list(ASPECT_RATIOS_MAP.keys()),
677
  value="خودکار (پیش‌فرض)",
678
  interactive=True
679
  )
680
 
681
  with gr.Row(visible=False) as custom_dims_row:
682
+ custom_width = gr.Slider(minimum=256, maximum=2048, step=8, value=1024, label="Width")
683
+ custom_height = gr.Slider(minimum=256, maximum=2048, step=8, value=1024, label="Height")
 
 
 
 
 
 
684
 
685
  seed = gr.Slider(label="دانه تصادفی (Seed)", minimum=0, maximum=MAX_SEED, step=1, value=0)
686
  randomize_seed = gr.Checkbox(label="استفاده از Seed تصادفی", value=True)
687
+ guidance_scale = gr.Slider(label="Guidance Scale", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
688
+ steps = gr.Slider(label="Steps", minimum=1, maximum=50, step=1, value=4)
689
 
 
690
  def toggle_row(choice):
691
+ return gr.update(visible=(choice == "شخصی‌سازی (Custom)"))
 
 
 
 
 
 
 
 
692
 
693
+ aspect_ratio_selection.change(fn=toggle_row, inputs=aspect_ratio_selection, outputs=custom_dims_row)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
694
 
695
+ # اتصال رویداد کلیک
696
  run_button.click(
697
  fn=infer,
698
+ inputs=[
699
+ input_image, prompt, lora_adapter, seed, randomize_seed,
700
+ guidance_scale, steps, aspect_ratio_selection, custom_width, custom_height,
701
+ fingerprint_hash, is_paid_user
702
+ ],
703
+ outputs=[output_image, seed, status_box, credit_status]
704
  )
705
 
706
+ download_button.click(fn=None, inputs=[output_image], outputs=None, js="window.downloadImage")
 
 
 
 
 
707
 
708
  if __name__ == "__main__":
709
  demo.queue(max_size=30).launch(show_error=True)