Opera8 commited on
Commit
7742f47
·
verified ·
1 Parent(s): 2f01800

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +354 -534
app.py CHANGED
@@ -4,169 +4,117 @@ 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(
84
- name="steel_blue",
85
- c50="#EBF3F8",
86
- c100="#D3E5F0",
87
- c200="#A8CCE1",
88
- c300="#7DB3D2",
89
- c400="#529AC3",
90
- c500="#4682B4",
91
- c600="#3E72A0",
92
- c700="#36638C",
93
- c800="#2E5378",
94
- c900="#264364",
95
- c950="#1E3450",
96
- )
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
@@ -179,15 +127,11 @@ print("Loading Generation Pipeline...")
179
  pipe = QwenImageEditPlusPipeline.from_pretrained(
180
  "Qwen/Qwen-Image-Edit-2509",
181
  transformer=QwenImageTransformer2DModel.from_pretrained(
182
- "linoyts/Qwen-Image-Edit-Rapid-AIO",
183
- subfolder='transformer',
184
- torch_dtype=dtype,
185
- device_map='cuda'
186
  ),
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")
@@ -201,530 +145,406 @@ pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
201
  MAX_SEED = np.iinfo(np.int32).max
202
 
203
  LORA_MAPPING = {
204
- "تبدیل عکس به انیمه": "anime",
205
- "تغییر زاویه دید": "multiple-angles",
206
- "اصلاح نور و سایه": "light-restoration",
207
- "نورپردازی مجدد (Relight)": "relight",
208
- "نورپردازی چند زاویه‌ای": "multi-angle-lighting",
209
- "روتوش پوست": "edit-skin",
210
- "صحنه بعدی (سینمایی)": "next-scene",
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),
225
- "۱۶:۹ (افقی - 1344x768)": (1344, 768),
226
- "۹:۱۶ (عمودی - 768x1344)": (768, 1344),
227
- "شخصی‌سازی (Custom)": "Custom"
228
  }
229
 
230
  def update_dimensions_on_upload(image):
231
- if image is None:
232
- return 1024, 1024
233
- original_width, original_height = image.size
234
- if original_width > original_height:
235
- new_width = 1024
236
- aspect_ratio = original_height / original_width
237
- new_height = int(new_width * aspect_ratio)
238
- else:
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>
252
- """
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,
276
- lora_adapter_persian,
277
- seed,
278
- randomize_seed,
279
- guidance_scale,
280
- steps,
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)
 
4
  import spaces
5
  import torch
6
  import random
7
+ from PIL import Image
 
 
 
8
  from typing import Iterable
9
+ from gradio.themes.utils import colors
 
10
  from deep_translator import GoogleTranslator
11
  from transformers import pipeline
12
+ import datetime
13
+ import time
14
+ from collections import defaultdict
15
+ import threading
16
 
17
+ # --- تعریف تم (بدون تغییر) ---
18
+ colors.steel_blue = colors.Color(
19
+ name="steel_blue", c50="#EBF3F8", c100="#D3E5F0", c200="#A8CCE1", c300="#7DB3D2", c400="#529AC3", c500="#4682B4", c600="#3E72A0", c700="#36638C", c800="#2E5378", c900="#264364", c950="#1E3450",
20
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
+ # --- سیستم مدیریت اعتبار کاربران (بخش جدید) ---
23
+ USER_DATA = {}
24
+ USER_LOCKS = defaultdict(threading.Lock)
25
+ DAILY_CREDIT_LIMIT = 5
 
 
 
 
26
 
27
+ def check_and_use_credit(fingerprint: str) -> bool:
28
+ """اعتبار کاربر را بررسی و در صورت وجود، یکی کم می‌کند."""
29
+ if not fingerprint:
30
+ return False
31
+ with USER_LOCKS[fingerprint]:
32
+ today = datetime.date.today().isoformat()
33
+ user = USER_DATA.get(fingerprint, {'credits': DAILY_CREDIT_LIMIT, 'last_reset': today})
34
+
35
+ # اگر تاریخ گذشته بود، اعتبار را ریست کن
36
+ if user['last_reset'] != today:
37
+ user['credits'] = DAILY_CREDIT_LIMIT
38
+ user['last_reset'] = today
39
+
40
+ if user['credits'] > 0:
41
+ user['credits'] -= 1
42
+ USER_DATA[fingerprint] = user
43
+ print(f"Credit used for {fingerprint}. Remaining: {user['credits']}")
44
+ return True
45
+ else:
46
+ print(f"No credits left for {fingerprint}.")
47
+ return False
48
 
49
+ def get_user_status_api(fingerprint: str) -> dict:
50
+ """وضعیت فعلی کاربر را برای نمایش در UI برمی‌گرداند."""
51
+ if not fingerprint:
52
+ return {'credits': 0, 'next_reset_timestamp': 0}
53
+
54
+ with USER_LOCKS[fingerprint]:
55
+ today = datetime.date.today()
56
+ user = USER_DATA.get(fingerprint, {'credits': DAILY_CREDIT_LIMIT, 'last_reset': today.isoformat()})
 
 
 
 
 
 
 
 
 
57
 
58
+ if user['last_reset'] != today.isoformat():
59
+ user['credits'] = DAILY_CREDIT_LIMIT
60
+ user['last_reset'] = today.isoformat()
61
+ USER_DATA[fingerprint] = user
62
 
63
+ tomorrow = today + datetime.timedelta(days=1)
64
+ next_reset_timestamp = int(time.mktime(tomorrow.timetuple()))
65
+
66
+ return {'credits': user['credits'], 'next_reset_timestamp': next_reset_timestamp}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
 
68
 
69
+ # --- بارگذاری سیستم امنیتی دوگانه (بدون تغییر) ---
70
  print("Loading Safety Checkers...")
 
 
71
  safety_classifier_1 = pipeline("image-classification", model="Falconsai/nsfw_image_detection", device=-1)
 
 
72
  safety_classifier_2 = pipeline("image-classification", model="AdamCodd/vit-base-nsfw-detector", device=-1)
73
 
74
  def is_image_nsfw(image):
75
  if image is None: return False
76
  try:
 
77
  results1 = safety_classifier_1(image)
78
  for result in results1:
79
  if result['label'] == 'nsfw' and result['score'] > 0.5:
80
  print(f"Safety Check 1 Failed: {result['score']}")
81
  return True
 
 
82
  results2 = safety_classifier_2(image)
83
  for result in results2:
84
+ label, score = result['label'].lower(), result['score']
 
 
85
  if label == 'nsfw' and score > 0.3:
86
  print(f"Safety Check 2 (NSFW) Failed: {score}")
87
  return True
 
88
  if label in ['sexy', 'porn', 'hentai'] and score > 0.4:
89
  print(f"Safety Check 2 (Partial) Failed: {label} - {score}")
90
  return True
 
91
  return False
92
  except Exception as e:
93
  print(f"Safety check error: {e}")
94
  return True
95
 
 
96
  BANNED_WORDS = [
97
+ "nsfw", "nude", "naked", "sex", "porn", "erotic", "xxx", "18+", "uncensored", "breast", "nipple", "areola", "cleavage", "topless", "open chest", "genital", "vagina", "penis", "dick", "cock", "pussy", "ass", "butt", "anus", "lingerie", "bikini", "swimwear", "underwear", "panties", "bra", "fetish", "bdsm", "bondage", "exhibitionism", "voyeur", "hentai", "ecchi", "ahegao", "paizuri", "undressed", "stripping", "naked body", "exposed skin", "sheer", "see-through", "rape", "violence", "blood", "gore", "sexual"
 
 
 
 
 
 
 
98
  ]
99
 
100
  def check_text_safety(text):
101
  if not text: return True
102
  text_lower = text.lower()
103
  for word in BANNED_WORDS:
104
+ if word in text_lower:
105
  print(f"Banned word found: {word}")
106
  return False
107
  return True
108
 
109
  def translate_prompt(text):
110
+ if not text: return ""
 
111
  try:
112
+ return GoogleTranslator(source='auto', target='en').translate(text)
 
113
  except Exception as e:
114
  print(f"Translation Error: {e}")
115
  return text
116
 
117
+ # --- بارگذاری مدل اصلی ویرایش تصویر (بدون تغییر) ---
118
  from diffusers import FlowMatchEulerDiscreteScheduler
119
  from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
120
  from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
 
127
  pipe = QwenImageEditPlusPipeline.from_pretrained(
128
  "Qwen/Qwen-Image-Edit-2509",
129
  transformer=QwenImageTransformer2DModel.from_pretrained(
130
+ "linoyts/Qwen-Image-Edit-Rapid-AIO", subfolder='transformer', torch_dtype=dtype, device_map='cuda'
 
 
 
131
  ),
132
  torch_dtype=dtype
133
  ).to(device)
134
 
 
135
  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")
136
  pipe.load_lora_weights("dx8152/Qwen-Edit-2509-Multiple-angles", weight_name="镜头转换.safetensors", adapter_name="multiple-angles")
137
  pipe.load_lora_weights("dx8152/Qwen-Image-Edit-2509-Light_restoration", weight_name="移除光影.safetensors", adapter_name="light-restoration")
 
145
  MAX_SEED = np.iinfo(np.int32).max
146
 
147
  LORA_MAPPING = {
148
+ "تبدیل عکس به انیمه": "anime", "تغییر زاویه دید": "multiple-angles", "اصلاح نور و سایه": "light-restoration", "نورپردازی مجدد (Relight)": "relight", "نورپردازی چند زاویه‌ای": "multi-angle-lighting", "روتوش پوست": "edit-skin", "صحنه بعدی (سینمایی)": "next-scene", "افزایش کیفیت (Upscale)": "upscale-image"
 
 
 
 
 
 
 
149
  }
 
150
  ASPECT_RATIOS_LIST = [
151
+ "خودکار (پیش‌فرض)", "۱:۱ (مربع - 1024x1024)", "۱۶:۹ (افقی - 1344x768)", "۹:۱۶ (عمودی - 768x1344)", "شخصی‌سازی (Custom)"
 
 
 
 
152
  ]
 
153
  ASPECT_RATIOS_MAP = {
154
+ "خودکار (پیش‌فرض)": "Auto", "۱:۱ (مربع - 1024x1024)": (1024, 1024), "۱۶:۹ (افقی - 1344x768)": (1344, 768), "۹:۱۶ (عمودی - 768x1344)": (768, 1344), "شخصی‌سازی (Custom)": "Custom"
 
 
 
 
155
  }
156
 
157
  def update_dimensions_on_upload(image):
158
+ if image is None: return 1024, 1024
159
+ w, h = image.size
160
+ new_w, new_h = (1024, int(1024 * h / w)) if w > h else (int(1024 * w / h), 1024)
161
+ return (new_w // 8) * 8, (new_h // 8) * 8
 
 
 
 
 
 
 
 
 
 
162
 
163
  def get_error_html(message):
164
+ return f"""<div style="background-color: #fee2e2; border: 1px solid #ef4444; color: #b91c1c; padding: 12px; border-radius: 8px; text-align: center; margin-bottom: 10px; font-weight: bold; display: flex; align-items: center; justify-content: center; gap: 8px;"><span style="font-size: 1.2em;">⛔</span> {message}</div>"""
 
 
 
 
 
165
 
166
  def get_success_html(message):
167
+ return f"""<div style="background-color: #dcfce7; border: 1px solid #22c55e; color: #15803d; padding: 12px; border-radius: 8px; text-align: center; margin-bottom: 10px; font-weight: bold; display: flex; align-items: center; justify-content: center; gap: 8px;"><span style="font-size: 1.2em;">✅</span> {message}</div>"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
169
+ @spaces.GPU(duration=45)
170
  def infer(
171
+ input_image, prompt, lora_adapter_persian, seed, randomize_seed,
172
+ guidance_scale, steps, aspect_ratio_selection, custom_width, custom_height,
173
+ is_paid_user, user_fingerprint, # ورودی‌های جدید برای مدیریت اعتبار
 
 
 
 
 
 
 
 
 
174
  progress=gr.Progress(track_tqdm=True)
175
  ):
176
+ # --- بخش جدید: بررسی اعتبار قبل از هر چیز ---
177
+ if not is_paid_user:
178
+ can_generate = check_and_use_credit(user_fingerprint)
179
+ if not can_generate:
180
+ return None, seed, get_error_html("اعتبار رایگان روزانه شما تمام شده است. اعتبار فردا مجدداً شارژ خواهد شد.")
181
+
182
+ if input_image is None: return None, seed, get_error_html("لطفاً ابتدا یک تصویر بارگذاری کنید.")
183
+ if is_image_nsfw(input_image): return None, seed, get_error_html("تصویر ورودی دارای محتوای نامناسب است.")
184
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  english_prompt = translate_prompt(prompt)
186
+ if not check_text_safety(english_prompt): return None, seed, get_error_html("متن درخواست شامل کلمات غیرمجاز است.")
 
187
 
188
  adapter_internal_name = LORA_MAPPING.get(lora_adapter_persian)
189
+ if adapter_internal_name: pipe.set_adapters([adapter_internal_name], adapter_weights=[1.0])
 
190
 
191
+ if randomize_seed: seed = random.randint(0, MAX_SEED)
 
 
192
  generator = torch.Generator(device=device).manual_seed(seed)
193
 
194
+ safety_negative = "nsfw, nude, naked, porn, sexual, xxx, breast, nipple, areola, genital, vagina, penis, ass, lingerie, bikini, swimwear, underwear, fetish, topless, open chest, revealing clothes, cleavage, see-through, sheer, gore, violence, blood, navel, midriff, exposed skin, erotic, ecchi, hentai, uncensored, stripping"
195
+ final_negative_prompt = f"{safety_negative}, worst quality, low quality"
 
 
 
 
 
 
 
196
 
197
  original_image = input_image.convert("RGB")
 
198
  selection_value = ASPECT_RATIOS_MAP.get(aspect_ratio_selection)
199
 
200
  if selection_value == "Custom":
201
+ width, height = (int(custom_width) // 8) * 8, (int(custom_height) // 8) * 8
202
+ elif selection_value == "Auto":
 
203
  width, height = update_dimensions_on_upload(original_image)
204
  else:
205
  width, height = selection_value
206
 
207
  try:
208
+ result = pipe(image=original_image, prompt=english_prompt, negative_prompt=final_negative_prompt, height=height, width=width, num_inference_steps=steps, generator=generator, true_cfg_scale=guidance_scale).images[0]
209
+ if is_image_nsfw(result): return None, seed, get_error_html("تصویر تولید شده حاوی محتوای نامناسب بود و حذف شد.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  return result, seed, get_success_html("تصویر با موفقیت ویرایش شد.")
 
211
  except Exception as e:
212
  error_str = str(e)
213
+ if "quota" in error_str.lower() or "exceeded" in error_str.lower(): raise e
 
214
  return None, seed, get_error_html(f"خطا در پردازش: {error_str}")
215
 
216
  @spaces.GPU(duration=30)
217
  def infer_example(input_image, prompt, lora_adapter):
218
+ # مثال‌ها اعتبار کم نمی‌کنند
219
+ res, s, status = infer(input_image, prompt, lora_adapter, 0, True, 1.0, 4, "خودکار (پیش‌فرض)", 1024, 1024, True, "example_user")
220
  return res, s, status
221
 
222
+ # --- جاوااسکریپت برای دانلود (بدون تغییر) ---
223
+ js_download_func = "async(e)=>{if(!e){return void alert('لطفاً ابتدا تصویر را تولید کنید.')};let o=e.url;o&&!o.startsWith('http')?o=window.location.origin+o:!o&&e.path&&(o=window.location.origin+'/file='+e.path),window.parent.postMessage({type:'DOWNLOAD_REQUEST',url:o},'*')}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
 
225
+ # --- جاوااسکریپت و CSS اصلی (با تغییرات زیاد) ---
226
+ js_and_css_code = """
227
  <script>
228
+ // --- START OF JS LOGIC FOR USER AUTH & CREDITS ---
229
  document.addEventListener('DOMContentLoaded', () => {
230
+
231
+ // --- بخش جدید: مدیریت وضعیت کاربر و اعتبار ---
232
+ const PREMIUM_PAGE_ID = '1149636'; // شناسه صفحه کاربران ویژه
233
+ const PREMIUM_URL = '#/nav/online/news/getSingle/1149636/eyJpdiI6InZSVUdlLzBlR0FzOHZJdXFZeWhER0E9PSIsInZhbHVlIjoiWFhqRXBLc29vSFpHdk9nYmRjZGVuWHRHRHVSZHRlTG1BUENLaE5mNXBNVVRGWFg3ZWN0djJ5K1dIY1RqTHJGaCIsIm1hYyI6IjIzYzFlZTMwYmVmMTdkYjQ0YTQ4YWMxNmFhN2RmNWQ2OTc1NDIyNGVlZGI3ZjJjMjhkNmQxNjM4MDFlZTIxNmUiLCJ0YWciOiIifQ==/20934991';
234
+
235
+ let userFingerprint = null;
236
+ let isPaidUser = false;
237
+ let countdownInterval = null;
238
+
239
+ // توابع UI برای نمایش وضعیت
240
+ const statusContainer = document.getElementById('user-status-section');
241
+ const badgeEl = document.getElementById('subscription-badge');
242
+ const creditTextEl = document.getElementById('credit-status-text');
243
+ const countdownEl = document.getElementById('countdown-timer');
244
+ const upgradeBtn = document.getElementById('upgrade-btn');
245
+ const runBtn = document.getElementById('run-btn');
246
+
247
+ // تابع اصلی برای آپدیت UI بر اساس وضعیت کاربر
248
+ function updateUIForUserStatus(statusData) {
249
+ statusContainer.style.display = 'block';
250
+ if (countdownInterval) clearInterval(countdownInterval);
251
+
252
+ if (isPaidUser) {
253
+ badgeEl.textContent = 'کاربر ویژه';
254
+ badgeEl.className = 'paid-badge';
255
+ creditTextEl.innerHTML = 'دسترسی شما <strong>نامحدود</strong> است.';
256
+ countdownEl.style.display = 'none';
257
+ upgradeBtn.style.display = 'none';
258
+ runBtn.disabled = false;
259
+ } else {
260
+ badgeEl.textContent = 'کاربر رایگان';
261
+ badgeEl.className = 'free-badge';
262
+ const credits = statusData.credits || 0;
263
 
264
+ if (credits > 0) {
265
+ creditTextEl.innerHTML = `شما <strong>${credits} اعتبار</strong> ساخت تصویر برای امروز دارید.`;
266
+ runBtn.disabled = false;
267
+ upgradeBtn.style.display = 'none';
268
+ } else {
269
+ creditTextEl.innerHTML = 'اعتبار رایگان امروز شما تمام شده است.';
270
+ runBtn.disabled = true;
271
+ upgradeBtn.style.display = 'block';
 
 
 
 
 
 
 
 
272
  }
273
+
274
+ // شروع شمارش معکوس
275
+ const resetTimestamp = statusData.next_reset_timestamp;
276
+ const updateTimer = () => {
277
+ const now = Date.now() / 1000;
278
+ const timeLeft = Math.max(0, resetTimestamp - now);
279
+ if (timeLeft === 0) {
280
+ clearInterval(countdownInterval);
281
+ initializeApp(); // برای گرفتن اعتبار جدید
282
+ return;
283
+ }
284
+ const h = Math.floor(timeLeft / 3600);
285
+ const m = Math.floor((timeLeft % 3600) / 60);
286
+ const s = Math.floor(timeLeft % 60);
287
+ countdownEl.querySelector('span').textContent = `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
288
+ };
289
+ updateTimer();
290
+ countdownInterval = setInterval(updateTimer, 1000);
291
+ countdownEl.style.display = 'block';
292
  }
 
293
  }
294
 
295
+ // ارتباط با برنامه اصلی (Parent)
296
+ window.addEventListener('message', (event) => {
297
+ if (event.data && event.data.type === 'USER_STATUS_RESPONSE') {
298
+ try {
299
+ const userObject = JSON.parse(event.data.payload || '{}');
300
+ isPaidUser = userObject && userObject.isLogin && userObject.accessible_pages &&
301
+ (userObject.accessible_pages.includes(PREMIUM_PAGE_ID) || userObject.accessible_pages.includes(parseInt(PREMIUM_PAGE_ID)));
302
+
303
+ // آپدیت کردن کامپوننت‌های مخفی گرادیو
304
+ const fingerprintInput = document.querySelector('#fingerprint-input-for-backend textarea');
305
+ const isPaidInput = document.querySelector('#is-paid-input-for-backend textarea');
306
+ if(fingerprintInput) fingerprintInput.value = userFingerprint;
307
+ if(isPaidInput) isPaidInput.value = isPaidUser ? 'true' : 'false';
308
+
309
+ // ایجاد یک Event برای اینکه گرادیو تغییر را تشخیص دهد
310
+ const eventToDispatch = new Event('input', { bubbles: true });
311
+ if(fingerprintInput) fingerprintInput.dispatchEvent(eventToDispatch);
312
+ if(isPaidInput) isPaidInput.dispatchEvent(eventToDispatch);
313
+
314
+ // حالا که اطلاعات کاربر را داریم، از بک‌اند گرادیو وضعیت اعتبار را می‌پرسیم
315
+ // با کلیک کردن روی دکمه مخفی، تابع پایتون را فراخوانی می‌کنیم
316
+ const statusCheckBtn = document.getElementById('status-check-btn-hidden');
317
+ if(statusCheckBtn) statusCheckBtn.click();
318
+
319
+ } catch (e) {
320
+ console.error("Error processing user status:", e);
321
+ isPaidUser = false; // در صورت خطا، کاربر رایگان در نظر گرفته می‌شود
322
+ }
323
+ }
324
 
325
+ if (event.data.type === 'DOWNLOAD_REQUEST' && event.data.url) {
326
+ // این بخش برای دکمه دانلود است که در HTML دیگر تعریف شده
 
327
  }
328
+
329
+ if (event.data.type === 'NAVIGATE_TO_URL' && event.data.url) {
330
+ window.top.location.href = event.data.url;
331
  }
332
+ });
333
+
334
+ // تابع برای ساخت اثرانگشت مرورگر
335
+ async function getBrowserFingerprint() {
336
+ const components = [navigator.userAgent, navigator.language, screen.width+'x'+screen.height, new Date().getTimezoneOffset()];
337
+ const fingerprintString = components.join('~~~');
338
+ let hash = 0;
339
+ for (let i = 0; i < fingerprintString.length; i++) {
340
+ const char = fingerprintString.charCodeAt(i);
341
+ hash = ((hash << 5) - hash) + char;
342
+ hash |= 0;
343
+ }
344
+ return 'fp_' + Math.abs(hash).toString(16);
345
  }
346
 
347
+ // تابع اصلی برای شروع
348
+ async function initializeApp() {
349
+ userFingerprint = await getBrowserFingerprint();
350
+ // درخواست اطلاعات کاربر از برنامه اصلی
351
+ parent.postMessage({ type: 'REQUEST_USER_STATUS' }, '*');
352
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
 
354
+ // اتصال دکمه ارتقا
355
+ if(upgradeBtn) {
356
+ upgradeBtn.addEventListener('click', () => {
357
+ parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM', payload: { url: PREMIUM_URL } }, '*');
358
+ });
359
+ }
360
 
361
+ // --- مدیریت خطای Quota (بدون تغییر) ---
 
 
362
  const showQuotaModal = () => {
363
  if (document.getElementById('custom-quota-modal')) return;
364
+ const modalHtml = `<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;"><div class="ip-reset-guide-container"><div class="guide-header"><h2>یک قدم تا ساخت تصاویر جدید</h2><p>نیازمند تغییر نقطه دستیابی</p></div><div class="guide-content"><p>برای استفاده نامحدود از هوش مصنوعی، لطفاً نقطه اتصال اینترنت (IP) خود را تغییر دهید (مثلاً با خاموش/روشن کردن حالت پرواز) و دوباره تلاش کنید.</p><button onclick="parent.postMessage({ type: 'NAVIGATE_TO_URL', url: '#/nav/online/news/getSingle/1149635/...'}, '*')">دیدن ویدیو آموزشی</button></div><div class="guide-actions"><button class="action-button back-button" onclick="this.closest('#custom-quota-modal').remove()">بازگشت</button><button class="action-button retry-button" onclick="document.getElementById('run-btn').click(); this.closest('#custom-quota-modal').remove();">تلاش مجدد</button></div></div></div>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
  document.body.insertAdjacentHTML('beforeend', modalHtml);
 
366
  };
 
 
 
 
367
  setInterval(() => {
368
+ document.querySelectorAll('.toast-body, .error').forEach(el => {
369
+ if ((el.innerText || "").toLowerCase().includes('quota')) {
370
+ showQuotaModal();
371
+ el.style.display = 'none';
 
 
 
 
 
 
372
  }
373
  });
374
  }, 100);
375
+
376
+ initializeApp();
377
+
378
+ // این تابع مخصوص گرادیو است تا وقتی خروجی JSON آپدیت شد، UI را آپدیت کند
379
+ window.updateUIFromGradio = function(jsonData) {
380
+ updateUIForUserStatus(jsonData);
381
+ }
382
  });
383
  </script>
 
 
 
 
384
  <style>
385
+ /* --- استایل‌های اصلی (مشابه قبل با اضافات جدید) --- */
386
  @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700&display=swap');
387
+ body, .gradio-container { font-family: 'Vazirmatn', sans-serif !important; background-color: #f5f7fa !important; color-scheme: light !important; }
388
+ #col-container { direction: rtl; text-align: right; max-width: 980px; margin: 0 auto; padding: 30px; background: #ffffff; border-radius: 24px; box-shadow: 0 10px 40px -10px rgba(0,0,0,0.08); }
389
+ #main-title h1 { text-align: center; font-size: 2.4em !important; font-weight: 800; background: -webkit-linear-gradient(45deg, #2563eb, #1e40af); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
390
+ #main-description { text-align: center; font-size: 1.15em; color: #4b5563 !important; margin-bottom: 25px; line-height: 1.6; }
391
+ footer, .flagging { display: none !important; }
392
+
393
+ /* --- استایل‌های جدید برای بخش وضعیت کاربر --- */
394
+ #user-status-section {
395
+ display: none; /* Initially hidden */
396
+ text-align: center;
397
+ margin-bottom: 25px;
398
+ padding: 15px;
399
+ background-color: #f8f9fa;
400
+ border-radius: 14px;
401
+ border: 1px solid #e9ecef;
402
  }
403
+ #subscription-badge {
404
+ display: inline-block;
405
+ padding: 6px 16px;
406
+ border-radius: 20px;
407
+ font-size: 0.9em;
 
 
 
 
408
  font-weight: 700;
409
+ margin-bottom: 10px;
 
 
 
410
  }
411
+ #subscription-badge.free-badge { background: linear-gradient(45deg, #6c757d, #495057); color: white; }
412
+ #subscription-badge.paid-badge { background: linear-gradient(45deg, #198754, #157347); color: white; }
413
+ #credit-status-text {
414
+ font-size: 1em;
415
+ color: #495057;
416
+ margin-bottom: 10px;
417
  }
418
+ #credit-status-text strong { color: #0d6efd; }
419
+ #countdown-timer {
420
+ font-size: 0.9em;
421
+ color: #6c757d;
 
422
  }
423
+ #countdown-timer span { font-weight: bold; font-variant-numeric: tabular-nums; }
424
+ #upgrade-btn {
425
+ display: none; /* Initially hidden */
426
+ margin-top: 15px;
427
+ padding: 10px 20px;
428
+ font-family: 'Vazirmatn', sans-serif;
429
+ font-weight: 700;
430
+ font-size: 1em;
431
+ color: white;
432
+ background: linear-gradient(135deg, #ffc107, #ff9800);
433
+ border: none;
434
+ border-radius: 12px;
435
+ cursor: pointer;
436
+ transition: all 0.2s ease;
437
+ box-shadow: 0 4px 10px rgba(255, 193, 7, 0.3);
438
  }
439
+ #upgrade-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 15px rgba(255, 193, 7, 0.4); }
440
 
441
+ .primary-btn:disabled {
442
+ background: #adb5bd !important;
443
+ cursor: not-allowed;
444
+ box-shadow: none !important;
445
+ transform: none !important;
 
 
446
  }
447
+
448
+ /* --- بقیه استایل‌ها (بدون تغییر) --- */
449
+ .primary-btn { background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important; border:none !important; color:white !important; font-weight:700 !important; font-size:1.1em !important; padding:14px 28px !important; border-radius:14px !important; box-shadow:0 4px 15px rgba(16,185,129,0.3) !important; transition:all 0.3s ease !important; cursor:pointer !important; width:100%; margin-top:15px; }
450
+ .primary-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 25px rgba(16,185,129,0.45) !important; }
451
+ .ip-reset-guide-container { direction: rtl; ... } /* Styles for quota modal */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
452
  </style>
453
  """
454
 
455
+ # استفاده از gr.Blocks
456
+ with gr.Blocks(theme=gr.themes.Soft(primary_hue=colors.steel_blue)) as demo:
457
+ gr.HTML(js_and_css_code)
 
458
 
459
  with gr.Column(elem_id="col-container"):
460
  gr.Markdown("# **ویرایشگر هوشمند آلفا**", elem_id="main-title")
461
  gr.Markdown("با هوش مصنوعی آلفا تصاویر تونو به مدل های مختلف ویرایش کنید.", elem_id="main-description")
 
 
 
462
 
463
+ # --- بخش جدید: نمایش وضعیت کاربر ---
464
+ gr.HTML("""
465
+ <div id="user-status-section">
466
+ <div id="subscription-badge">...</div>
467
+ <p id="credit-status-text">در حال بررسی وضعیت حساب...</p>
468
+ <div id="countdown-timer" style="display: none;">
469
+ زمان تا شارژ مجدد اعتبار: <span></span>
470
+ </div>
471
+ <button id="upgrade-btn">⭐️ ارتقا به نسخه کامل و نامحدود</button>
472
+ </div>
473
+ """)
474
+
475
+ # --- بخش جدید: کامپوننت‌های مخفی برای ارتباط JS و Python ---
476
+ with gr.Row(visible=False):
477
+ fingerprint_input = gr.Textbox(elem_id="fingerprint-input-for-backend", label="FP")
478
+ is_paid_input = gr.Textbox(elem_id="is-paid-input-for-backend", label="Paid")
479
+ status_check_btn = gr.Button(elem_id="status-check-btn-hidden")
480
+ status_json_output = gr.JSON(label="Status JSON")
481
 
482
  with gr.Row(equal_height=True):
483
  with gr.Column():
484
  input_image = gr.Image(label="بارگذاری تصویر", type="pil", height=320)
485
  prompt = gr.Text(label="دستور ویرایش (به فارسی)", placeholder="مثال: تصویر را به سبک انیمه تبدیل کن...", rtl=True, lines=3)
486
  status_box = gr.HTML(label="وضعیت")
487
+ run_button = gr.Button("✨ شروع پردازش و ساخت تصویر", variant="primary", elem_classes="primary-btn", elem_id="run-btn")
488
 
489
  with gr.Column():
490
  output_image = gr.Image(label="تصویر نهایی", interactive=False, format="png", height=380)
491
  download_button = gr.Button("📥 دانلود و ذخیره تصویر", variant="secondary", elem_id="download-btn", elem_classes="primary-btn")
492
+ lora_adapter = gr.Dropdown(label="انتخاب سبک ویرایش (LoRA)", choices=list(LORA_MAPPING.keys()), value="تبدیل عکس به انیمه")
 
 
493
 
494
  with gr.Accordion("تنظیمات پیشرفته", open=False):
495
+ aspect_ratio_selection = gr.Dropdown(label="ابعاد تصویر خروجی", choices=ASPECT_RATIOS_LIST, value="خودکار (پیش‌فرض)")
496
  with gr.Row(visible=False) as custom_dims_row:
497
  custom_width = gr.Slider(label="عرض", minimum=256, maximum=2048, step=8, value=1024)
498
  custom_height = gr.Slider(label="ارتفاع", minimum=256, maximum=2048, step=8, value=1024)
499
+ seed = gr.Slider(label="دانه تصادفی (Seed)", minimum=0, maximum=MAX_SEED, step=1, value=0)
500
+ randomize_seed = gr.Checkbox(label="استفاده از Seed تصادفی", value=True)
501
+ guidance_scale = gr.Slider(label="وفاداری به متن", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
502
+ steps = gr.Slider(label="مراحل پردازش", minimum=1, maximum=50, step=1, value=4)
503
 
 
504
  def toggle_row(choice):
505
+ return gr.update(visible=choice == "شخصی‌سازی (Custom)")
 
506
  aspect_ratio_selection.change(fn=toggle_row, inputs=aspect_ratio_selection, outputs=custom_dims_row)
507
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
  gr.Examples(
509
+ examples=[["examples/1.jpg", "تبدیل به انیمه کن.", "تبدیل عکس به انیمه"], ["examples/5.jpg", "سایه‌ها را حذف کن و نورپردازی نرم بده.", "اصلاح نور و سایه"]],
510
+ inputs=[input_image, prompt, lora_adapter], outputs=[output_image, seed, status_box], fn=infer_example, cache_examples=False, label="نمونه‌ها"
 
 
 
 
 
 
 
511
  )
512
 
513
+ # --- اتصال رویدادها ---
514
+ run_button.click(
515
+ fn=infer,
516
+ inputs=[input_image, prompt, lora_adapter, seed, randomize_seed, guidance_scale, steps, aspect_ratio_selection, custom_width, custom_height, is_paid_input, fingerprint_input],
517
+ outputs=[output_image, seed, status_box],
518
+ api_name="predict"
519
+ )
520
+
521
+ # رویداد دکمه مخفی برای گرفتن وضعیت کاربر
522
+ status_check_btn.click(
523
+ fn=get_user_status_api,
524
+ inputs=[fingerprint_input],
525
+ outputs=[status_json_output]
526
+ )
527
+
528
+ # وقتی خروجی JSON آپدیت شد، تابع جاوااسکریپت را صدا بزن
529
+ status_json_output.change(
530
+ fn=None,
531
+ inputs=[status_json_output],
532
+ js="""
533
+ (jsonData) => {
534
+ // Check if the function exists on window before calling
535
+ if (window.updateUIFromGradio) {
536
+ window.updateUIFromGradio(jsonData);
537
+ } else {
538
+ console.error("Gradio UI update function not found!");
539
+ }
540
+ }
541
+ """
542
+ )
543
+
544
+ download_button.click(fn=None, inputs=[output_image], outputs=None, js=js_download_func)
545
+
546
+ # --- تعریف API برای ارتباط مستقیم (در صورت نیاز) ---
547
+ demo.api(name="get_status")(get_user_status_api)
548
+
549
  if __name__ == "__main__":
550
  demo.queue(max_size=30).launch(show_error=True)