Opera8 commited on
Commit
9991c95
·
verified ·
1 Parent(s): d9eef71

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +791 -396
app.py CHANGED
@@ -4,117 +4,53 @@ import numpy as np
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
@@ -123,15 +59,19 @@ from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
123
  dtype = torch.bfloat16
124
  device = "cuda" if torch.cuda.is_available() else "cpu"
125
 
126
- print("Loading Generation Pipeline...")
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,406 +85,861 @@ pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
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)
 
4
  import spaces
5
  import torch
6
  import random
7
+ from PIL import Image, ImageFilter
8
  from typing import Iterable
9
+ from gradio.themes import Soft
10
+ from gradio.themes.utils import colors, fonts, sizes
11
  from deep_translator import GoogleTranslator
12
  from transformers import pipeline
13
+ from datetime import datetime
 
 
 
14
 
15
+ # --- متغیرهای سراسری برای مدیریت محدودیت ---
16
+ USAGE_TRACKER = {} # ذخیره: {ip: {'count': 0, 'date': 'YYYY-MM-DD'}}
17
+ DAILY_LIMIT = 5 # محدودیت تعداد عکس در روز برای کاربران رایگان
18
+
19
+ # --- تعریف تم ---
20
  colors.steel_blue = colors.Color(
21
+ name="steel_blue",
22
+ c50="#EBF3F8",
23
+ c100="#D3E5F0",
24
+ c200="#A8CCE1",
25
+ c300="#7DB3D2",
26
+ c400="#529AC3",
27
+ c500="#4682B4",
28
+ c600="#3E72A0",
29
+ c700="#36638C",
30
+ c800="#2E5378",
31
+ c900="#264364",
32
+ c950="#1E3450",
33
  )
34
 
35
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
+ # --- بارگذاری مدل تشخیص محتوای نامناسب (NSFW) ---
38
+ print("Loading Safety Checker...")
39
+ safety_classifier = pipeline("image-classification", model="Falconsai/nsfw_image_detection", device=-1)
 
40
 
41
  def is_image_nsfw(image):
42
  if image is None: return False
43
  try:
44
+ results = safety_classifier(image)
45
+ for result in results:
46
+ if result['label'] == 'nsfw' and result['score'] > 0.75:
 
 
 
 
 
 
 
 
 
 
47
  return True
48
  return False
49
  except Exception as e:
50
  print(f"Safety check error: {e}")
51
+ return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
+ # --- بارگذاری مدل اصلی ---
54
  from diffusers import FlowMatchEulerDiscreteScheduler
55
  from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
56
  from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
 
59
  dtype = torch.bfloat16
60
  device = "cuda" if torch.cuda.is_available() else "cpu"
61
 
62
+ print("Loading pipeline...")
63
  pipe = QwenImageEditPlusPipeline.from_pretrained(
64
  "Qwen/Qwen-Image-Edit-2509",
65
  transformer=QwenImageTransformer2DModel.from_pretrained(
66
+ "linoyts/Qwen-Image-Edit-Rapid-AIO",
67
+ subfolder='transformer',
68
+ torch_dtype=dtype,
69
+ device_map='cuda'
70
  ),
71
  torch_dtype=dtype
72
  ).to(device)
73
 
74
+ # بارگذاری LoRA ها
75
  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")
76
  pipe.load_lora_weights("dx8152/Qwen-Edit-2509-Multiple-angles", weight_name="镜头转换.safetensors", adapter_name="multiple-angles")
77
  pipe.load_lora_weights("dx8152/Qwen-Image-Edit-2509-Light_restoration", weight_name="移除光影.safetensors", adapter_name="light-restoration")
 
85
  MAX_SEED = np.iinfo(np.int32).max
86
 
87
  LORA_MAPPING = {
88
+ "تبدیل عکس به انیمه": "anime",
89
+ "تغییر زاویه دید": "multiple-angles",
90
+ "اصلاح نور و سایه": "light-restoration",
91
+ "نورپردازی مجدد (Relight)": "relight",
92
+ "نورپردازی چند زاویه‌ای": "multi-angle-lighting",
93
+ "روتوش پوست": "edit-skin",
94
+ "صحنه بعدی (سینمایی)": "next-scene",
95
+ "افزایش کیفیت (Upscale)": "upscale-image"
96
  }
97
+
98
  ASPECT_RATIOS_LIST = [
99
+ "خودکار (پیش‌فرض)",
100
+ "۱:۱ (مربع - 1024x1024)",
101
+ "۱۶:۹ (افقی - 1344x768)",
102
+ "۹:۱۶ (عمودی - 768x1344)",
103
+ "شخصی‌سازی (Custom)"
104
  ]
105
+
106
  ASPECT_RATIOS_MAP = {
107
+ "خودکار (پیش‌فرض)": "Auto",
108
+ "۱:۱ (مربع - 1024x1024)": (1024, 1024),
109
+ "۱۶:۹ (افقی - 1344x768)": (1344, 768),
110
+ "۹:۱۶ (عمودی - 768x1344)": (768, 1344),
111
+ "شخصی‌سازی (Custom)": "Custom"
112
  }
113
 
114
+ BANNED_WORDS = [
115
+ "nude", "naked", "sex", "porn", "undressed", "nsfw", "erotic", "xxx",
116
+ "breast", "nipple", "genital", "vagina", "penis", "ass", "butt", "sexual",
117
+ "lingerie", "bikini", "swimwear", "underwear", "fetish", "topless",
118
+ "exhibitionism", "hentai", "ecchi", "18+"
119
+ ]
120
+
121
+ def check_text_safety(text):
122
+ text_lower = text.lower()
123
+ for word in BANNED_WORDS:
124
+ if f" {word} " in f" {text_lower} ":
125
+ return False
126
+ return True
127
+
128
+ def translate_prompt(text):
129
+ if not text:
130
+ return ""
131
+ try:
132
+ translated = GoogleTranslator(source='auto', target='en').translate(text)
133
+ return translated
134
+ except Exception as e:
135
+ print(f"Translation Error: {e}")
136
+ return text
137
+
138
  def update_dimensions_on_upload(image):
139
+ if image is None:
140
+ return 1024, 1024
141
+ original_width, original_height = image.size
142
+ if original_width > original_height:
143
+ new_width = 1024
144
+ aspect_ratio = original_height / original_width
145
+ new_height = int(new_width * aspect_ratio)
146
+ else:
147
+ new_height = 1024
148
+ aspect_ratio = original_width / original_height
149
+ new_width = int(new_height * aspect_ratio)
150
+ new_width = (new_width // 8) * 8
151
+ new_height = (new_height // 8) * 8
152
+ return new_width, new_height
153
+
154
+ def update_sliders_visibility(choice):
155
+ if choice == "شخصی‌سازی (Custom)":
156
+ return gr.update(visible=True), gr.update(visible=True)
157
+ else:
158
+ return gr.update(visible=False), gr.update(visible=False)
159
 
160
  def get_error_html(message):
161
+ return f"""
162
+ <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;">
163
+ <span style="font-size: 1.2em;">⛔</span>
164
+ {message}
165
+ </div>
166
+ """
167
 
168
  def get_success_html(message):
169
+ return f"""
170
+ <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;">
171
+ <span style="font-size: 1.2em;">✅</span>
172
+ {message}
173
+ </div>
174
+ """
175
+
176
+ def check_and_update_quota(request: gr.Request, is_vip: bool):
177
+ """
178
+ بررسی محدودیت کاربر و بروزرسانی شمارنده.
179
+ اگر VIP باشد، محدودیت ندارد.
180
+ اگر رایگان باشد، بر اساس IP محدودیت ۵ بار در روز اعمال می‌شود.
181
+ """
182
+ if is_vip:
183
+ return True, ""
184
+
185
+ try:
186
+ client_ip = request.client.host
187
+ except:
188
+ client_ip = "unknown"
189
+
190
+ today = datetime.now().strftime("%Y-%m-%d")
191
+
192
+ # مقداردهی اولیه برای IP جدید
193
+ if client_ip not in USAGE_TRACKER:
194
+ USAGE_TRACKER[client_ip] = {'count': 0, 'date': today}
195
+
196
+ user_data = USAGE_TRACKER[client_ip]
197
+
198
+ # ریست کردن اگر روز تغییر کرده باشد
199
+ if user_data['date'] != today:
200
+ user_data['count'] = 0
201
+ user_data['date'] = today
202
+
203
+ # بررسی سقف مجاز
204
+ if user_data['count'] >= DAILY_LIMIT:
205
+ return False, f"شما به سقف مجاز روزانه ({DAILY_LIMIT} تصویر) رسیده‌اید. فردا مجدداً تلاش کنید یا حساب خود را ارتقا دهید."
206
+
207
+ return True, ""
208
+
209
+ def increment_quota(request: gr.Request, is_vip: bool):
210
+ """
211
+ افزایش شمارنده پس از موفقیت آمیز بودن عملیات
212
+ """
213
+ if is_vip: return
214
+ try:
215
+ client_ip = request.client.host
216
+ if client_ip in USAGE_TRACKER:
217
+ USAGE_TRACKER[client_ip]['count'] += 1
218
+ except:
219
+ pass
220
 
221
+ @spaces.GPU(duration=30)
222
  def infer(
223
+ input_image,
224
+ prompt,
225
+ lora_adapter_persian,
226
+ seed,
227
+ randomize_seed,
228
+ guidance_scale,
229
+ steps,
230
+ aspect_ratio_selection,
231
+ custom_width,
232
+ custom_height,
233
+ is_vip_state, # ورودی جدید برای وضعیت VIP
234
+ request: gr.Request,
235
  progress=gr.Progress(track_tqdm=True)
236
  ):
237
+ # --- بررسی سهمیه (Quota) ---
238
+ allowed, message = check_and_update_quota(request, is_vip_state)
239
+ if not allowed:
240
+ return None, seed, get_error_html(message)
241
+
242
+ if input_image is None:
243
+ return None, seed, get_error_html("لطفاً ابتدا یک تصویر بارگذاری کنید.")
244
+
245
+ if is_image_nsfw(input_image):
246
+ return None, seed, get_error_html("تصویر ورودی دارای محتوای نامناسب است و پردازش نمی‌شود.")
247
+
248
  english_prompt = translate_prompt(prompt)
249
+ if not check_text_safety(english_prompt):
250
+ return None, seed, get_error_html("متن درخواست شامل کلمات غیرمجاز یا غیراخلاقی است.")
251
 
252
  adapter_internal_name = LORA_MAPPING.get(lora_adapter_persian)
253
+ if adapter_internal_name:
254
+ pipe.set_adapters([adapter_internal_name], adapter_weights=[1.0])
255
 
256
+ if randomize_seed:
257
+ seed = random.randint(0, MAX_SEED)
258
+
259
  generator = torch.Generator(device=device).manual_seed(seed)
260
 
261
+ safety_negative = "nsfw, nude, naked, porn, sexual, xxx, breast, nipple, genital, vagina, penis, ass, lingerie, bikini, swimwear, underwear, fetish, topless, gore, violence, blood"
262
+ 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"
263
+ final_negative_prompt = f"{safety_negative}, {base_negative}"
264
 
265
  original_image = input_image.convert("RGB")
266
+
267
  selection_value = ASPECT_RATIOS_MAP.get(aspect_ratio_selection)
268
 
269
  if selection_value == "Custom":
270
+ width = (int(custom_width) // 8) * 8
271
+ height = (int(custom_height) // 8) * 8
272
+ elif selection_value == "Auto" or selection_value is None:
273
  width, height = update_dimensions_on_upload(original_image)
274
  else:
275
  width, height = selection_value
276
 
277
  try:
278
+ result = pipe(
279
+ image=original_image,
280
+ prompt=english_prompt,
281
+ negative_prompt=final_negative_prompt,
282
+ height=height,
283
+ width=width,
284
+ num_inference_steps=steps,
285
+ generator=generator,
286
+ true_cfg_scale=guidance_scale,
287
+ ).images[0]
288
+
289
+ if is_image_nsfw(result):
290
+ return None, seed, get_error_html("تصویر تولید شده حاوی محتوای نامناسب بود و حذف شد.")
291
+
292
+ # افزایش شمارنده مصرف فقط در صورت موفقیت
293
+ increment_quota(request, is_vip_state)
294
+
295
  return result, seed, get_success_html("تصویر با موفقیت ویرایش شد.")
296
+
297
  except Exception as e:
298
  error_str = str(e)
299
+ if "quota" in error_str.lower() or "exceeded" in error_str.lower():
300
+ raise e
301
  return None, seed, get_error_html(f"خطا در پردازش: {error_str}")
302
 
303
  @spaces.GPU(duration=30)
304
  def infer_example(input_image, prompt, lora_adapter):
305
+ # برای مثال‌ها محدودیت VIP بررسی نمی‌شود و IP لوکال فرض می‌شود
306
+ res, s, status = infer(input_image, prompt, lora_adapter, 0, True, 1.0, 4, "خودکار (پیش‌فرض)", 1024, 1024, True, None)
307
  return res, s, status
308
 
309
+ # --- جاوااسکریپت برای دکمه دانلود ---
310
+ js_download_func = """
311
+ async (image) => {
312
+ if (!image) {
313
+ alert("لطفاً ابتدا تصویر را تولید کنید.");
314
+ return;
315
+ }
316
+ let fileUrl = image.url;
317
+ if (fileUrl && !fileUrl.startsWith('http')) {
318
+ fileUrl = window.location.origin + fileUrl;
319
+ } else if (!fileUrl && image.path) {
320
+ fileUrl = window.location.origin + "/file=" + image.path;
321
+ }
322
+ window.parent.postMessage({
323
+ type: 'DOWNLOAD_REQUEST',
324
+ url: fileUrl
325
+ }, '*');
326
+ }
327
+ """
328
 
329
+ # --- جاوااسکریپت سراسری (استراتژی موبایل + بستن خودکار ۱۰ ثانیه + تشخیص VIP) ---
330
+ js_global_content = """
331
  <script>
332
+ // Global variable to store VIP status
333
+ window.userIsVip = false;
334
+ const PREMIUM_PAGE_ID = '1149635'; // شناسه صفحه پرداخت برای بررسی دسترسی
335
+
336
+ // Request User Status on Load
337
+ setTimeout(() => {
338
+ window.parent.postMessage({ type: 'REQUEST_USER_STATUS' }, '*');
339
+ }, 1000);
340
+
341
+ // Listener for User Status
342
+ window.addEventListener('message', (event) => {
343
+ if (event.data && event.data.type === 'USER_STATUS_RESPONSE') {
344
+ try {
345
+ const user = JSON.parse(event.data.payload);
346
+ // بررسی دسترسی کاربر به صفحه پرمیوم
347
+ if (user && user.isLogin && user.accessible_pages &&
348
+ (user.accessible_pages.includes(PREMIUM_PAGE_ID) ||
349
+ user.accessible_pages.includes(parseInt(PREMIUM_PAGE_ID)))) {
350
+
351
+ window.userIsVip = true;
352
+
353
+ // نمایش نشان VIP
354
+ const badge = document.createElement('div');
355
+ badge.innerHTML = '👑 نسخه ویژه (نامحدود)';
356
+ badge.style.cssText = 'position:fixed;top:10px;left:10px;background:linear-gradient(45deg, #FFD700, #FFC107);color:#333;padding:6px 12px;border-radius:20px;z-index:9999;font-weight:bold;box-shadow:0 2px 5px rgba(0,0,0,0.2);font-family:"Vazirmatn",sans-serif;font-size:0.8rem;';
357
+ document.body.appendChild(badge);
358
+
359
+ // حذف مدال محدودیت اگر باز باشد
360
+ window.closeErrorModal();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
  }
362
+ } catch (e) { console.log('Error parsing user status:', e); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
  }
364
+ });
365
 
366
+ document.addEventListener('DOMContentLoaded', () => {
367
+ // 1. Force Light Mode
368
+ const forceLight = () => {
369
+ const body = document.querySelector('body');
370
+ if (body) {
371
+ body.classList.remove('dark');
372
+ body.style.backgroundColor = '#f5f7fa';
373
+ body.style.color = '#333333';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  }
375
+ document.querySelectorAll('.dark').forEach(el => el.classList.remove('dark'));
376
+ };
377
+ forceLight();
378
+ setInterval(forceLight, 1000);
379
+
380
+ // 2. RETRY FUNCTION
381
+ window.retryGeneration = function() {
382
+ const modal = document.getElementById('custom-quota-modal');
383
+ if (modal) modal.remove();
384
+
385
+ const runBtn = document.getElementById('run-btn');
386
+ if(runBtn) runBtn.click();
387
+ };
388
 
389
+ // Close function
390
+ window.closeErrorModal = function() {
391
+ const modal = document.getElementById('custom-quota-modal');
392
+ if (modal) modal.remove();
393
+ };
 
394
 
395
+ // 3. SHOW MODAL FUNCTION
396
  const showQuotaModal = () => {
397
  if (document.getElementById('custom-quota-modal')) return;
398
+
399
+ const modalHtml = `
400
+ <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;">
401
+ <div class="ip-reset-guide-container">
402
+ <div class="guide-header">
403
+ <svg class="guide-header-icon" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
404
+ <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>
405
+ <circle cx="50" cy="50" r="45" fill="url(#grad1)" opacity="0.1"></circle>
406
+ <circle cx="50" cy="50" r="35" fill="none" stroke="url(#grad1)" stroke-width="2" opacity="0.3"></circle>
407
+ <path d="M35 50 L45 60 L65 40" stroke="url(#grad1)" stroke-width="4" fill="none" stroke-linecap="round" stroke-linejoin="round"></path>
408
+ <circle cx="65" cy="35" r="8" fill="#fee140"></circle>
409
+ <path d="M62 35 L68 35 M65 32 L65 38" stroke="white" stroke-width="2" stroke-linecap="round"></path>
410
+ </svg>
411
+ <div>
412
+ <h2>محدودیت سرور گرافیکی</h2>
413
+ <p>نیازمند تغییر نقطه دستیابی</p>
414
+ </div>
415
+ </div>
416
+
417
+ <div class="guide-content">
418
+ <div class="info-card">
419
+ <div class="info-card-header">
420
+ <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>
421
+ <span class="info-card-title">راه حل سریع</span>
422
+ </div>
423
+ <p>طبق ویدیو آموزشی پایین بین نقطه دستیابی جابجا شوید تلاش مجدد بزنید تا تصاویر مجدداً تولید بشه.</p>
424
+ </div>
425
+
426
+ <div class="summary-section">
427
+ <div class="summary-header">
428
+ <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>
429
+ <span class="summary-title">خلاصه راهنما</span>
430
+ </div>
431
+ <div class="summary-text">این پیام مربوط به سرور جهانی است. برای رفع آن: اینترنت سیم‌کارت استفاده کنید، VPN را خاموش کرده و طبق ویدیو آموزشی نقطه دستیابی رو تغییر دهید.</div>
432
+ </div>
433
+
434
+ <div class="video-button-container">
435
+ <button onclick="parent.postMessage({ type: 'NAVIGATE_TO_URL', url: '#/nav/online/news/getSingle/1149635/eyJpdiI6IjhHVGhPQWJwb3E0cjRXbnFWTW5BaUE9PSIsInZhbHVlIjoiS1V0dTdvT21wbXAwSXZaK1RCTG1pVXZqdlFJa1hXV1RKa2FLem9zU3pXMjd5MmlVOGc2YWY0NVdNR3h3Smp1aSIsIm1hYyI6IjY1NTA5ZDYzMjAzMTJhMGQyMWQ4NjA4ZDgyNGZjZDVlY2MyNjdiMjA2NWYzOWRjY2M4ZmVjYWRlMWNlMWQ3ODEiLCJ0YWciOiIifQ==/21135210' }, '*')" class="elegant-video-button">
436
+ <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>
437
+ <span>دیدن ویدیو آموزشی استفاده نامحدود</span>
438
+ </button>
439
+ </div>
440
+ </div>
441
+
442
+ <div class="guide-actions">
443
+ <button class="action-button back-button" onclick="window.closeErrorModal()">
444
+ <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>
445
+ <span>بازگشت</span>
446
+ </button>
447
+ <button class="action-button retry-button" onclick="window.retryGeneration()">
448
+ <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>
449
+ <span>تلاش مجدد</span>
450
+ </button>
451
+ </div>
452
+ </div>
453
+ </div>
454
+ `;
455
+
456
  document.body.insertAdjacentHTML('beforeend', modalHtml);
457
+
458
+ // Auto close after 10 seconds
459
+ setTimeout(() => {
460
+ window.closeErrorModal();
461
+ }, 10000);
462
  };
463
+
464
+ // 4. SCANNER (For HuggingFace GPU Quota)
465
  setInterval(() => {
466
+ const potentialErrors = document.querySelectorAll('.toast-body, .error, .toast-wrap, .eta-bar, div[class*="error"]');
467
+
468
+ potentialErrors.forEach(el => {
469
+ const text = el.innerText || "";
470
+ if (text.toLowerCase().includes('quota') || text.toLowerCase().includes('exceeded')) {
471
+
472
+ showQuotaModal();
473
+
474
+ // Immediately hide the Gradio error
475
+ el.style.display = 'none';
476
+ el.style.opacity = '0';
477
+ el.innerText = '';
478
+
479
+ const parentWrap = el.closest('.toast-wrap');
480
+ if(parentWrap) parentWrap.style.display = 'none';
481
  }
482
  });
483
  }, 100);
 
 
 
 
 
 
 
484
  });
485
  </script>
486
+ """
487
+
488
+ # --- CSS Updated (Larger & Auto Scroll) ---
489
+ css_code = """
490
  <style>
 
491
  @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700&display=swap');
492
+
493
+ :root, .dark, body, .gradio-container {
494
+ --body-background-fill: #f5f7fa !important;
495
+ --body-text-color: #1f2937 !important;
496
+ --background-fill-primary: #ffffff !important;
497
+ --background-fill-secondary: #f3f4f6 !important;
498
+ --border-color-primary: #e5e7eb !important;
499
+ --block-background-fill: #ffffff !important;
500
+ --block-label-text-color: #374151 !important;
501
+ --block-title-text-color: #111827 !important;
502
+ --input-background-fill: #ffffff !important;
503
+ color-scheme: light !important;
504
+ }
505
+
506
+ /* --- IP Reset Guide CSS --- */
507
+ :root {
508
+ --guide-bg: rgba(255, 255, 255, 0.98);
509
+ --guide-border: rgba(102, 126, 234, 0.2);
510
+ --guide-text-title: #2d3748;
511
+ --guide-text-body: #4a5568;
512
+ --guide-accent: #667eea;
513
+ --primary-gradient-guide: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
514
+ --success-gradient-guide: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%);
515
+ --radius-md-guide: 12px;
516
+ --radius-lg-guide: 16px;
517
+ --shadow-sm: 0 1px 2px 0 rgba(26, 32, 44, 0.03);
518
+ --shadow-md: 0 4px 6px -1px rgba(26, 32, 44, 0.05), 0 2px 4px -2px rgba(26, 32, 44, 0.04);
519
+ --shadow-xl: 0 20px 25px -5px rgba(26, 32, 44, 0.07), 0 8px 10px -6px rgba(26, 32, 44, 0.05);
520
+ }
521
+
522
+ @keyframes float {
523
+ 0%, 100% { transform: translateY(0px); }
524
+ 50% { transform: translateY(-10px); }
525
+ }
526
+ @keyframes slideInUp {
527
+ from { opacity: 0; transform: translateY(30px); }
528
+ to { opacity: 1; transform: translateY(0); }
529
+ }
530
+
531
+ .ip-reset-guide-container {
532
+ text-align: right;
533
+ direction: rtl;
534
+ background: var(--guide-bg);
535
+ backdrop-filter: blur(10px);
536
+ padding: 20px; /* Increased Padding */
537
+ border-radius: var(--radius-lg-guide);
538
+ box-shadow: var(--shadow-xl);
539
+ border: 1px solid var(--guide-border);
540
+ animation: slideInUp 0.6s cubic-bezier(0.4, 0, 0.2, 1) both;
541
+ width: 90%;
542
+ max-width: 420px; /* Increased width slightly */
543
+ max-height: 90vh; /* Prevent overflow on small screens */
544
+ overflow-y: auto; /* Enable scroll if needed */
545
+ position: relative;
546
+ box-sizing: border-box;
547
+ font-family: 'Vazirmatn', sans-serif !important;
548
+ }
549
+ .ip-reset-guide-container::before {
550
+ content: ''; position: absolute; top: 0; left: 0; right: 0; height: 4px; background: var(--primary-gradient-guide);
551
+ }
552
+ .guide-header { display: flex; align-items: center; margin-bottom: 15px; }
553
+ .guide-header-icon { width: 45px; height: 45px; margin-left: 15px; animation: float 3s ease-in-out infinite; flex-shrink: 0; }
554
+ .guide-header h2 { font-size: 1.2rem; color: var(--guide-text-title); font-weight: 700; margin: 0; }
555
+ .guide-header p { color: var(--guide-text-body); font-size: 0.8rem; margin-top: 3px; margin-bottom: 0; }
556
+
557
+ .guide-content { font-size: 0.9rem; color: var(--guide-text-body); line-height: 1.6; }
558
+
559
+ .info-card { background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%); border: 1px solid rgba(102, 126, 234, 0.2); border-radius: var(--radius-md-guide); padding: 12px; margin: 12px 0; position: relative; overflow: hidden; }
560
+ .info-card p { font-size: 0.85rem; line-height: 1.6; margin: 0; }
561
+ .info-card::before { content: ''; position: absolute; top: 0; right: 0; width: 3px; height: 100%; background: var(--primary-gradient-guide); }
562
+ .info-card-header { display: flex; align-items: center; margin-bottom: 8px; }
563
+ .info-card-icon { width: 18px; height: 18px; margin-left: 8px; }
564
+ .info-card-title { font-weight: 600; color: var(--guide-text-title); font-size: 0.95rem; }
565
+
566
+ .summary-section { margin-top: 12px; padding: 12px; border-radius: var(--radius-md-guide); background: linear-gradient(135deg, #56ab2f15 0%, #a8e06315 100%); border: 1px solid rgba(86, 171, 47, 0.2); position: relative; overflow: hidden; }
567
+ .summary-section::before { content: ''; position: absolute; top: 0; right: 0; width: 3px; height: 100%; background: var(--success-gradient-guide); }
568
+ .summary-header { display: flex; align-items: center; margin-bottom: 8px; }
569
+ .summary-icon { width: 18px; height: 18px; margin-left: 8px; }
570
+ .summary-title { font-weight: 600; color: #2f5a33; font-size: 0.95rem; }
571
+ .summary-text { color: #2f5a33; font-size: 0.85rem; line-height: 1.6; }
572
+
573
+ /* Tutorial Button */
574
+ .video-button-container { text-align: center; margin: 20px 0 15px 0; width: 100%; }
575
+ .elegant-video-button {
576
+ display: inline-flex !important;
577
+ align-items: center;
578
+ justify-content: center;
579
+ padding: 10px 20px !important;
580
+ background-color: #fff !important;
581
+ color: var(--guide-accent) !important;
582
+ border: 1px solid #e2e8f0 !important;
583
+ text-decoration: none;
584
+ border-radius: 50px !important;
585
+ font-weight: 600 !important;
586
+ font-size: 0.9rem !important;
587
+ cursor: pointer !important;
588
+ font-family: inherit;
589
+ transition: all 0.3s ease !important;
590
+ box-shadow: 0 2px 10px rgba(0,0,0,0.05) !important;
591
+ width: auto !important;
592
+ }
593
+ .elegant-video-button:hover {
594
+ background: var(--primary-gradient-guide) !important;
595
+ color: white !important;
596
+ border-color: transparent !important;
597
+ transform: translateY(-2px);
598
+ box-shadow: 0 6px 16px rgba(102, 126, 234, 0.3) !important;
599
+ }
600
+ .elegant-video-button-icon { width: 18px; height: 18px; margin-left: 8px; fill: currentColor; }
601
+
602
+ /* Action Buttons */
603
+ .guide-actions {
604
+ display: flex !important;
605
+ gap: 12px !important;
606
+ margin-top: 20px;
607
+ padding-top: 20px;
608
+ border-top: 1px solid #e2e8f0;
609
+ width: 100% !important;
610
+ }
611
+ .action-button {
612
+ padding: 12px 15px !important;
613
+ border: none !important;
614
+ border-radius: 12px !important;
615
+ font-size: 0.95rem !important;
616
+ font-weight: 600 !important;
617
+ cursor: pointer !important;
618
+ flex: 1 !important;
619
+ transition: all 0.3s ease !important;
620
+ display: flex !important;
621
+ align-items: center;
622
+ justify-content: center;
623
+ font-family: inherit;
624
+ height: 48px !important;
625
+ }
626
+ .action-button-icon { width: 20px; height: 20px; margin-right: 0; margin-left: 8px; }
627
+
628
+ .back-button {
629
+ background: white !important;
630
+ color: var(--guide-text-body) !important;
631
+ border: 2px solid #e2e8f0 !important;
632
+ }
633
+ .back-button:hover {
634
+ background: #f7fafc !important;
635
+ border-color: var(--guide-accent) !important;
636
+ transform: translateY(-2px);
637
+ box-shadow: var(--shadow-md) !important;
638
+ }
639
+
640
+ .retry-button {
641
+ background: var(--primary-gradient-guide) !important;
642
+ color: white !important;
643
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3) !important;
644
+ }
645
+ .retry-button:hover {
646
+ transform: translateY(-2px);
647
+ box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4) !important;
648
+ }
649
+
650
+ /* --- Main App CSS --- */
651
+ body {
652
+ font-family: 'Vazirmatn', sans-serif !important;
653
+ background-color: #f5f7fa !important;
654
+ margin: 0;
655
+ padding: 10px;
656
+ }
657
+
658
+ #col-container {
659
+ margin: 0 auto;
660
+ max-width: 980px;
661
+ direction: rtl;
662
+ text-align: right;
663
+ padding: 30px;
664
+ background: #ffffff !important;
665
+ border-radius: 24px;
666
+ box-shadow: 0 10px 40px -10px rgba(0,0,0,0.08);
667
+ border: 1px solid rgba(255,255,255,0.8);
668
+ }
669
+
670
+ #main-title h1 {
671
+ font-size: 2.4em !important;
672
+ text-align: center;
673
+ color: #1a202c !important;
674
+ margin-bottom: 15px;
675
+ font-weight: 800;
676
+ background: -webkit-linear-gradient(45deg, #2563eb, #1e40af);
677
+ -webkit-background-clip: text;
678
+ -webkit-text-fill-color: transparent;
679
+ }
680
+
681
+ #main-description {
682
  text-align: center;
683
+ font-size: 1.15em;
684
+ color: #4b5563 !important;
685
+ margin-bottom: 40px;
686
+ line-height: 1.6;
687
+ }
688
+
689
+ .gr-input-label, span.label-wrap, label span {
690
+ font-weight: 700 !important;
691
+ color: #374151 !important;
692
+ font-size: 0.95em !important;
693
+ margin-bottom: 8px !important;
694
+ }
695
+
696
+ textarea, input[type="text"] {
697
+ border: 2px solid #e2e8f0 !important;
698
+ border-radius: 12px !important;
699
+ background-color: #ffffff !important;
700
+ color: #111827 !important;
701
+ padding: 12px !important;
702
+ transition: all 0.3s ease;
703
+ font-family: 'Vazirmatn', sans-serif !important;
704
+ }
705
+
706
+ textarea:focus, input[type="text"]:focus {
707
+ border-color: #3b82f6 !important;
708
+ box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1) !important;
709
+ outline: none;
710
+ }
711
+
712
+ .gr-dropdown {
713
+ background: #ffffff !important;
714
+ border-radius: 12px !important;
715
+ }
716
+
717
+ .primary-btn, button.primary {
718
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important;
719
+ border: none !important;
720
+ color: white !important;
721
+ font-weight: 700 !important;
722
+ font-size: 1.1em !important;
723
+ padding: 14px 28px !important;
724
+ border-radius: 14px !important;
725
+ box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3) !important;
726
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
727
+ cursor: pointer !important;
728
+ width: 100%;
729
  margin-top: 15px;
730
+ }
731
+
732
+ .primary-btn:hover, button.primary:hover {
733
+ transform: translateY(-2px);
734
+ box-shadow: 0 8px 25px rgba(16, 185, 129, 0.45) !important;
735
+ }
736
+
737
+ .primary-btn:active, button.primary:active {
738
+ transform: translateY(1px);
739
+ }
740
+
741
+ #download-btn {
742
+ background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
743
+ box-shadow: 0 4px 15px rgba(59, 130, 246, 0.3) !important;
744
+ }
745
+ #download-btn:hover {
746
+ box-shadow: 0 8px 25px rgba(59, 130, 246, 0.45) !important;
747
+ }
748
+
749
+ .gradio-container .prose table,
750
+ .gradio-container table {
751
+ background-color: #ffffff !important;
752
+ color: #111827 !important;
753
+ border: 1px solid #e5e7eb !important;
754
+ border-radius: 12px !important;
755
+ overflow: hidden !important;
756
+ width: 100% !important;
757
+ margin-top: 20px !important;
758
+ }
759
+
760
+ .gradio-container thead th {
761
+ background-color: #f3f4f6 !important;
762
+ color: #374151 !important;
763
+ font-weight: 700 !important;
764
+ border-bottom: 2px solid #e5e7eb !important;
765
+ padding: 12px !important;
766
+ text-align: right !important;
767
+ }
768
+
769
+ .gradio-container tbody tr {
770
+ background-color: #ffffff !important;
771
+ border-bottom: 1px solid #f3f4f6 !important;
772
+ }
773
+
774
+ .gradio-container tbody tr:hover {
775
+ background-color: #f9fafb !important;
776
+ }
777
+
778
+ .gradio-container tbody td {
779
+ background-color: #ffffff !important;
780
+ color: #374151 !important;
781
+ padding: 10px !important;
782
+ }
783
+
784
+ .gradio-container tbody td span,
785
+ .gradio-container tbody td p {
786
+ color: #374151 !important;
787
+ }
788
+
789
+ footer { display: none !important; }
790
+ .flagging { display: none !important; }
791
+
792
+ /* Force toast transparency */
793
+ .toast-body {
794
+ direction: rtl !important;
795
+ text-align: right !important;
796
+ background: transparent !important;
797
+ box-shadow: none !important;
798
+ border: none !important;
799
+ padding: 0 !important;
800
+ max-width: 100% !important;
801
+ width: auto !important;
802
+ }
803
+ .toast-wrap {
804
+ background: transparent !important;
805
+ border: none !important;
806
  box-shadow: none !important;
 
807
  }
808
 
809
+ @media (prefers-color-scheme: dark) {
810
+ body, .gradio-container, .prose, table, tr, td, th {
811
+ background-color: #ffffff !important;
812
+ color: #333333 !important;
813
+ }
814
+ }
815
  </style>
816
  """
817
 
818
+ # ادغام CSS و JS
819
+ combined_html = css_code + js_global_content
820
+
821
  # استفاده از gr.Blocks
822
+ with gr.Blocks() as demo:
823
+ # تزریق کدها به عنوان HTML
824
+ gr.HTML(combined_html)
825
 
826
  with gr.Column(elem_id="col-container"):
827
  gr.Markdown("# **ویرایشگر هوشمند آلفا**", elem_id="main-title")
828
+ gr.Markdown(
829
+ "با هوش مصنوعی آلفا تصاویر تونو به مدل های مختلف ویرایش کنید.",
830
+ elem_id="main-description"
831
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
832
 
833
  with gr.Row(equal_height=True):
834
  with gr.Column():
835
  input_image = gr.Image(label="بارگذاری تصویر", type="pil", height=320)
836
+
837
+ prompt = gr.Text(
838
+ label="دستور ویرایش (به فارسی)",
839
+ show_label=True,
840
+ placeholder="مثال: تصویر را به سبک انیمه تبدیل کن...",
841
+ rtl=True,
842
+ lines=3
843
+ )
844
+
845
  status_box = gr.HTML(label="وضعیت")
846
+
847
+ # Checkbox مخفی برای انتقال وضعیت VIP از جاوااسکریپت به پایتون
848
+ is_vip_state = gr.Checkbox(visible=False, value=False, label="VIP State")
849
+
850
+ # IMPORTANT: Added elem_id="run-btn" here for JS targeting
851
  run_button = gr.Button("✨ شروع پردازش و ساخت تصویر", variant="primary", elem_classes="primary-btn", elem_id="run-btn")
852
 
853
  with gr.Column():
854
  output_image = gr.Image(label="تصویر نهایی", interactive=False, format="png", height=380)
855
+
856
  download_button = gr.Button("📥 دانلود و ذخیره تصویر", variant="secondary", elem_id="download-btn", elem_classes="primary-btn")
 
857
 
858
+ with gr.Row():
859
+ lora_adapter = gr.Dropdown(
860
+ label="انتخاب سبک ویرایش (LoRA)",
861
+ choices=list(LORA_MAPPING.keys()),
862
+ value="تبدیل عکس به انیمه"
863
+ )
864
+
865
+ with gr.Accordion("تنظیمات پیشرفته", open=False, visible=True):
866
+ aspect_ratio_selection = gr.Dropdown(
867
+ label="ابعاد تصویر خروجی",
868
+ choices=ASPECT_RATIOS_LIST,
869
+ value="خودکار (پیش‌فرض)",
870
+ interactive=True
871
+ )
872
+
873
  with gr.Row(visible=False) as custom_dims_row:
874
+ custom_width = gr.Slider(
875
+ label="عرض دلخواه (Width)",
876
+ minimum=256, maximum=2048, step=8, value=1024
877
+ )
878
+ custom_height = gr.Slider(
879
+ label="ارتفاع دلخواه (Height)",
880
+ minimum=256, maximum=2048, step=8, value=1024
881
+ )
882
+
883
  seed = gr.Slider(label="دانه تصادفی (Seed)", minimum=0, maximum=MAX_SEED, step=1, value=0)
884
  randomize_seed = gr.Checkbox(label="استفاده از Seed تصادفی", value=True)
885
+ guidance_scale = gr.Slider(label="میزان وفاداری به متن (Guidance Scale)", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
886
+ steps = gr.Slider(label="تعداد مراحل پردازش (Steps)", minimum=1, maximum=50, step=1, value=4)
887
 
888
+ # اصلاح تابع نمایش ردیف اسلایدرها
889
  def toggle_row(choice):
890
+ if choice == "شخصی‌سازی (Custom)":
891
+ return gr.update(visible=True)
892
+ return gr.update(visible=False)
893
+
894
+ aspect_ratio_selection.change(
895
+ fn=toggle_row,
896
+ inputs=aspect_ratio_selection,
897
+ outputs=custom_dims_row
898
+ )
899
 
900
  gr.Examples(
901
+ examples=[
902
+ ["examples/1.jpg", "تبدیل به انیمه کن.", "تبدیل عکس به انیمه"],
903
+ ["examples/5.jpg", "سایه‌ها را حذف کن و نورپردازی نرم به تصویر بده.", "اصلاح نور و سایه"],
904
+ ["examples/4.jpg", "از فیلتر ساعت طلایی با پخش نور ملایم استفاده کن.", "نورپردازی مجدد (Relight)"],
905
+ ["examples/2.jpeg", "دوربین را ۴۵ درجه به سمت چپ بچرخان.", "تغییر زاویه دید"],
906
+ ["examples/7.jpg", "منبع نور را از سمت راست عقب قرار بده.", "نورپردازی چند زاویه‌ای"],
907
+ ["examples/10.jpeg", "کیفیت تصویر را افزایش بده (Upscale).", "افزایش کیفیت (Upscale)"],
908
+ ["examples/7.jpg", "منبع نور را از پایین بتابان.", "نورپردازی چند زاویه‌ای"],
909
+ ["examples/2.jpeg", "زاویه دوربین را به نمای بالا گوشه راست تغییر بده.", "تغییر زاویه دید"],
910
+ ["examples/9.jpg", "دوربین کمی به جلو حرکت می‌کند در حالی که نور خورشید از میان ابرها می‌تابد و درخششی نرم اطراف شبح شخصیت در مه ایجاد می‌کند. سبک سینمایی واقعی.", "صحنه بعدی (سینمایی)"],
911
+ ["examples/8.jpg", "جزئیات پوست سوژه را برجسته‌تر و طبیعی‌تر کن.", "روتوش پوست"],
912
+ ["examples/6.jpg", "دوربین را به نمای پایین به بالا تغییر بده.", "تغییر زاویه دید"],
913
+ ],
914
+ inputs=[input_image, prompt, lora_adapter],
915
+ outputs=[output_image, seed, status_box],
916
+ fn=infer_example,
917
+ cache_examples=False,
918
+ label="نمونه‌ها (برای تست کلیک کنید)"
919
  )
920
 
921
+ # جاوااسکریپت برای تزریق متغیر سراسری JS به ورودی Checkbox پایتون
922
+ # این تابع مقدار window.userIsVip را می‌خواند و به عنوان آخرین آرگومان به تابع پایتون می‌فرستد
923
+ inject_vip_js = """
924
+ (img, p, lora, seed, rand, scale, steps, aspect, w, h, vip_dummy) => {
925
+ return [img, p, lora, seed, rand, scale, steps, aspect, w, h, window.userIsVip];
926
+ }
927
+ """
928
+
929
  run_button.click(
930
  fn=infer,
931
+ inputs=[input_image, prompt, lora_adapter, seed, randomize_seed, guidance_scale, steps, aspect_ratio_selection, custom_width, custom_height, is_vip_state],
932
  outputs=[output_image, seed, status_box],
933
+ js=inject_vip_js,
934
  api_name="predict"
935
  )
 
 
 
 
 
 
 
936
 
937
+ download_button.click(
 
938
  fn=None,
939
+ inputs=[output_image],
940
+ outputs=None,
941
+ js=js_download_func
 
 
 
 
 
 
 
 
942
  )
 
 
 
 
 
943
 
944
  if __name__ == "__main__":
945
  demo.queue(max_size=30).launch(show_error=True)