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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +503 -385
app.py CHANGED
@@ -4,6 +4,7 @@ import numpy as np
4
  import spaces
5
  import torch
6
  import random
 
7
  import json
8
  from datetime import datetime
9
  from PIL import Image, ImageFilter
@@ -13,7 +14,72 @@ from gradio.themes.utils import colors, fonts, sizes
13
  from deep_translator import GoogleTranslator
14
  from transformers import pipeline
15
 
16
- # --- تنظیمات تم و رنگ ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  colors.steel_blue = colors.Color(
18
  name="steel_blue",
19
  c50="#EBF3F8",
@@ -31,57 +97,76 @@ colors.steel_blue = colors.Color(
31
 
32
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
33
 
34
- # --- دیتابیس موقت حافظه برای کاربران رایگان ---
35
- # ساختار: { 'fingerprint_hash': {'date': '2023-10-27', 'count': 0} }
36
- USER_CREDITS_DB = {}
37
- MAX_FREE_DAILY_CREDITS = 5
38
- PREMIUM_PAGE_ID = '1149636'
39
 
40
- # --- مدل تشخیص محتوای نامناسب (NSFW) ---
41
- print("Loading Safety Checker...")
42
- safety_classifier = pipeline("image-classification", model="Falconsai/nsfw_image_detection", device=-1)
 
 
43
 
44
  def is_image_nsfw(image):
45
  if image is None: return False
46
  try:
47
- results = safety_classifier(image)
48
- for result in results:
49
- if result['label'] == 'nsfw' and result['score'] > 0.75:
 
 
50
  return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  return False
52
  except Exception as e:
53
  print(f"Safety check error: {e}")
54
- return False
55
 
56
- # --- لیست کلمات ممنوعه (انگلیسی) ---
57
  BANNED_WORDS = [
58
  "nsfw", "nude", "naked", "sex", "porn", "erotic", "xxx", "18+", "uncensored",
59
- "breast", "nipple", "genital", "vagina", "penis", "ass", "butt", "buttocks",
60
- "lingerie", "bikini", "swimwear", "underwear", "panties", "bra", "topless",
61
- "fetish", "exhibitionism", "hentai", "ecchi", "ahegao", "futanari", "yuri", "yaoi",
62
- "sexual", "intercourse", "masturbation", "orgasm", "cum", "semen", "sperm",
63
- "blowjob", "handjob", "tit", "boob", "dick", "cock", "pussy", "cunt", "whore",
64
- "slut", "prostitute", "stripper", "bondage", "bdsm", "slave", "dominatrix",
65
- "rape", "incest", "pedophile", "lolicon", "shotacon", "bestiality", "zoophilia",
66
- "gore", "blood", "kill", "murder", "suicide", "torture", "violence", "horror",
67
- "dead", "corpse", "mutilation", "flesh", "viscera", "guts", "decapitation",
68
- "milf", "dilf", "threesome", "orgy", "gangbang", "creampie", "deepthroat",
69
- "upskirt", "boudoir", "sensual", "seductive", "horny", "lust", "desire",
70
- "groin", "pubic", "rectum", "anus", "clitoris", "scrotum", "testicle",
71
- "penetration", "dildo", "vibrator", "toy", "kink", "submission", "domination"
72
  ]
73
 
74
  def check_text_safety(text):
75
  if not text: return True
76
  text_lower = text.lower()
77
  for word in BANNED_WORDS:
78
- if f" {word} " in f" {text_lower} ":
79
- return False
80
- if word in ["porn", "hentai", "xxx", "nude", "sex"] and word in text_lower:
81
  return False
82
  return True
83
 
84
- # --- بارگذاری مدل Qwen ---
 
 
 
 
 
 
 
 
 
 
85
  from diffusers import FlowMatchEulerDiscreteScheduler
86
  from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
87
  from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
@@ -90,7 +175,7 @@ from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
90
  dtype = torch.bfloat16
91
  device = "cuda" if torch.cuda.is_available() else "cpu"
92
 
93
- print("Loading pipeline...")
94
  pipe = QwenImageEditPlusPipeline.from_pretrained(
95
  "Qwen/Qwen-Image-Edit-2509",
96
  transformer=QwenImageTransformer2DModel.from_pretrained(
@@ -102,7 +187,7 @@ pipe = QwenImageEditPlusPipeline.from_pretrained(
102
  torch_dtype=dtype
103
  ).to(device)
104
 
105
- # LoRA Loading
106
  pipe.load_lora_weights("autoweeb/Qwen-Image-Edit-2509-Photo-to-Anime", weight_name="Qwen-Image-Edit-2509-Photo-to-Anime_000001000.safetensors", adapter_name="anime")
107
  pipe.load_lora_weights("dx8152/Qwen-Edit-2509-Multiple-angles", weight_name="镜头转换.safetensors", adapter_name="multiple-angles")
108
  pipe.load_lora_weights("dx8152/Qwen-Image-Edit-2509-Light_restoration", weight_name="移除光影.safetensors", adapter_name="light-restoration")
@@ -126,6 +211,14 @@ LORA_MAPPING = {
126
  "افزایش کیفیت (Upscale)": "upscale-image"
127
  }
128
 
 
 
 
 
 
 
 
 
129
  ASPECT_RATIOS_MAP = {
130
  "خودکار (پیش‌فرض)": "Auto",
131
  "۱:۱ (مربع - 1024x1024)": (1024, 1024),
@@ -134,18 +227,6 @@ ASPECT_RATIOS_MAP = {
134
  "شخصی‌سازی (Custom)": "Custom"
135
  }
136
 
137
- ASPECT_RATIOS_LIST = list(ASPECT_RATIOS_MAP.keys())
138
-
139
- def translate_prompt(text):
140
- if not text:
141
- return ""
142
- try:
143
- translated = GoogleTranslator(source='auto', target='en').translate(text)
144
- return translated
145
- except Exception as e:
146
- print(f"Translation Error: {e}")
147
- return text
148
-
149
  def update_dimensions_on_upload(image):
150
  if image is None:
151
  return 1024, 1024
@@ -158,15 +239,13 @@ def update_dimensions_on_upload(image):
158
  new_height = 1024
159
  aspect_ratio = original_width / original_height
160
  new_width = int(new_height * aspect_ratio)
161
-
162
- # Ensure divisible by 8
163
  new_width = (new_width // 8) * 8
164
  new_height = (new_height // 8) * 8
165
  return new_width, new_height
166
 
167
  def get_error_html(message):
168
  return f"""
169
- <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;">
170
  <span style="font-size: 1.2em;">⛔</span>
171
  {message}
172
  </div>
@@ -174,46 +253,23 @@ def get_error_html(message):
174
 
175
  def get_success_html(message):
176
  return f"""
177
- <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;">
178
  <span style="font-size: 1.2em;">✅</span>
179
  {message}
180
  </div>
181
  """
182
 
183
- # --- مدیریت اعتبار کاربر ---
184
- def check_credits(fingerprint, is_premium):
185
- if is_premium:
186
- return True, "Unlimited"
187
-
188
- if not fingerprint:
189
- return True, "Unknown" # Fallback if fingerprint missing
190
-
191
- today_str = datetime.now().strftime("%Y-%m-%d")
192
-
193
- if fingerprint not in USER_CREDITS_DB:
194
- USER_CREDITS_DB[fingerprint] = {'date': today_str, 'count': 0}
195
-
196
- user_data = USER_CREDITS_DB[fingerprint]
197
-
198
- if user_data['date'] != today_str:
199
- user_data['date'] = today_str
200
- user_data['count'] = 0
201
-
202
- if user_data['count'] >= MAX_FREE_DAILY_CREDITS:
203
- return False, 0
204
-
205
- # محاسبه اعتبار باقی‌مانده بدون کسر (کسر بعد از موفقیت انجام میشه)
206
- remaining = MAX_FREE_DAILY_CREDITS - user_data['count']
207
- return True, remaining
208
-
209
- def deduct_credit(fingerprint, is_premium):
210
- if is_premium or not fingerprint:
211
- return
212
-
213
- if fingerprint in USER_CREDITS_DB:
214
- USER_CREDITS_DB[fingerprint]['count'] += 1
215
 
216
- @spaces.GPU(duration=45)
217
  def infer(
218
  input_image,
219
  prompt,
@@ -225,388 +281,450 @@ def infer(
225
  aspect_ratio_selection,
226
  custom_width,
227
  custom_height,
228
- user_fingerprint,
229
- user_is_premium,
230
  progress=gr.Progress(track_tqdm=True)
231
  ):
232
- # --- رفع خطای NoneType vs Int ---
233
- # اگر مقادیر عددی نال باشند، مقدار پیش‌فرض ست می‌کنیم
234
- seed = seed if seed is not None else 0
235
- steps = steps if steps is not None else 4
236
- guidance_scale = guidance_scale if guidance_scale is not None else 1.0
237
- custom_width = custom_width if custom_width is not None else 1024
238
- custom_height = custom_height if custom_height is not None else 1024
239
 
240
- # تبدیل وضعیت پرمیوم
241
- is_premium = str(user_is_premium).lower() == 'true'
242
 
 
243
  if input_image is None:
244
  return None, seed, get_error_html("لطفاً ابتدا یک تصویر بارگذاری کنید.")
245
 
246
- # 1. بررسی اعتبار
247
- allowed, credit_info = check_credits(user_fingerprint, is_premium)
248
- if not allowed:
249
- msg = "اعتبار رایگان امروز شما (۵ تصویر) به پایان رسیده است."
250
- return None, seed, get_error_html(msg + " <span class='quota-limit-reached' style='display:none'></span>")
251
-
252
- # 2. بررسی تصویر ورودی (NSFW)
253
  if is_image_nsfw(input_image):
254
- return None, seed, get_error_html("تصویر ورودی دارای محتوای نامناسب است.")
255
 
256
- # 3. بررسی متن
257
  english_prompt = translate_prompt(prompt)
258
  if not check_text_safety(english_prompt):
259
- return None, seed, get_error_html("متن درخواست شامل کلمات غیرمجاز است.")
260
 
261
- # تنظیمات مدل
262
- adapter_name = LORA_MAPPING.get(lora_adapter_persian)
263
- if adapter_name:
264
- pipe.set_adapters([adapter_name], adapter_weights=[1.0])
265
 
266
  if randomize_seed:
267
  seed = random.randint(0, MAX_SEED)
268
 
269
  generator = torch.Generator(device=device).manual_seed(seed)
270
 
271
- # ابعاد
272
- dims = ASPECT_RATIOS_MAP.get(aspect_ratio_selection)
273
- if dims == "Custom":
274
- w = (int(custom_width) // 8) * 8
275
- h = (int(custom_height) // 8) * 8
276
- elif dims == "Auto" or dims is None:
277
- w, h = update_dimensions_on_upload(input_image)
 
 
 
 
 
 
 
 
 
 
 
 
278
  else:
279
- w, h = dims
280
 
281
  try:
282
- final_negative = "nsfw, nude, naked, porn, sexual, xxx, breast, nipple, genital, vagina, penis, ass, lingerie, bikini, swimwear, underwear, fetish, topless, gore, violence, blood, bad anatomy, bad hands, missing fingers, blurry"
283
-
284
  result = pipe(
285
- image=input_image.convert("RGB"),
286
  prompt=english_prompt,
287
- negative_prompt=final_negative,
288
- height=h,
289
- width=w,
290
- num_inference_steps=int(steps), # اطمینان از int بودن
291
  generator=generator,
292
- true_cfg_scale=float(guidance_scale), # اطمینان از float بودن
293
  ).images[0]
294
 
295
- # 4. بررسی خروجی (NSFW)
296
  if is_image_nsfw(result):
297
- return None, seed, get_error_html("تصویر خروجی حاوی محتوای نامناسب بود و حذف شد.")
298
 
299
- # کسر اعتبار پس از موفقیت
300
- deduct_credit(user_fingerprint, is_premium)
301
 
302
- # پیام موفقیت
303
- success_msg = "تصویر با موفقیت ساخته شد."
304
- if not is_premium:
305
- # محاسبه مجدد باقیمانده
306
- _, new_rem = check_credits(user_fingerprint, is_premium)
307
- success_msg += f"<br><small>اعتبار باقی‌مانده امروز: {new_rem}</small>"
308
-
309
- return result, seed, get_success_html(success_msg)
310
 
311
  except Exception as e:
312
  error_str = str(e)
313
- if "quota" in error_str.lower(): raise e
314
- return None, seed, get_error_html(f"خطا: {error_str}")
 
315
 
316
- # --- تابع نمونه‌ها (بدون کسر اعتبار) ---
317
  def infer_example(input_image, prompt, lora_adapter):
318
- # کاربر پرمیوم ف��ک برای اجرای نمونه‌ها
319
- return infer(input_image, prompt, lora_adapter, 0, True, 1.0, 4, "خودکار (پیش‌فرض)", 1024, 1024, "test_fp", "true")
320
-
321
- # --- جاوااسکریپت و HTML ---
322
- js_code = """
323
- <script>
324
- // --- تنظیمات ---
325
- const PREMIUM_PAGE_ID = '1149636';
326
- const PREMIUM_URL = '#/nav/online/news/getSingle/1149636/eyJpdiI6InZSVUdlLzBlR0FzOHZJdXFZeWhER0E9PSIsInZhbHVlIjoiWFhqRXBLc29vSFpHdk9nYmRjZGVuWHRHRHVSZHRlTG1BUENLaE5mNXBNVVRGWFg3ZWN0djJ5K1dIY1RqTHJGaCIsIm1hYyI6IjIzYzFlZTMwYmVmMTdkYjQ0YTQ4YWMxNmFhN2RmNWQ2OTc1NDIyNGVlZGI3ZjJjMjhkNmQxNjM4MDFlZTIxNmUiLCJ0YWciOiIifQ==/20934991';
327
-
328
- // --- تولید اثر انگشت ---
329
- function getBrowserFingerprint() {
330
- const components = [navigator.userAgent, navigator.language, screen.width + 'x' + screen.height, new Date().getTimezoneOffset()];
331
- try {
332
- const canvas = document.createElement('canvas');
333
- const ctx = canvas.getContext('2d');
334
- ctx.textBaseline = "top"; ctx.font = "14px 'Arial'"; ctx.textBaseline = "alphabetic";
335
- ctx.fillStyle = "#f60"; ctx.fillRect(125, 1, 62, 20);
336
- ctx.fillStyle = "#069"; ctx.fillText("val", 2, 15);
337
- components.push(canvas.toDataURL());
338
- } catch(e) { components.push("err"); }
339
- const str = components.join('~~~');
340
- let hash = 0;
341
- for (let i = 0; i < str.length; i++) {
342
- hash = ((hash << 5) - hash) + str.charCodeAt(i);
343
- hash |= 0;
344
  }
345
- return 'fp_' + Math.abs(hash).toString(16);
 
 
 
 
 
 
 
 
 
346
  }
 
347
 
348
- let userFingerprint = getBrowserFingerprint();
349
- let isUserPremium = false;
350
-
351
- // --- بروزرسانی UI وضعیت کاربر ---
352
- function updateStatusUI(status) {
353
- const badge = document.getElementById('status-badge-text');
354
- const desc = document.getElementById('status-desc');
355
- const upgradeBox = document.getElementById('upgrade-box');
356
-
357
- if (status === 'paid') {
358
- isUserPremium = true;
359
- if(badge) {
360
- badge.innerText = 'نسخه نامحدود 💎';
361
- badge.className = 'badge paid';
362
- }
363
- if(desc) desc.innerText = 'وضعیت: کاربر ویژه (بدون محدودیت)';
364
- if(upgradeBox) upgradeBox.style.display = 'none';
365
- } else {
366
- isUserPremium = false;
367
- if(badge) {
368
- badge.innerText = 'نسخه رایگان';
369
- badge.className = 'badge free';
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  }
371
- if(desc) desc.innerText = '۵ تصویر رایگان در روز دارید';
372
- }
373
- syncHiddenInputs();
374
- }
375
 
376
- function syncHiddenInputs() {
377
- // پیدا کردن تکست‌باکس‌های مخفی گرادیو و پر کردن آن‌ها
378
- const fpArea = document.querySelector('.hidden-fingerprint textarea');
379
- const premArea = document.querySelector('.hidden-premium textarea');
380
-
381
- if (fpArea) {
382
- fpArea.value = userFingerprint;
383
- fpArea.dispatchEvent(new Event('input', { bubbles: true }));
384
- }
385
- if (premArea) {
386
- premArea.value = isUserPremium ? "true" : "false";
387
- premArea.dispatchEvent(new Event('input', { bubbles: true }));
388
- }
389
- }
390
 
391
- function isUserPaid(userObject) {
392
- return userObject && userObject.isLogin && userObject.accessible_pages &&
393
- (userObject.accessible_pages.includes(PREMIUM_PAGE_ID) || userObject.accessible_pages.includes(parseInt(PREMIUM_PAGE_ID)));
394
- }
395
 
396
- // --- شنود پیام از اپلیکیشن مادر ---
397
- window.addEventListener('message', (event) => {
398
- if (event.data && event.data.type === 'USER_STATUS_RESPONSE') {
399
- try {
400
- if (event.data.error || !event.data.payload) {
401
- updateStatusUI('free');
402
- } else {
403
- const userObj = JSON.parse(event.data.payload);
404
- updateStatusUI(isUserPaid(userObj) ? 'paid' : 'free');
405
- }
406
- } catch (e) { updateStatusUI('free'); }
407
  }
408
- });
409
 
410
- // --- شروع کار ---
411
- document.addEventListener('DOMContentLoaded', () => {
412
- // درخواست وضعیت کاربر به محض لود
413
- parent.postMessage({ type: 'REQUEST_USER_STATUS' }, '*');
 
 
 
 
 
 
 
 
 
 
 
414
 
415
- // Fallback: اگر پاسخی نیامد بعد از 2 ثانیه رایگان فرض کن
416
- setTimeout(() => {
417
- const badge = document.getElementById('status-badge-text');
418
- if (!badge || badge.innerText === '') {
419
- updateStatusUI('free');
 
 
 
 
 
 
 
 
 
420
  }
421
- }, 2500);
422
-
423
- // اسکن برای پیام‌های خطا و لیمیت
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
  setInterval(() => {
425
- // Quota Limit from Python
426
- if (document.querySelector('.quota-limit-reached')) {
427
- document.getElementById('upgrade-box').style.display = 'block';
428
- document.querySelector('.quota-limit-reached').remove();
429
- }
430
-
431
- // Hide default Gradio toasts if needed
432
- document.querySelectorAll('.toast-wrap').forEach(el => {
433
- if (el.innerText.includes('quota') || el.innerText.includes('exceeded')) {
434
- el.style.display = 'none';
435
- showQuotaModal();
436
  }
437
  });
438
- }, 500);
439
  });
440
-
441
- // توابع کمکی
442
- window.openPremium = function() {
443
- parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM', payload: { url: PREMIUM_URL } }, '*');
444
- };
445
-
446
- window.prepareSubmission = function() {
447
- syncHiddenInputs();
448
- return [];
449
- }
450
-
451
- // دانلود
452
- window.downloadImage = async (image) => {
453
- if (!image) return alert("تصویری وجود ندارد!");
454
- let url = image.url;
455
- if (!url.startsWith('http')) url = window.location.origin + url;
456
- window.parent.postMessage({ type: 'DOWNLOAD_REQUEST', url: url }, '*');
457
- };
458
-
459
- // مودال خطای IP
460
- function showQuotaModal() {
461
- if (document.getElementById('custom-quota-modal')) return;
462
- const html = `<div id="custom-quota-modal" style="position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);z-index:99999;display:flex;justify-content:center;align-items:center;color:#fff;font-family:'Vazirmatn';">
463
- <div style="background:#2d3748;padding:2rem;border-radius:1rem;text-align:center;max-width:90%;">
464
- <h3>خطای محدودیت سرور</h3>
465
- <p>لطفاً IP خود را تغییر دهید (VPN را خاموش/روشن کنید) و دوباره تلاش کنید.</p>
466
- <button onclick="document.getElementById('custom-quota-modal').remove();" style="padding:10px 20px;margin-top:10px;border-radius:8px;border:none;background:#4a5568;color:white;cursor:pointer;">باشه</button>
467
- </div>
468
- </div>`;
469
- document.body.insertAdjacentHTML('beforeend', html);
470
- }
471
  </script>
 
472
 
 
 
473
  <style>
474
- @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;700&display=swap');
475
 
476
- body { font-family: 'Vazirmatn', sans-serif !important; }
 
 
 
 
477
 
478
- /* استایل بج و هدر */
479
- .user-info-header {
480
- display: flex;
481
- flex-direction: column;
482
  align-items: center;
483
  justify-content: center;
484
- background: #ffffff;
485
- padding: 15px;
486
- border-radius: 16px;
487
- margin-bottom: 20px;
488
- box-shadow: 0 4px 15px rgba(0,0,0,0.05);
489
- border: 1px solid #e2e8f0;
490
- }
491
-
492
- .badge {
493
- padding: 6px 18px;
494
  border-radius: 50px;
495
- font-size: 0.95rem;
496
- font-weight: 800;
497
- margin-bottom: 8px;
498
- display: inline-block;
 
 
499
  }
500
 
501
- .badge.free {
502
- background: #e2e8f0;
503
- color: #4a5568;
504
- border: 1px solid #cbd5e0;
 
505
  }
506
 
507
- .badge.paid {
508
- background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
509
- color: #744210;
510
- box-shadow: 0 4px 12px rgba(255, 165, 0, 0.3);
511
  }
512
 
513
- #status-desc {
514
- font-size: 0.9rem;
515
- color: #718096;
516
  }
517
 
518
- /* دکمه ارتقا */
519
- #upgrade-box {
520
- width: 100%;
521
- margin-top: 12px;
522
- display: none; /* پیش‌فرض مخفی */
 
 
523
  }
524
-
525
- .upgrade-btn {
526
- width: 100%;
527
- background: linear-gradient(90deg, #F59E0B, #D97706);
528
- color: white;
529
- border: none;
530
- padding: 12px;
531
- border-radius: 12px;
532
- font-weight: 700;
533
- cursor: pointer;
534
- font-family: inherit;
535
- animation: pulse 2s infinite;
536
- }
537
- @keyframes pulse {
538
- 0% { transform: scale(1); }
539
- 50% { transform: scale(1.02); }
540
- 100% { transform: scale(1); }
541
- }
542
-
543
- .hidden-fingerprint, .hidden-premium { display: none !important; }
544
-
545
- /* مخفی کردن فوتر پیشفرض */
546
  footer { display: none !important; }
547
  </style>
548
  """
549
 
550
- with gr.Blocks(theme=Soft()) as demo:
551
- gr.HTML(js_code)
 
 
552
 
553
  with gr.Column(elem_id="col-container"):
554
  gr.Markdown("# **ویرایشگر هوشمند آلفا**", elem_id="main-title")
 
555
 
556
- # بخش نمایش وضعیت کاربر
557
- gr.HTML("""
558
- <div class="user-info-header">
559
- <span id="status-badge-text" class="badge"></span>
560
- <span id="status-desc">در حال بررسی اشتراک...</span>
561
- <div id="upgrade-box">
562
- <button class="upgrade-btn" onclick="openPremium()">⭐️ ارتقا به نسخه نامحدود</button>
563
- </div>
564
- </div>
565
- """)
566
-
567
- # ورودی‌های مخفی برای ارتباط JS -> Python
568
- user_fingerprint = gr.Textbox(elem_classes="hidden-fingerprint", visible=False)
569
- user_is_premium = gr.Textbox(elem_classes="hidden-premium", visible=False)
570
 
571
- with gr.Row():
572
  with gr.Column():
573
- input_image = gr.Image(label="تصویر اصلی", type="pil", height=320)
574
- prompt = gr.Text(label="دستور ویرایش (فارسی)", placeholder="مثلاً: تصویر را کارتونی کن", rtl=True, lines=3)
575
- run_btn = gr.Button("✨ شروع پردازش", variant="primary")
576
-
 
577
  with gr.Column():
578
- output_image = gr.Image(label="نتیجه", interactive=False)
579
- dl_btn = gr.Button("📥 دانلود تصویر")
 
 
 
580
 
581
  with gr.Accordion("تنظیمات پیشرفته", open=False):
582
- lora_adapter = gr.Dropdown(list(LORA_MAPPING.keys()), label="سبک", value="تبدیل عکس به انیمه")
583
- ratio = gr.Dropdown(ASPECT_RATIOS_LIST, label="ابعاد", value="خودکار (پیش‌فرض)")
584
-
585
- with gr.Row(visible=False) as custom_row:
586
- w_slider = gr.Slider(256, 2048, value=1024, step=8, label="عرض")
587
- h_slider = gr.Slider(256, 2048, value=1024, step=8, label="ارتفاع")
588
-
589
- ratio.change(lambda x: gr.update(visible=(x == "شخصی‌سازی (Custom)")), ratio, custom_row)
590
-
591
- seed = gr.Slider(0, MAX_SEED, label="Seed", value=0)
592
- rand_seed = gr.Checkbox(True, label="Seed تصادفی")
593
- cfg = gr.Slider(1.0, 10.0, value=5.0, label="Guidance Scale") # مقدار پیشفرض مطمئن
594
- steps = gr.Slider(1, 50, value=30, label="Steps") # مقدار پیشفرض مطمئن
595
 
596
- status_box = gr.HTML()
597
-
598
- # اتصال دکمه‌ها
599
- run_btn.click(
600
- fn=infer,
601
- inputs=[
602
- input_image, prompt, lora_adapter, seed, rand_seed, cfg, steps,
603
- ratio, w_slider, h_slider, user_fingerprint, user_is_premium
604
- ],
605
- outputs=[output_image, seed, status_box],
606
- js="window.prepareSubmission" # فراخوانی JS قبل از اجرا برای سینک کردن وضعیت
607
- )
608
-
609
- dl_btn.click(fn=None, inputs=[output_image], js="window.downloadImage")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
610
 
611
  if __name__ == "__main__":
612
- demo.queue().launch(show_error=True)
 
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
 
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(
84
  name="steel_blue",
85
  c50="#EBF3F8",
 
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
171
  from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
172
  from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
 
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(
 
187
  torch_dtype=dtype
188
  ).to(device)
189
 
190
+ # بارگذاری LoRA ها
191
  pipe.load_lora_weights("autoweeb/Qwen-Image-Edit-2509-Photo-to-Anime", weight_name="Qwen-Image-Edit-2509-Photo-to-Anime_000001000.safetensors", adapter_name="anime")
192
  pipe.load_lora_weights("dx8152/Qwen-Edit-2509-Multiple-angles", weight_name="镜头转换.safetensors", adapter_name="multiple-angles")
193
  pipe.load_lora_weights("dx8152/Qwen-Image-Edit-2509-Light_restoration", weight_name="移除光影.safetensors", adapter_name="light-restoration")
 
211
  "افزایش کیفیت (Upscale)": "upscale-image"
212
  }
213
 
214
+ ASPECT_RATIOS_LIST = [
215
+ "خودکار (پیش‌فرض)",
216
+ "۱:۱ (مربع - 1024x1024)",
217
+ "۱۶:۹ (افقی - 1344x768)",
218
+ "۹:۱۶ (عمودی - 768x1344)",
219
+ "شخصی‌سازی (Custom)"
220
+ ]
221
+
222
  ASPECT_RATIOS_MAP = {
223
  "خودکار (پیش‌فرض)": "Auto",
224
  "۱:۱ (مربع - 1024x1024)": (1024, 1024),
 
227
  "شخصی‌سازی (Custom)": "Custom"
228
  }
229
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  def update_dimensions_on_upload(image):
231
  if image is None:
232
  return 1024, 1024
 
239
  new_height = 1024
240
  aspect_ratio = original_width / original_height
241
  new_width = int(new_height * aspect_ratio)
 
 
242
  new_width = (new_width // 8) * 8
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;">
249
  <span style="font-size: 1.2em;">⛔</span>
250
  {message}
251
  </div>
 
253
 
254
  def get_success_html(message):
255
  return f"""
256
+ <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;">
257
  <span style="font-size: 1.2em;">✅</span>
258
  {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,
275
  prompt,
 
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("متن درخواست شامل کلمات غیرمجاز یا غیراخلاقی است.")
307
 
308
+ adapter_internal_name = LORA_MAPPING.get(lora_adapter_persian)
309
+ if adapter_internal_name:
310
+ pipe.set_adapters([adapter_internal_name], adapter_weights=[1.0])
 
311
 
312
  if randomize_seed:
313
  seed = random.randint(0, MAX_SEED)
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
+
327
+ original_image = input_image.convert("RGB")
328
+
329
+ selection_value = ASPECT_RATIOS_MAP.get(aspect_ratio_selection)
330
+
331
+ if selection_value == "Custom":
332
+ width = (int(custom_width) // 8) * 8
333
+ height = (int(custom_height) // 8) * 8
334
+ elif selection_value == "Auto" or selection_value is None:
335
+ width, height = update_dimensions_on_upload(original_image)
336
  else:
337
+ width, height = selection_value
338
 
339
  try:
 
 
340
  result = pipe(
341
+ image=original_image,
342
  prompt=english_prompt,
343
+ negative_prompt=final_negative_prompt,
344
+ height=height,
345
+ width=width,
346
+ num_inference_steps=steps,
347
  generator=generator,
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:
361
  error_str = str(e)
362
+ if "quota" in error_str.lower() or "exceeded" in error_str.lower():
363
+ raise e
364
+ return None, seed, get_error_html(f"خطا در پردازش: {error_str}")
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
+ # --- جاوااسکریپت برای دکمه دانلود ---
373
+ js_download_func = """
374
+ async (image) => {
375
+ if (!image) {
376
+ alert("لطفاً ابتدا تصویر را تولید کنید.");
377
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  }
379
+ let fileUrl = image.url;
380
+ if (fileUrl && !fileUrl.startsWith('http')) {
381
+ fileUrl = window.location.origin + fileUrl;
382
+ } else if (!fileUrl && image.path) {
383
+ fileUrl = window.location.origin + "/file=" + image.path;
384
+ }
385
+ window.parent.postMessage({
386
+ type: 'DOWNLOAD_REQUEST',
387
+ url: fileUrl
388
+ }, '*');
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) {
474
+ body.classList.remove('dark');
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();
489
+ const runBtn = document.getElementById('run-btn');
490
+ if(runBtn) runBtn.click();
491
+ };
492
+
493
+ window.closeErrorModal = function() {
494
+ const modal = document.getElementById('custom-quota-modal');
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);
577
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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],
724
+ fn=infer_example,
725
+ cache_examples=False,
726
+ label="نمونه‌ها"
727
+ )
728
 
729
  if __name__ == "__main__":
730
+ demo.queue(max_size=30).launch(show_error=True)