Opera8 commited on
Commit
ca97deb
·
verified ·
1 Parent(s): 7a1f3e6

Update app.py

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