Opera8 commited on
Commit
582db3a
·
verified ·
1 Parent(s): 1750045

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +486 -397
app.py CHANGED
@@ -4,80 +4,14 @@ import numpy as np
4
  import spaces
5
  import torch
6
  import random
7
- import sqlite3
8
- import json
9
- from datetime import datetime
10
  from PIL import Image, ImageFilter
11
  from typing import Iterable
12
  from gradio.themes import Soft
13
  from gradio.themes.utils import colors, fonts, sizes
14
  from deep_translator import GoogleTranslator
15
  from transformers import pipeline
16
-
17
- # --- تنظیمات دیتابیس برای مدیریت کردیت کاربران ---
18
- DB_NAME = "user_limits.db"
19
-
20
- def init_db():
21
- """ایجاد جدول برای ذخیره تعداد استفاده کاربران"""
22
- conn = sqlite3.connect(DB_NAME)
23
- c = conn.cursor()
24
- c.execute('''
25
- CREATE TABLE IF NOT EXISTS usage_logs (
26
- user_id TEXT,
27
- date TEXT,
28
- count INTEGER,
29
- PRIMARY KEY (user_id, date)
30
- )
31
- ''')
32
- conn.commit()
33
- conn.close()
34
-
35
- init_db()
36
-
37
- def check_and_update_quota(user_id, is_premium):
38
- """
39
- بررسی مجاز بودن کاربر برای تولید تصویر
40
- کاربران پریمیوم: همیشه مجاز
41
- کاربران رایگان: حداکثر 5 تصویر در روز
42
- """
43
- if not user_id:
44
- return True, "Guest" # اگر یوزر آیدی نبود، سختگیری نمی‌کنیم (یا می‌توان محدود کرد)
45
-
46
- if is_premium:
47
- return True, "نامحدود"
48
-
49
- today = datetime.now().strftime("%Y-%m-%d")
50
-
51
- conn = sqlite3.connect(DB_NAME)
52
- c = conn.cursor()
53
-
54
- c.execute("SELECT count FROM usage_logs WHERE user_id = ? AND date = ?", (user_id, today))
55
- result = c.fetchone()
56
-
57
- current_count = result[0] if result else 0
58
- limit = 5
59
-
60
- conn.close()
61
-
62
- if current_count >= limit:
63
- return False, f"0 از {limit}"
64
-
65
- return True, f"{limit - current_count} از {limit}"
66
-
67
- def increment_usage(user_id, is_premium):
68
- """افزایش شمارنده استفاده کاربر پس از تولید موفق"""
69
- if not user_id or is_premium:
70
- return
71
-
72
- today = datetime.now().strftime("%Y-%m-%d")
73
- conn = sqlite3.connect(DB_NAME)
74
- c = conn.cursor()
75
-
76
- c.execute("INSERT OR IGNORE INTO usage_logs (user_id, date, count) VALUES (?, ?, 0)", (user_id, today))
77
- c.execute("UPDATE usage_logs SET count = count + 1 WHERE user_id = ? AND date = ?", (user_id, today))
78
-
79
- conn.commit()
80
- conn.close()
81
 
82
  # --- تعریف تم ---
83
  colors.steel_blue = colors.Color(
@@ -97,74 +31,21 @@ colors.steel_blue = colors.Color(
97
 
98
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
99
 
100
- # --- بارگذاری سیستم امنیتی دوگانه (Dual Safety System) ---
101
- print("Loading Safety Checkers...")
102
-
103
- # مدل اول: سریع و استاندارد
104
- safety_classifier_1 = pipeline("image-classification", model="Falconsai/nsfw_image_detection", device=-1)
105
-
106
- # مدل دوم: بسیار دقیق و سخت‌گیر (ViT)
107
- safety_classifier_2 = pipeline("image-classification", model="AdamCodd/vit-base-nsfw-detector", device=-1)
108
 
109
  def is_image_nsfw(image):
110
  if image is None: return False
111
  try:
112
- # بررسی با مدل اول
113
- results1 = safety_classifier_1(image)
114
- for result in results1:
115
- if result['label'] == 'nsfw' and result['score'] > 0.5:
116
- print(f"Safety Check 1 Failed: {result['score']}")
117
- return True
118
-
119
- # بررسی با مدل دوم
120
- results2 = safety_classifier_2(image)
121
- for result in results2:
122
- label = result['label'].lower()
123
- score = result['score']
124
-
125
- if label == 'nsfw' and score > 0.3:
126
- print(f"Safety Check 2 (NSFW) Failed: {score}")
127
- return True
128
-
129
- if label in ['sexy', 'porn', 'hentai'] and score > 0.4:
130
- print(f"Safety Check 2 (Partial) Failed: {label} - {score}")
131
  return True
132
-
133
  return False
134
  except Exception as e:
135
  print(f"Safety check error: {e}")
136
- return True
137
-
138
- # --- لیست کلمات ممنوعه ---
139
- BANNED_WORDS = [
140
- "nsfw", "nude", "naked", "sex", "porn", "erotic", "xxx", "18+", "uncensored",
141
- "breast", "nipple", "areola", "cleavage", "topless", "open chest",
142
- "genital", "vagina", "penis", "dick", "cock", "pussy", "ass", "butt", "anus",
143
- "lingerie", "bikini", "swimwear", "underwear", "panties", "bra",
144
- "fetish", "bdsm", "bondage", "exhibitionism", "voyeur",
145
- "hentai", "ecchi", "ahegao", "paizuri",
146
- "undressed", "stripping", "naked body", "exposed skin", "sheer", "see-through",
147
- "rape", "violence", "blood", "gore", "sexual"
148
- ]
149
-
150
- def check_text_safety(text):
151
- if not text: return True
152
- text_lower = text.lower()
153
- for word in BANNED_WORDS:
154
- if word in text_lower:
155
- print(f"Banned word found: {word}")
156
- return False
157
- return True
158
-
159
- def translate_prompt(text):
160
- if not text:
161
- return ""
162
- try:
163
- translated = GoogleTranslator(source='auto', target='en').translate(text)
164
- return translated
165
- except Exception as e:
166
- print(f"Translation Error: {e}")
167
- return text
168
 
169
  # --- بارگذاری مدل اصلی ---
170
  from diffusers import FlowMatchEulerDiscreteScheduler
@@ -175,7 +56,7 @@ from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
175
  dtype = torch.bfloat16
176
  device = "cuda" if torch.cuda.is_available() else "cpu"
177
 
178
- print("Loading Generation Pipeline...")
179
  pipe = QwenImageEditPlusPipeline.from_pretrained(
180
  "Qwen/Qwen-Image-Edit-2509",
181
  transformer=QwenImageTransformer2DModel.from_pretrained(
@@ -227,6 +108,30 @@ ASPECT_RATIOS_MAP = {
227
  "شخصی‌سازی (Custom)": "Custom"
228
  }
229
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  def update_dimensions_on_upload(image):
231
  if image is None:
232
  return 1024, 1024
@@ -243,6 +148,12 @@ def update_dimensions_on_upload(image):
243
  new_height = (new_height // 8) * 8
244
  return new_width, new_height
245
 
 
 
 
 
 
 
246
  def get_error_html(message):
247
  return f"""
248
  <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;">
@@ -259,16 +170,6 @@ def get_success_html(message):
259
  </div>
260
  """
261
 
262
- def get_limit_error_html():
263
- return f"""
264
- <div style="background-color: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 16px; border-radius: 12px; text-align: center; margin-bottom: 15px; direction: rtl;">
265
- <h3 style="margin: 0 0 10px 0;">🚫 محدودیت اعتبار روزانه</h3>
266
- <p style="margin: 0;">شما از سهمیه ۵ تصویر رایگان امروز خود استفاده کرده‌اید.</p>
267
- <p style="margin: 8px 0 0 0; font-size: 0.9em;">برای استفاده نامحدود، حساب خود را به نسخه ویژه ارتقا دهید.</p>
268
- <button onclick="window.parent.postMessage({{type: 'NAVIGATE_TO_PREMIUM'}}, '*')" style="margin-top: 12px; background: linear-gradient(90deg, #FFD700, #FFA500); border: none; padding: 8px 16px; border-radius: 20px; color: black; font-weight: bold; cursor: pointer;">ارتقا به نسخه ویژه ⭐️</button>
269
- </div>
270
- """
271
-
272
  @spaces.GPU(duration=30)
273
  def infer(
274
  input_image,
@@ -281,26 +182,14 @@ def infer(
281
  aspect_ratio_selection,
282
  custom_width,
283
  custom_height,
284
- user_id_hidden, # دریافت شناسه کاربر از فرانت
285
- is_premium_hidden, # دریافت وضعیت پریمیوم از فرانت
286
  progress=gr.Progress(track_tqdm=True)
287
  ):
288
- # --- بررسی اعتبار کاربر ---
289
- is_premium = str(is_premium_hidden).lower() == "true"
290
- allowed, limit_msg = check_and_update_quota(user_id_hidden, is_premium)
291
-
292
- if not allowed:
293
- return None, seed, get_limit_error_html()
294
-
295
- # --- بررسی وجود تصویر ---
296
  if input_image is None:
297
  return None, seed, get_error_html("لطفاً ابتدا یک تصویر بارگذاری کنید.")
298
 
299
- # --- بررسی امنیتی ورودی ---
300
  if is_image_nsfw(input_image):
301
  return None, seed, get_error_html("تصویر ورودی دارای محتوای نامناسب است و پردازش نمی‌شود.")
302
 
303
- # --- ترجمه و بررسی متن ---
304
  english_prompt = translate_prompt(prompt)
305
  if not check_text_safety(english_prompt):
306
  return None, seed, get_error_html("متن درخواست شامل کلمات غیرمجاز یا غیراخلاقی است.")
@@ -314,13 +203,7 @@ def infer(
314
 
315
  generator = torch.Generator(device=device).manual_seed(seed)
316
 
317
- safety_negative = (
318
- "nsfw, nude, naked, porn, sexual, xxx, breast, nipple, areola, genital, "
319
- "vagina, penis, ass, lingerie, bikini, swimwear, underwear, fetish, "
320
- "topless, open chest, revealing clothes, cleavage, see-through, sheer, "
321
- "gore, violence, blood, navel, midriff, exposed skin, erotic, ecchi, hentai, "
322
- "uncensored, stripping"
323
- )
324
  base_negative = "worst quality, low quality, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, jpeg artifacts, signature, watermark, username, blurry"
325
  final_negative_prompt = f"{safety_negative}, {base_negative}"
326
 
@@ -348,13 +231,9 @@ def infer(
348
  true_cfg_scale=guidance_scale,
349
  ).images[0]
350
 
351
- # --- بررسی امنیتی خروجی ---
352
  if is_image_nsfw(result):
353
  return None, seed, get_error_html("تصویر تولید شده حاوی محتوای نامناسب بود و حذف شد.")
354
 
355
- # --- کسر اعتبار (فقط برای کاربران رایگان) ---
356
- increment_usage(user_id_hidden, is_premium)
357
-
358
  return result, seed, get_success_html("تصویر با موفقیت ویرایش شد.")
359
 
360
  except Exception as e:
@@ -365,8 +244,7 @@ def infer(
365
 
366
  @spaces.GPU(duration=30)
367
  def infer_example(input_image, prompt, lora_adapter):
368
- # برای مثال‌ها، آیدی کاربر فرضی و پریمیوم در نظر گرفته می‌شود تا محدودیت اعمال نشود
369
- res, s, status = infer(input_image, prompt, lora_adapter, 0, True, 1.0, 4, "خودکار (پیش‌فرض)", 1024, 1024, "example_user", "true")
370
  return res, s, status
371
 
372
  # --- جاوااسکریپت برای دکمه دانلود ---
@@ -389,85 +267,362 @@ async (image) => {
389
  }
390
  """
391
 
392
- # --- جاوااسکریپت سراسری (شناسایی کاربر، پیام رفع محدودیت، دانلود) ---
393
- js_global_content = """
394
  <script>
395
- document.addEventListener('DOMContentLoaded', () => {
396
- // --------------------------------------------------------
397
- // 1. User Identification & Credit Logic (PostMessage)
398
- // --------------------------------------------------------
399
- const PREMIUM_PAGE_ID = '1149636';
400
- let userId = "guest_" + Math.random().toString(36).substr(2, 9);
401
- let isPremium = false;
402
-
403
- // Listen for response from Parent (Android/Iframe)
404
- window.addEventListener('message', (event) => {
405
- if (event.data && event.data.type === 'USER_DATA_RESPONSE') {
406
- const payload = event.data.payload;
407
-
408
- if (payload) {
409
- try {
410
- const userObj = JSON.parse(payload);
411
- if (userObj.id) userId = userObj.id;
412
-
413
- // Check Premium Status
414
- if (userObj.isLogin && userObj.accessible_pages) {
415
- const pages = userObj.accessible_pages;
416
- // Check if premium page ID exists in allowed pages
417
- if (pages.includes(PREMIUM_PAGE_ID) || pages.includes(parseInt(PREMIUM_PAGE_ID))) {
418
- isPremium = true;
419
- }
420
- }
421
- } catch (e) {
422
- console.error("Error parsing user data:", e);
423
- }
424
- }
425
- updateUserStatusUI();
426
- updateHiddenInputs();
427
- }
428
- });
 
429
 
430
- // Request User Data on Load
431
- window.parent.postMessage({ type: 'REQUEST_USER_DATA' }, '*');
 
 
 
432
 
433
- function updateUserStatusUI() {
434
- const badge = document.getElementById('user-status-badge');
435
- if (!badge) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
436
 
437
- if (isPremium) {
438
- badge.className = 'status-badge premium';
439
- badge.innerHTML = '⭐️ نسخه نامحدود ویژه';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  } else {
441
- badge.className = 'status-badge free';
442
- badge.innerHTML = '👤 نسخه رایگان (۵ تصویر در روز)';
443
  }
444
- badge.style.display = 'inline-flex';
445
  }
446
-
447
- function updateHiddenInputs() {
448
- // Find hidden textboxes in Gradio (hacky but standard for Gradio)
449
- // We look for elements with specific IDs assigned in Python
450
- const idInput = document.querySelector('#hidden_user_id textarea');
451
- const premiumInput = document.querySelector('#hidden_user_premium textarea');
452
-
453
- if (idInput) {
454
- idInput.value = userId;
455
- idInput.dispatchEvent(new Event('input', { bubbles: true }));
 
 
 
456
  }
457
- if (premiumInput) {
458
- premiumInput.value = isPremium ? "true" : "false";
459
- premiumInput.dispatchEvent(new Event('input', { bubbles: true }));
 
 
 
460
  }
 
461
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462
 
463
- // Retry finding elements if Gradio hasn't fully loaded them
464
- setTimeout(updateHiddenInputs, 2000);
465
- setTimeout(updateHiddenInputs, 5000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
466
 
 
467
 
468
- // --------------------------------------------------------
469
- // 2. Force Light Mode
470
- // --------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
  const forceLight = () => {
472
  const body = document.querySelector('body');
473
  if (body) {
@@ -475,14 +630,11 @@ document.addEventListener('DOMContentLoaded', () => {
475
  body.style.backgroundColor = '#f5f7fa';
476
  body.style.color = '#333333';
477
  }
478
- document.querySelectorAll('.dark').forEach(el => el.classList.remove('dark'));
479
  };
480
  forceLight();
481
  setInterval(forceLight, 1000);
482
 
483
- // --------------------------------------------------------
484
- // 3. Retry Functionality
485
- // --------------------------------------------------------
486
  window.retryGeneration = function() {
487
  const modal = document.getElementById('custom-quota-modal');
488
  if (modal) modal.remove();
@@ -495,82 +647,39 @@ document.addEventListener('DOMContentLoaded', () => {
495
  if (modal) modal.remove();
496
  };
497
 
498
- // --------------------------------------------------------
499
- // 4. Quota Modal (IP Reset Guide)
500
- // --------------------------------------------------------
501
  const showQuotaModal = () => {
502
  if (document.getElementById('custom-quota-modal')) return;
503
-
504
  const modalHtml = `
505
  <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;">
506
  <div class="ip-reset-guide-container">
507
  <div class="guide-header">
508
- <svg class="guide-header-icon" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
509
- <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>
510
- <circle cx="50" cy="50" r="45" fill="url(#grad1)" opacity="0.1"></circle>
511
- <circle cx="50" cy="50" r="35" fill="none" stroke="url(#grad1)" stroke-width="2" opacity="0.3"></circle>
512
- <path d="M35 50 L45 60 L65 40" stroke="url(#grad1)" stroke-width="4" fill="none" stroke-linecap="round" stroke-linejoin="round"></path>
513
- <circle cx="65" cy="35" r="8" fill="#fee140"></circle>
514
- <path d="M62 35 L68 35 M65 32 L65 38" stroke="white" stroke-width="2" stroke-linecap="round"></path>
515
- </svg>
516
  <div>
517
  <h2>یک قدم تا ساخت تصاویر جدید</h2>
518
  <p>نیازمند تغییر نقطه دستیابی</p>
519
  </div>
520
  </div>
521
  <div class="guide-content">
522
- <div class="info-card">
523
- <div class="info-card-header">
524
- <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>
525
- <span class="info-card-title">راه حل سریع</span>
526
- </div>
527
- <p>طبق ویدیو آموزشی پایین بین نقطه دستیابی جابجا شوید تلاش مجدد بزنید تا تصاویر مجدداً تولید بشه.</p>
528
- </div>
529
- <div class="summary-section">
530
- <div class="summary-header">
531
- <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>
532
- <span class="summary-title">خلاصه راهنما</span>
533
- </div>
534
- <div class="summary-text">هربار که این صفحه را مشاهده کردید: از اینترنت سیم‌کارت استفاده کنید، VPN را خاموش کرده و طبق ویدیو آموزشی پایین نقطه دستیابی رو تغییر دهید. «تلاش مجدد» کلیک کنید.</div>
535
- </div>
536
- <div class="video-button-container">
537
- <button onclick="parent.postMessage({ type: 'NAVIGATE_TO_URL', url: '#/nav/online/news/getSingle/1149635/eyJpdiI6IjhHVGhPQWJwb3E0cjRXbnFWTW5BaUE9PSIsInZhbHVlIjoiS1V0dTdvT21wbXAwSXZaK1RCTG1pVXZqdlFJa1hXV1RKa2FLem9zU3pXMjd5MmlVOGc2YWY0NVdNR3h3Smp1aSIsIm1hYyI6IjY1NTA5ZDYzMjAzMTJhMGQyMWQ4NjA4ZDgyNGZjZDVlY2MyNjdiMjA2NWYzOWRjY2M4ZmVjYWRlMWNlMWQ3ODEiLCJ0YWciOiIifQ==/21135210' }, '*')" class="elegant-video-button">
538
- <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>
539
- <span>دیدن ویدیو آموزشی استفاده نامحدود</span>
540
- </button>
541
  </div>
542
  </div>
543
- <div class="guide-actions">
544
- <button class="action-button back-button" onclick="window.closeErrorModal()">
545
- <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>
546
- <span>بازگشت</span>
547
- </button>
548
- <button class="action-button retry-button" onclick="window.retryGeneration()">
549
- <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>
550
- <span>تلاش مجدد</span>
551
- </button>
552
- </div>
553
  </div>
554
  </div>
555
  `;
556
  document.body.insertAdjacentHTML('beforeend', modalHtml);
557
- setTimeout(() => { window.closeErrorModal(); }, 10000);
558
  };
559
 
560
- // --------------------------------------------------------
561
- // 5. Error Scanner
562
- // --------------------------------------------------------
563
  setInterval(() => {
564
- const potentialErrors = document.querySelectorAll('.toast-body, .error, .toast-wrap, .eta-bar, div[class*="error"]');
565
  potentialErrors.forEach(el => {
566
  const text = el.innerText || "";
567
  if (text.toLowerCase().includes('quota') || text.toLowerCase().includes('exceeded')) {
568
  showQuotaModal();
569
  el.style.display = 'none';
570
- el.style.opacity = '0';
571
- el.innerText = '';
572
- const parentWrap = el.closest('.toast-wrap');
573
- if(parentWrap) parentWrap.style.display = 'none';
574
  }
575
  });
576
  }, 100);
@@ -578,146 +687,100 @@ document.addEventListener('DOMContentLoaded', () => {
578
  </script>
579
  """
580
 
581
- # --- CSS Updated ---
582
- css_code = """
583
- <style>
584
- @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700&display=swap');
585
-
586
- :root, .dark, body, .gradio-container {
587
- --body-background-fill: #f5f7fa !important;
588
- --body-text-color: #1f2937 !important;
589
- font-family: 'Vazirmatn', sans-serif !important;
590
- }
591
-
592
- /* User Status Badge CSS */
593
- .status-badge {
594
- display: none; /* Initially hidden until JS detects user */
595
- align-items: center;
596
- justify-content: center;
597
- padding: 8px 16px;
598
- border-radius: 50px;
599
- font-size: 0.95em;
600
- font-weight: 700;
601
- margin: 10px auto 20px auto;
602
- width: fit-content;
603
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
604
- animation: fadeInBadge 0.5s ease-out;
605
- }
606
-
607
- .status-badge.premium {
608
- background: linear-gradient(135deg, #FFD700 0%, #FDB931 100%);
609
- color: #333;
610
- border: 2px solid #fff;
611
- box-shadow: 0 0 15px rgba(255, 215, 0, 0.4);
612
- }
613
-
614
- .status-badge.free {
615
- background: #e2e8f0;
616
- color: #475569;
617
- border: 1px solid #cbd5e1;
618
- }
619
-
620
- @keyframes fadeInBadge {
621
- from { opacity: 0; transform: translateY(-10px); }
622
- to { opacity: 1; transform: translateY(0); }
623
- }
624
-
625
- /* Existing CSS for Guide, Buttons, etc. */
626
- .ip-reset-guide-container {
627
- text-align: right; direction: rtl; background: rgba(255, 255, 255, 0.98);
628
- backdrop-filter: blur(10px); padding: 20px; border-radius: 16px;
629
- box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1); border: 1px solid rgba(102, 126, 234, 0.2);
630
- width: 90%; max-width: 420px; max-height: 90vh; overflow-y: auto;
631
- font-family: 'Vazirmatn', sans-serif !important;
632
- }
633
- .guide-header { display: flex; align-items: center; margin-bottom: 15px; }
634
- .guide-header-icon { width: 45px; height: 45px; margin-left: 15px; }
635
- .guide-header h2 { font-size: 1.2rem; margin: 0; }
636
- .guide-content { font-size: 0.9rem; line-height: 1.6; }
637
- .info-card, .summary-section { margin-top: 12px; padding: 12px; border-radius: 12px; }
638
- .info-card { background: #f0f4ff; border: 1px solid #c3dafe; }
639
- .summary-section { background: #f0fff4; border: 1px solid #c6f6d5; }
640
- .video-button-container { text-align: center; margin-top: 15px; }
641
- .elegant-video-button { padding: 8px 16px; border-radius: 20px; border: 1px solid #ddd; background: white; cursor: pointer; display: inline-flex; align-items: center; }
642
- .guide-actions { display: flex; gap: 10px; margin-top: 20px; }
643
- .action-button { flex: 1; padding: 10px; border-radius: 10px; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; font-weight: bold; }
644
- .back-button { background: white; border: 1px solid #ddd; }
645
- .retry-button { background: #667eea; color: white; }
646
-
647
- /* Main UI Tweaks */
648
- #col-container { max-width: 980px; margin: 0 auto; direction: rtl; text-align: right; background: white; padding: 20px; border-radius: 20px; }
649
- #main-title h1 { text-align: center; color: #2563eb; }
650
- #main-description { text-align: center; color: #4b5563; }
651
- .primary-btn { background: linear-gradient(135deg, #10b981, #059669); color: white; border: none; }
652
- footer { display: none !important; }
653
- </style>
654
- """
655
-
656
- combined_html = css_code + js_global_content
657
 
 
658
  with gr.Blocks() as demo:
 
659
  gr.HTML(combined_html)
660
 
661
  with gr.Column(elem_id="col-container"):
662
  gr.Markdown("# **ویرایشگر هوشمند آلفا**", elem_id="main-title")
663
- gr.Markdown("با هوش مصنوعی آلفا تصاویر تونو به مدل های مختلف ویرایش کنید.", elem_id="main-description")
 
 
 
664
 
665
- # بج وضعیت کاربر (HTML خالی که توسط JS پر می‌شود)
666
- gr.HTML('<div id="user-status-badge" class="status-badge"></div>')
667
-
668
- # فیلدهای مخفی برای انتقال اطلاعات کاربر از JS به پایتون
669
- user_id_storage = gr.Textbox(visible=False, elem_id="hidden_user_id")
670
- premium_status_storage = gr.Textbox(visible=False, elem_id="hidden_user_premium")
 
671
 
672
  with gr.Row(equal_height=True):
673
  with gr.Column():
674
  input_image = gr.Image(label="بارگذاری تصویر", type="pil", height=320)
675
- prompt = gr.Text(label="دستور ویرایش (به فارسی)", placeholder="مثال: تصویر را به سبک انیمه تبدیل کن...", rtl=True, lines=3)
 
 
 
 
 
 
 
 
676
  status_box = gr.HTML(label="وضعیت")
677
- run_button = gr.Button("✨ شروع پردازش", variant="primary", elem_classes="primary-btn", elem_id="run-btn")
 
 
 
 
 
678
 
679
  with gr.Column():
680
  output_image = gr.Image(label="تصویر نهایی", interactive=False, format="png", height=380)
 
681
  download_button = gr.Button("📥 دانلود و ذخیره تصویر", variant="secondary", elem_id="download-btn", elem_classes="primary-btn")
682
 
683
  with gr.Row():
684
- lora_adapter = gr.Dropdown(label="انتخاب سبک", choices=list(LORA_MAPPING.keys()), value="تبدیل عکس به انیمه")
 
 
 
 
685
 
686
- with gr.Accordion("تنظیمات پیشرفته", open=False):
687
- aspect_ratio_selection = gr.Dropdown(label="ابعاد", choices=ASPECT_RATIOS_LIST, value="خودکار (پیش‌فرض)")
 
 
 
 
 
 
688
  with gr.Row(visible=False) as custom_dims_row:
689
- custom_width = gr.Slider(label="عرض", minimum=256, maximum=2048, step=8, value=1024)
690
- custom_height = gr.Slider(label="ارتفاع", minimum=256, maximum=2048, step=8, value=1024)
691
- seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
692
- randomize_seed = gr.Checkbox(label="Seed تصادفی", value=True)
693
- guidance_scale = gr.Slider(label="Guidance Scale", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
694
- steps = gr.Slider(label="Steps", minimum=1, maximum=50, step=1, value=4)
 
 
 
 
 
 
 
695
 
696
- # لاجیک نمایش تنظیمات ابعاد
697
  def toggle_row(choice):
698
- return gr.update(visible=True) if choice == "شخصی‌سازی (Custom)" else gr.update(visible=False)
699
-
700
- aspect_ratio_selection.change(fn=toggle_row, inputs=aspect_ratio_selection, outputs=custom_dims_row)
701
-
702
- # اجرای مدل
703
- run_button.click(
704
- fn=infer,
705
- inputs=[
706
- input_image, prompt, lora_adapter, seed, randomize_seed, guidance_scale, steps,
707
- aspect_ratio_selection, custom_width, custom_height,
708
- user_id_storage, premium_status_storage # ارسال اطلاعات کاربر به تابع infer
709
- ],
710
- outputs=[output_image, seed, status_box],
711
- api_name="predict"
712
  )
713
 
714
- download_button.click(fn=None, inputs=[output_image], outputs=None, js=js_download_func)
715
-
716
- # نمونه‌ها
717
  gr.Examples(
718
  examples=[
719
  ["examples/1.jpg", "تبدیل به انیمه کن.", "تبدیل عکس به انیمه"],
720
- ["examples/5.jpg", "سایه‌ها را حذف کن.", "اصلاح نور و سایه"],
 
721
  ],
722
  inputs=[input_image, prompt, lora_adapter],
723
  outputs=[output_image, seed, status_box],
@@ -726,5 +789,31 @@ with gr.Blocks() as demo:
726
  label="نمونه‌ها"
727
  )
728
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
729
  if __name__ == "__main__":
730
  demo.queue(max_size=30).launch(show_error=True)
 
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
+ import json
14
+ from datetime import datetime
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
 
38
  def is_image_nsfw(image):
39
  if image is None: return False
40
  try:
41
+ results = safety_classifier(image)
42
+ for result in results:
43
+ if result['label'] == 'nsfw' and result['score'] > 0.75:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  return True
 
45
  return False
46
  except Exception as e:
47
  print(f"Safety check error: {e}")
48
+ return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
  # --- بارگذاری مدل اصلی ---
51
  from diffusers import FlowMatchEulerDiscreteScheduler
 
56
  dtype = torch.bfloat16
57
  device = "cuda" if torch.cuda.is_available() else "cpu"
58
 
59
+ print("Loading pipeline...")
60
  pipe = QwenImageEditPlusPipeline.from_pretrained(
61
  "Qwen/Qwen-Image-Edit-2509",
62
  transformer=QwenImageTransformer2DModel.from_pretrained(
 
108
  "شخصی‌سازی (Custom)": "Custom"
109
  }
110
 
111
+ BANNED_WORDS = [
112
+ "nude", "naked", "sex", "porn", "undressed", "nsfw", "erotic", "xxx",
113
+ "breast", "nipple", "genital", "vagina", "penis", "ass", "butt", "sexual",
114
+ "lingerie", "bikini", "swimwear", "underwear", "fetish", "topless",
115
+ "exhibitionism", "hentai", "ecchi", "18+"
116
+ ]
117
+
118
+ def check_text_safety(text):
119
+ text_lower = text.lower()
120
+ for word in BANNED_WORDS:
121
+ if f" {word} " in f" {text_lower} ":
122
+ return False
123
+ return True
124
+
125
+ def translate_prompt(text):
126
+ if not text:
127
+ return ""
128
+ try:
129
+ translated = GoogleTranslator(source='auto', target='en').translate(text)
130
+ return translated
131
+ except Exception as e:
132
+ print(f"Translation Error: {e}")
133
+ return text
134
+
135
  def update_dimensions_on_upload(image):
136
  if image is None:
137
  return 1024, 1024
 
148
  new_height = (new_height // 8) * 8
149
  return new_width, new_height
150
 
151
+ def update_sliders_visibility(choice):
152
+ if choice == "شخصی‌سازی (Custom)":
153
+ return gr.update(visible=True), gr.update(visible=True)
154
+ else:
155
+ return gr.update(visible=False), gr.update(visible=False)
156
+
157
  def get_error_html(message):
158
  return f"""
159
  <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;">
 
170
  </div>
171
  """
172
 
 
 
 
 
 
 
 
 
 
 
173
  @spaces.GPU(duration=30)
174
  def infer(
175
  input_image,
 
182
  aspect_ratio_selection,
183
  custom_width,
184
  custom_height,
 
 
185
  progress=gr.Progress(track_tqdm=True)
186
  ):
 
 
 
 
 
 
 
 
187
  if input_image is None:
188
  return None, seed, get_error_html("لطفاً ابتدا یک تصویر بارگذاری کنید.")
189
 
 
190
  if is_image_nsfw(input_image):
191
  return None, seed, get_error_html("تصویر ورودی دارای محتوای نامناسب است و پردازش نمی‌شود.")
192
 
 
193
  english_prompt = translate_prompt(prompt)
194
  if not check_text_safety(english_prompt):
195
  return None, seed, get_error_html("متن درخواست شامل کلمات غیرمجاز یا غیراخلاقی است.")
 
203
 
204
  generator = torch.Generator(device=device).manual_seed(seed)
205
 
206
+ safety_negative = "nsfw, nude, naked, porn, sexual, xxx, breast, nipple, genital, vagina, penis, ass, lingerie, bikini, swimwear, underwear, fetish, topless, gore, violence, blood"
 
 
 
 
 
 
207
  base_negative = "worst quality, low quality, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, jpeg artifacts, signature, watermark, username, blurry"
208
  final_negative_prompt = f"{safety_negative}, {base_negative}"
209
 
 
231
  true_cfg_scale=guidance_scale,
232
  ).images[0]
233
 
 
234
  if is_image_nsfw(result):
235
  return None, seed, get_error_html("تصویر تولید شده حاوی محتوای نامناسب بود و حذف شد.")
236
 
 
 
 
237
  return result, seed, get_success_html("تصویر با موفقیت ویرایش شد.")
238
 
239
  except Exception as e:
 
244
 
245
  @spaces.GPU(duration=30)
246
  def infer_example(input_image, prompt, lora_adapter):
247
+ res, s, status = infer(input_image, prompt, lora_adapter, 0, True, 1.0, 4, "خودکار (پیش‌فرض)", 1024, 1024)
 
248
  return res, s, status
249
 
250
  # --- جاوااسکریپت برای دکمه دانلود ---
 
267
  }
268
  """
269
 
270
+ # --- جاوااسکریپت مدیریت کاربر (رایگان/پولی) و محدودیت‌ها ---
271
+ js_user_auth_logic = """
272
  <script>
273
+ // --- تنظیمات پریمیوم ---
274
+ const PREMIUM_PAGE_ID = '1149636';
275
+ const PREMIUM_URL = '#/nav/online/news/getSingle/1149636/eyJpdiI6InZSVUdlLzBlR0FzOHZJdXFZeWhER0E9PSIsInZhbHVlIjoiWFhqRXBLc29vSFpHdk9nYmRjZGVuWHRHRHVSZHRlTG1BUENLaE5mNXBNVVRGWFg3ZWN0djJ5K1dIY1RqTHJGaCIsIm1hYyI6IjIzYzFlZTMwYmVmMTdkYjQ0YTQ4YWMxNmFhN2RmNWQ2OTc1NDIyNGVlZGI3ZjJjMjhkNmQxNjM4MDFlZTIxNmUiLCJ0YWciOiIifQ==/20934991';
276
+ const DAILY_LIMIT = 5;
277
+
278
+ // --- متغیرهای وضعیت ---
279
+ window.userSubscriptionStatus = 'free'; // پیش‌فرض رایگان
280
+ window.userFingerprint = null;
281
+
282
+ // --- ایجاد فینگرپرینت ---
283
+ async function getBrowserFingerprint() {
284
+ const components = [navigator.userAgent, navigator.language, screen.width + 'x' + screen.height, new Date().getTimezoneOffset()];
285
+ try {
286
+ const canvas = document.createElement('canvas');
287
+ const ctx = canvas.getContext('2d');
288
+ ctx.textBaseline = "top";
289
+ ctx.font = "14px 'Arial'";
290
+ ctx.textBaseline = "alphabetic";
291
+ ctx.fillStyle = "#f60";
292
+ ctx.fillRect(125, 1, 62, 20);
293
+ ctx.fillStyle = "#069";
294
+ ctx.fillText("a1b2c3d4e5f6g7h8i9j0_!@#$%^&*()", 2, 15);
295
+ components.push(canvas.toDataURL());
296
+ } catch (e) {
297
+ components.push("canvas-error");
298
+ }
299
+ const fingerprintString = components.join('~~~');
300
+ let hash = 0;
301
+ for (let i = 0; i < fingerprintString.length; i++) {
302
+ const char = fingerprintString.charCodeAt(i);
303
+ hash = ((hash << 5) - hash) + char;
304
+ hash |= 0;
305
+ }
306
+ return 'fp_' + Math.abs(hash).toString(16);
307
+ }
308
 
309
+ // --- بررسی اینکه آیا کاربر پولی است ---
310
+ function isUserPaid(userObject) {
311
+ return userObject && userObject.isLogin && userObject.accessible_pages &&
312
+ (userObject.accessible_pages.includes(PREMIUM_PAGE_ID) || userObject.accessible_pages.includes(parseInt(PREMIUM_PAGE_ID)));
313
+ }
314
 
315
+ // --- به‌روزرسانی UI بر اساس وضعیت کاربر ---
316
+ function updateUIForSubscriptionStatus(status) {
317
+ window.userSubscriptionStatus = status;
318
+
319
+ const badge = document.getElementById('subscription-status-badge');
320
+ const creditInfo = document.getElementById('credit-info');
321
+ const runBtn = document.getElementById('run-btn');
322
+ const upgradeBtn = document.getElementById('upgrade-premium-btn');
323
+
324
+ if (status === 'paid') {
325
+ if(badge) {
326
+ badge.textContent = '⭐️ نسخه نامحدود';
327
+ badge.className = 'paid-badge';
328
+ badge.style.display = 'inline-block';
329
+ }
330
+ if(creditInfo) creditInfo.style.display = 'none';
331
+
332
+ // کاربر پولی همیشه دکمه فعال دارد
333
+ if(runBtn) runBtn.disabled = false;
334
+ if(upgradeBtn) upgradeBtn.style.display = 'none';
335
+
336
+ } else {
337
+ if(badge) {
338
+ badge.textContent = 'نسخه رایگان';
339
+ badge.className = 'free-badge';
340
+ badge.style.display = 'inline-block';
341
+ }
342
+ // بررسی اعتبار برای کاربر رایگان
343
+ checkFreeUserCredit();
344
+ }
345
+ }
346
 
347
+ // --- مدیریت اعتبار کاربر رایگان (ذخیره در LocalStorage) ---
348
+ function checkFreeUserCredit() {
349
+ if (window.userSubscriptionStatus === 'paid') return;
350
+
351
+ const today = new Date().toLocaleDateString();
352
+ const storageKey = 'alpha_edit_usage';
353
+ let data = JSON.parse(localStorage.getItem(storageKey) || '{}');
354
+
355
+ // اگر تاریخ عوض شده، ریست کن
356
+ if (data.date !== today) {
357
+ data = { date: today, count: 0 };
358
+ localStorage.setItem(storageKey, JSON.stringify(data));
359
+ }
360
+
361
+ const used = data.count;
362
+ const remaining = DAILY_LIMIT - used;
363
+
364
+ const creditInfo = document.getElementById('credit-info');
365
+ if(creditInfo) {
366
+ creditInfo.style.display = 'block';
367
+ if (remaining > 0) {
368
+ creditInfo.innerHTML = `اعتبار امروز: <b>${remaining}</b> تصویر دیگر`;
369
+ creditInfo.style.color = '#4b5563';
370
  } else {
371
+ creditInfo.innerHTML = `<span style="color:#ef4444">اعتبار امروز شما تمام شده است.</span>`;
 
372
  }
 
373
  }
374
+
375
+ const runBtn = document.getElementById('run-btn');
376
+ const upgradeBtn = document.getElementById('upgrade-premium-btn');
377
+
378
+ if (used >= DAILY_LIMIT) {
379
+ if(runBtn) {
380
+ runBtn.disabled = true;
381
+ runBtn.innerText = "⛔ اعتبار روزانه تمام شد";
382
+ runBtn.style.opacity = "0.6";
383
+ runBtn.style.cursor = "not-allowed";
384
+ }
385
+ if(upgradeBtn) {
386
+ upgradeBtn.style.display = 'block'; // نمایش دکمه ارتقا
387
  }
388
+ } else {
389
+ if(runBtn) {
390
+ runBtn.disabled = false;
391
+ runBtn.innerText = "✨ شروع پردازش و ساخت تصویر";
392
+ runBtn.style.opacity = "1";
393
+ runBtn.style.cursor = "pointer";
394
  }
395
+ if(upgradeBtn) upgradeBtn.style.display = 'none';
396
  }
397
+ }
398
+
399
+ // --- تابع کسر اعتبار (قبل از اجرای پایتون صدا زده می‌شود) ---
400
+ async function deductCreditAndCheck() {
401
+ if (window.userSubscriptionStatus === 'paid') return true;
402
+
403
+ const today = new Date().toLocaleDateString();
404
+ const storageKey = 'alpha_edit_usage';
405
+ let data = JSON.parse(localStorage.getItem(storageKey) || '{}');
406
+
407
+ if (data.date !== today) {
408
+ data = { date: today, count: 0 };
409
+ }
410
+
411
+ if (data.count >= DAILY_LIMIT) {
412
+ alert("اعتبار روزانه شما تمام شده است. لطفاً نسخه نامحدود را تهیه کنید.");
413
+ throw new Error("Credit limit reached"); // توقف اجرا
414
+ }
415
+
416
+ // افزایش کنتور
417
+ data.count++;
418
+ localStorage.setItem(storageKey, JSON.stringify(data));
419
+ checkFreeUserCredit(); // بروزرسانی UI
420
+ return true;
421
+ }
422
+
423
+ // --- هندلر ارتقا ---
424
+ window.navigateToPremium = function() {
425
+ parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM', payload: { url: PREMIUM_URL } }, '*');
426
+ }
427
+
428
+ // --- شروع و لیسنرها ---
429
+ document.addEventListener('DOMContentLoaded', async () => {
430
+ window.userFingerprint = await getBrowserFingerprint();
431
+
432
+ // درخواست وضعیت از اندروید
433
+ parent.postMessage({ type: 'REQUEST_USER_STATUS' }, '*');
434
+
435
+ window.addEventListener('message', (event) => {
436
+ if (event.data && event.data.type === 'USER_STATUS_RESPONSE') {
437
+ if (event.data.error || !event.data.payload) {
438
+ updateUIForSubscriptionStatus('free');
439
+ return;
440
+ }
441
+ try {
442
+ const userObject = JSON.parse(event.data.payload);
443
+ const status = isUserPaid(userObject) ? 'paid' : 'free';
444
+ updateUIForSubscriptionStatus(status);
445
+ } catch (e) {
446
+ updateUIForSubscriptionStatus('free');
447
+ }
448
+ }
449
+ });
450
 
451
+ // بررسی اولیه
452
+ setTimeout(() => {
453
+ if(window.userSubscriptionStatus === 'free') checkFreeUserCredit();
454
+ }, 1000);
455
+ });
456
+ </script>
457
+ """
458
+
459
+ # --- CSS Updated (With Badges & Upgrade Button) ---
460
+ css_code = """
461
+ <style>
462
+ @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700;800&display=swap');
463
+
464
+ :root, .dark, body, .gradio-container {
465
+ --body-background-fill: #f5f7fa !important;
466
+ --body-text-color: #1f2937 !important;
467
+ font-family: 'Vazirmatn', sans-serif !important;
468
+ }
469
+
470
+ /* Badges */
471
+ #subscription-status-badge {
472
+ display: none; /* توسط JS کنترل می‌شود */
473
+ padding: 6px 16px;
474
+ border-radius: 20px;
475
+ font-size: 0.9em;
476
+ font-weight: 700;
477
+ margin-top: 10px;
478
+ letter-spacing: 0.5px;
479
+ text-shadow: 0 1px 2px rgba(0,0,0,0.1);
480
+ animation: fadeIn 0.5s ease;
481
+ }
482
+
483
+ .free-badge {
484
+ background: linear-gradient(45deg, #6c757d, #495057);
485
+ color: white;
486
+ box-shadow: 0 4px 10px rgba(108, 117, 125, 0.3);
487
+ }
488
+
489
+ .paid-badge {
490
+ background: linear-gradient(45deg, #FFC107, #ffca2c);
491
+ color: #333;
492
+ box-shadow: 0 4px 10px rgba(255, 193, 7, 0.4);
493
+ }
494
+
495
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
496
+
497
+ /* Upgrade Button */
498
+ @keyframes upgrade-pulse {
499
+ 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 193, 7, 0.7); }
500
+ 70% { transform: scale(1.02); box-shadow: 0 0 0 10px rgba(255, 193, 7, 0); }
501
+ 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 193, 7, 0); }
502
+ }
503
+
504
+ #upgrade-premium-btn {
505
+ background: linear-gradient(95deg, #FFD54F, #FFC107 100%) !important;
506
+ color: #212529 !important;
507
+ font-weight: 800 !important;
508
+ border: none !important;
509
+ font-size: 1.1em !important;
510
+ padding: 12px !important;
511
+ border-radius: 12px !important;
512
+ margin-top: 15px !important;
513
+ width: 100%;
514
+ animation: upgrade-pulse 2s infinite;
515
+ cursor: pointer !important;
516
+ display: none; /* پیش‌فرض مخفی */
517
+ }
518
+ #upgrade-premium-btn:hover {
519
+ transform: translateY(-2px);
520
+ box-shadow: 0 10px 20px -5px rgba(255, 193, 7, 0.5);
521
+ animation: none;
522
+ }
523
+
524
+ /* Other Styles */
525
+ #col-container {
526
+ margin: 0 auto;
527
+ max-width: 980px;
528
+ direction: rtl;
529
+ text-align: right;
530
+ padding: 30px;
531
+ background: #ffffff !important;
532
+ border-radius: 24px;
533
+ box-shadow: 0 10px 40px -10px rgba(0,0,0,0.08);
534
+ border: 1px solid rgba(255,255,255,0.8);
535
+ }
536
+
537
+ #main-title h1 {
538
+ font-size: 2.4em !important;
539
+ text-align: center;
540
+ color: #1a202c !important;
541
+ margin-bottom: 5px;
542
+ font-weight: 800;
543
+ background: -webkit-linear-gradient(45deg, #2563eb, #1e40af);
544
+ -webkit-background-clip: text;
545
+ -webkit-text-fill-color: transparent;
546
+ }
547
+
548
+ #main-description {
549
+ text-align: center;
550
+ font-size: 1.15em;
551
+ color: #4b5563 !important;
552
+ margin-bottom: 10px;
553
+ line-height: 1.6;
554
+ }
555
+
556
+ /* Center badge container */
557
+ .badge-container {
558
+ display: flex;
559
+ flex-direction: column;
560
+ align-items: center;
561
+ justify-content: center;
562
+ margin-bottom: 30px;
563
+ }
564
+
565
+ textarea, input[type="text"] {
566
+ border: 2px solid #e2e8f0 !important;
567
+ border-radius: 12px !important;
568
+ background-color: #ffffff !important;
569
+ color: #111827 !important;
570
+ padding: 12px !important;
571
+ font-family: 'Vazirmatn', sans-serif !important;
572
+ }
573
+
574
+ .primary-btn, button.primary {
575
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important;
576
+ border: none !important;
577
+ color: white !important;
578
+ font-weight: 700 !important;
579
+ font-size: 1.1em !important;
580
+ padding: 14px 28px !important;
581
+ border-radius: 14px !important;
582
+ box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3) !important;
583
+ width: 100%;
584
+ }
585
+
586
+ .primary-btn:disabled {
587
+ background: #9ca3af !important;
588
+ box-shadow: none !important;
589
+ cursor: not-allowed !important;
590
+ }
591
+
592
+ #download-btn {
593
+ background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
594
+ }
595
 
596
+ footer { display: none !important; }
597
 
598
+ /* IP Guide Styles (Keep existing) */
599
+ .ip-reset-guide-container {
600
+ text-align: right; direction: rtl; background: rgba(255, 255, 255, 0.98);
601
+ backdrop-filter: blur(10px); padding: 20px; border-radius: 16px;
602
+ box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1); border: 1px solid rgba(100,100,200,0.2);
603
+ width: 90%; max-width: 420px; font-family: 'Vazirmatn', sans-serif;
604
+ }
605
+ .guide-header { display: flex; align-items: center; margin-bottom: 15px; }
606
+ .guide-header h2 { font-size: 1.2rem; margin: 0; color: #2d3748; }
607
+ .elegant-video-button {
608
+ display: inline-flex; align-items: center; justify-content: center; padding: 10px 20px;
609
+ background: white; color: #667eea; border: 1px solid #e2e8f0; border-radius: 50px; cursor: pointer;
610
+ font-family: inherit; font-weight: 600; margin-top: 10px;
611
+ }
612
+ .action-button {
613
+ padding: 10px; border-radius: 12px; border: none; cursor: pointer; flex: 1; font-family: inherit; font-weight: 600;
614
+ }
615
+ .retry-button { background: linear-gradient(135deg, #667eea, #764ba2); color: white; }
616
+ .back-button { background: white; border: 1px solid #e2e8f0; }
617
+ </style>
618
+ """
619
+
620
+ # جاوااسکریپت سراسری (شامل راهنمای IP و استایل‌ها)
621
+ # توجه: ما کدهای جدید Auth را هم به این بخش اضافه می‌کنیم تا یکجا اینجکت شود
622
+ js_global_combined = js_user_auth_logic + """
623
+ <script>
624
+ document.addEventListener('DOMContentLoaded', () => {
625
+ // 1. Force Light Mode
626
  const forceLight = () => {
627
  const body = document.querySelector('body');
628
  if (body) {
 
630
  body.style.backgroundColor = '#f5f7fa';
631
  body.style.color = '#333333';
632
  }
 
633
  };
634
  forceLight();
635
  setInterval(forceLight, 1000);
636
 
637
+ // 2. IP Reset Guide Functions
 
 
638
  window.retryGeneration = function() {
639
  const modal = document.getElementById('custom-quota-modal');
640
  if (modal) modal.remove();
 
647
  if (modal) modal.remove();
648
  };
649
 
 
 
 
650
  const showQuotaModal = () => {
651
  if (document.getElementById('custom-quota-modal')) return;
 
652
  const modalHtml = `
653
  <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;">
654
  <div class="ip-reset-guide-container">
655
  <div class="guide-header">
 
 
 
 
 
 
 
 
656
  <div>
657
  <h2>یک قدم تا ساخت تصاویر جدید</h2>
658
  <p>نیازمند تغییر نقطه دستیابی</p>
659
  </div>
660
  </div>
661
  <div class="guide-content">
662
+ <p>برای استفاده نامحدود، اینترنت را خاموش/روشن کنید یا IP را تغییر دهید.</p>
663
+ <div style="display:flex; gap:10px; margin-top:20px;">
664
+ <button class="action-button back-button" onclick="window.closeErrorModal()">بازگشت</button>
665
+ <button class="action-button retry-button" onclick="window.retryGeneration()">تلاش مجدد</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
666
  </div>
667
  </div>
 
 
 
 
 
 
 
 
 
 
668
  </div>
669
  </div>
670
  `;
671
  document.body.insertAdjacentHTML('beforeend', modalHtml);
672
+ setTimeout(() => window.closeErrorModal(), 10000);
673
  };
674
 
675
+ // 3. Scanner for Quota Errors
 
 
676
  setInterval(() => {
677
+ const potentialErrors = document.querySelectorAll('.toast-body, .error, div[class*="error"]');
678
  potentialErrors.forEach(el => {
679
  const text = el.innerText || "";
680
  if (text.toLowerCase().includes('quota') || text.toLowerCase().includes('exceeded')) {
681
  showQuotaModal();
682
  el.style.display = 'none';
 
 
 
 
683
  }
684
  });
685
  }, 100);
 
687
  </script>
688
  """
689
 
690
+ # ادغام نهایی
691
+ combined_html = css_code + js_global_combined
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
692
 
693
+ # استفاده از gr.Blocks
694
  with gr.Blocks() as demo:
695
+ # تزریق کدها به عنوان HTML
696
  gr.HTML(combined_html)
697
 
698
  with gr.Column(elem_id="col-container"):
699
  gr.Markdown("# **ویرایشگر هوشمند آلفا**", elem_id="main-title")
700
+ gr.Markdown(
701
+ "با هوش مصنوعی آلفا تصاویر تونو به مدل های مختلف ویرایش کنید.",
702
+ elem_id="main-description"
703
+ )
704
 
705
+ # --- بخش جدید: بج و اطلاعات اعتبار ---
706
+ gr.HTML("""
707
+ <div class="badge-container">
708
+ <div id="subscription-status-badge"></div>
709
+ <div id="credit-info" style="font-size: 0.9em; margin-top: 8px; color: #666; display:none;"></div>
710
+ </div>
711
+ """)
712
 
713
  with gr.Row(equal_height=True):
714
  with gr.Column():
715
  input_image = gr.Image(label="بارگذاری تصویر", type="pil", height=320)
716
+
717
+ prompt = gr.Text(
718
+ label="دستور ویرایش (به فارسی)",
719
+ show_label=True,
720
+ placeholder="مثال: تصویر را به سبک انیمه تبدیل کن...",
721
+ rtl=True,
722
+ lines=3
723
+ )
724
+
725
  status_box = gr.HTML(label="وضعیت")
726
+
727
+ # دکمه اجرا (با شناسه برای JS)
728
+ run_button = gr.Button("✨ شروع پردازش و ساخت تصویر", variant="primary", elem_classes="primary-btn", elem_id="run-btn")
729
+
730
+ # دکمه ارتقا (مخفی، توسط JS کنترل می‌شود)
731
+ upgrade_button = gr.Button("⭐️ ارتقا به نسخه کامل و نامحدود", visible=True, elem_id="upgrade-premium-btn")
732
 
733
  with gr.Column():
734
  output_image = gr.Image(label="تصویر نهایی", interactive=False, format="png", height=380)
735
+
736
  download_button = gr.Button("📥 دانلود و ذخیره تصویر", variant="secondary", elem_id="download-btn", elem_classes="primary-btn")
737
 
738
  with gr.Row():
739
+ lora_adapter = gr.Dropdown(
740
+ label="انتخاب سبک ویرایش (LoRA)",
741
+ choices=list(LORA_MAPPING.keys()),
742
+ value="تبدیل عکس به انیمه"
743
+ )
744
 
745
+ with gr.Accordion("تنظیمات پیشرفته", open=False, visible=True):
746
+ aspect_ratio_selection = gr.Dropdown(
747
+ label="ابعاد تصویر خروجی",
748
+ choices=ASPECT_RATIOS_LIST,
749
+ value="خودکار (پیش‌فرض)",
750
+ interactive=True
751
+ )
752
+
753
  with gr.Row(visible=False) as custom_dims_row:
754
+ custom_width = gr.Slider(
755
+ label="عرض دلخواه (Width)",
756
+ minimum=256, maximum=2048, step=8, value=1024
757
+ )
758
+ custom_height = gr.Slider(
759
+ label="ارتفاع دلخواه (Height)",
760
+ minimum=256, maximum=2048, step=8, value=1024
761
+ )
762
+
763
+ seed = gr.Slider(label="دانه تصادفی (Seed)", minimum=0, maximum=MAX_SEED, step=1, value=0)
764
+ randomize_seed = gr.Checkbox(label="استفاده از Seed تصادفی", value=True)
765
+ guidance_scale = gr.Slider(label="میزان وفاداری به متن (Guidance Scale)", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
766
+ steps = gr.Slider(label="تعداد مراحل پردازش (Steps)", minimum=1, maximum=50, step=1, value=4)
767
 
 
768
  def toggle_row(choice):
769
+ if choice == "شخصی‌سازی (Custom)":
770
+ return gr.update(visible=True)
771
+ return gr.update(visible=False)
772
+
773
+ aspect_ratio_selection.change(
774
+ fn=toggle_row,
775
+ inputs=aspect_ratio_selection,
776
+ outputs=custom_dims_row
 
 
 
 
 
 
777
  )
778
 
 
 
 
779
  gr.Examples(
780
  examples=[
781
  ["examples/1.jpg", "تبدیل به انیمه کن.", "تبدیل عکس به انیمه"],
782
+ ["examples/5.jpg", "سایه‌ها را حذف کن و نورپردازی نرم به تصویر بده.", "اصلاح نور و سایه"],
783
+ ["examples/4.jpg", "از فیلتر ساعت طلایی با پخش نور ملایم استفاده کن.", "نورپردازی مجدد (Relight)"],
784
  ],
785
  inputs=[input_image, prompt, lora_adapter],
786
  outputs=[output_image, seed, status_box],
 
789
  label="نمونه‌ها"
790
  )
791
 
792
+ # --- اتصال رویدادها ---
793
+
794
+ # دکمه اجرا: اول JS اجرا می‌شود (کسر اعتبار)، اگر موفق بود پایتون اجرا می‌شود
795
+ run_button.click(
796
+ fn=infer,
797
+ inputs=[input_image, prompt, lora_adapter, seed, randomize_seed, guidance_scale, steps, aspect_ratio_selection, custom_width, custom_height],
798
+ outputs=[output_image, seed, status_box],
799
+ api_name="predict",
800
+ js="deductCreditAndCheck" # این تابع JS قبل از پایتون اجرا می‌شود
801
+ )
802
+
803
+ # دکمه ارتقا: فقط JS اجرا می‌شود (پیام به اندروید)
804
+ upgrade_button.click(
805
+ fn=None,
806
+ inputs=None,
807
+ outputs=None,
808
+ js="navigateToPremium"
809
+ )
810
+
811
+ download_button.click(
812
+ fn=None,
813
+ inputs=[output_image],
814
+ outputs=None,
815
+ js=js_download_func
816
+ )
817
+
818
  if __name__ == "__main__":
819
  demo.queue(max_size=30).launch(show_error=True)