Opera8 commited on
Commit
f02a91d
·
verified ·
1 Parent(s): c3ba67c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +148 -426
app.py CHANGED
@@ -33,10 +33,10 @@ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
33
 
34
  # --- تنظیمات سیستم اعتبار ---
35
  USAGE_LIMIT = 5
36
- DATA_FILE = "usage_data.json" # فایل ذخیره اطلاعات
37
  PREMIUM_PAGE_ID = '1149636'
38
 
39
- # --- توابع مدیریت فایل JSON ---
40
  def load_usage_data():
41
  if os.path.exists(DATA_FILE):
42
  try:
@@ -53,10 +53,10 @@ def save_usage_data(data):
53
  except Exception as e:
54
  print(f"Error saving data: {e}")
55
 
56
- # بارگذاری اولیه داده‌ها
57
  usage_data_cache = load_usage_data()
58
 
59
- # --- بارگذاری مدل تشخیص محتوای نامناسب (NSFW) ---
60
  print("Loading Safety Checker...")
61
  safety_classifier = pipeline("image-classification", model="Falconsai/nsfw_image_detection", device=-1)
62
 
@@ -117,268 +117,190 @@ LORA_MAPPING = {
117
  "افزایش کیفیت (Upscale)": "upscale-image"
118
  }
119
 
120
- ASPECT_RATIOS_LIST = [
121
- "خودکار (پیش‌فرض)",
122
- "۱:۱ (مربع - 1024x1024)",
123
- "۱۶:۹ (افقی - 1344x768)",
124
- "۹:۱۶ (عمودی - 768x1344)",
125
- "شخصی‌سازی (Custom)"
126
- ]
127
-
128
- ASPECT_RATIOS_MAP = {
129
- "خودکار (پیش‌فرض)": "Auto",
130
- "۱:۱ (مربع - 1024x1024)": (1024, 1024),
131
- "۱۶:۹ (افقی - 1344x768)": (1344, 768),
132
- "۹:۱۶ (عمودی - 768x1344)": (768, 1344),
133
- "شخصی‌سازی (Custom)": "Custom"
134
- }
135
 
136
- BANNED_WORDS = [
137
- "nude", "naked", "sex", "porn", "undressed", "nsfw", "erotic", "xxx",
138
- "breast", "nipple", "genital", "vagina", "penis", "ass", "butt", "sexual",
139
- "lingerie", "bikini", "swimwear", "underwear", "fetish", "topless",
140
- "exhibitionism", "hentai", "ecchi", "18+"
141
- ]
142
 
143
  def check_text_safety(text):
144
  text_lower = text.lower()
145
  for word in BANNED_WORDS:
146
- if f" {word} " in f" {text_lower} ":
147
- return False
148
  return True
149
 
150
  def translate_prompt(text):
151
- if not text:
152
- return ""
153
- try:
154
- translated = GoogleTranslator(source='auto', target='en').translate(text)
155
- return translated
156
- except Exception as e:
157
- print(f"Translation Error: {e}")
158
- return text
159
 
160
  def update_dimensions_on_upload(image):
161
- if image is None:
162
- return 1024, 1024
163
- original_width, original_height = image.size
164
- if original_width > original_height:
165
- new_width = 1024
166
- aspect_ratio = original_height / original_width
167
- new_height = int(new_width * aspect_ratio)
168
- else:
169
- new_height = 1024
170
- aspect_ratio = original_width / original_height
171
- new_width = int(new_height * aspect_ratio)
172
- new_width = (new_width // 8) * 8
173
- new_height = (new_height // 8) * 8
174
- return new_width, new_height
175
-
176
- def update_sliders_visibility(choice):
177
- if choice == "شخصی‌سازی (Custom)":
178
- return gr.update(visible=True), gr.update(visible=True)
179
- else:
180
- return gr.update(visible=False), gr.update(visible=False)
181
 
182
  def get_error_html(message):
183
  return f"""
184
  <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;">
185
- <span style="font-size: 1.2em;">⛔</span>
186
- {message}
187
- </div>
188
- """
189
 
190
  def get_success_html(message):
191
  return f"""
192
  <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;">
193
- <span style="font-size: 1.2em;">✅</span>
194
- {message}
 
 
 
 
 
 
 
195
  </div>
196
  """
197
 
198
  def check_quota(fingerprint, subscription_status):
199
- """
200
- بررسی اعتبار کاربر.
201
- """
202
  global usage_data_cache
 
203
 
204
- # لاگ برای دیباگ
205
- print(f"CHECK QUOTA: FP={fingerprint}, STATUS={subscription_status}")
206
-
207
- if subscription_status == 'paid':
208
- return True, "unlimited"
209
-
210
- if not fingerprint or fingerprint == "unknown":
211
- # اگر فینگرپرینت نرسید، موقتا اجازه ندهیم یا یک شناسه موقت بسازیم
212
- # اینجا فرض می‌کنیم کاربر ناشناس است
213
- print("Warning: No fingerprint received.")
214
- return True, 0
215
 
216
  today_str = date.today().isoformat()
217
-
218
- # رفرش کردن دیتا از فایل (در صورت تغییر توسط ترد دیگر)
219
  usage_data_cache = load_usage_data()
220
-
221
  user_record = usage_data_cache.get(fingerprint)
222
 
223
- # اگر کاربر جدید است یا روز عوض شده
224
  if not user_record or user_record.get("last_reset") != today_str:
225
  user_record = {"count": 0, "last_reset": today_str}
226
  usage_data_cache[fingerprint] = user_record
227
 
228
- # بررسی محدودیت
229
  if user_record["count"] >= USAGE_LIMIT:
230
  return False, user_record["count"]
231
 
232
- # کسر اعتبار
233
  user_record["count"] += 1
234
-
235
- # ذخیره در فایل
236
  usage_data_cache[fingerprint] = user_record
237
  save_usage_data(usage_data_cache)
238
-
239
  return True, user_record["count"]
240
 
241
-
242
  @spaces.GPU(duration=30)
243
  def infer(
244
- input_image,
245
- prompt,
246
- lora_adapter_persian,
247
- seed,
248
- randomize_seed,
249
- guidance_scale,
250
- steps,
251
- aspect_ratio_selection,
252
- custom_width,
253
- custom_height,
254
- fingerprint,
255
- subscription_status,
256
- progress=gr.Progress(track_tqdm=True)
257
  ):
258
- print(f"INFER CALLED: Fingerprint='{fingerprint}', Sub='{subscription_status}'")
259
-
260
  # --- بررسی اعتبار ---
261
  is_allowed, usage_count = check_quota(fingerprint, subscription_status)
262
 
 
263
  if not is_allowed:
264
- msg = "شما به محدودیت ۵ تصویر رایگان روزانه خود رسیده‌اید. لطفاً برای استفاده نامحدود حساب خود را ارتقا دهید."
265
- return None, seed, get_error_html(msg)
 
 
 
 
 
 
266
 
 
267
  if input_image is None:
268
- return None, seed, get_error_html("لطفاً ابتدا یک تصویر بارگذاری کنید.")
269
 
270
  if is_image_nsfw(input_image):
271
- return None, seed, get_error_html("تصویر ورودی دارای محتوای نامناسب است و پردازش نمی‌شود.")
272
 
273
  english_prompt = translate_prompt(prompt)
274
  if not check_text_safety(english_prompt):
275
- return None, seed, get_error_html("متن درخواست شامل کلمات غیرمجاز یا غیراخلاقی است.")
276
 
277
  adapter_internal_name = LORA_MAPPING.get(lora_adapter_persian)
278
- if adapter_internal_name:
279
- pipe.set_adapters([adapter_internal_name], adapter_weights=[1.0])
280
 
281
- if randomize_seed:
282
- seed = random.randint(0, MAX_SEED)
283
-
284
  generator = torch.Generator(device=device).manual_seed(seed)
285
 
286
  safety_negative = "nsfw, nude, naked, porn, sexual, xxx, breast, nipple, genital, vagina, penis, ass, lingerie, bikini, swimwear, underwear, fetish, topless, gore, violence, blood"
287
  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"
288
- final_negative_prompt = f"{safety_negative}, {base_negative}"
289
-
290
- original_image = input_image.convert("RGB")
291
 
 
292
  selection_value = ASPECT_RATIOS_MAP.get(aspect_ratio_selection)
293
-
294
- if selection_value == "Custom":
295
- width = (int(custom_width) // 8) * 8
296
- height = (int(custom_height) // 8) * 8
297
- elif selection_value == "Auto" or selection_value is None:
298
- width, height = update_dimensions_on_upload(original_image)
299
- else:
300
- width, height = selection_value
301
 
302
  try:
303
  result = pipe(
304
- image=original_image,
305
- prompt=english_prompt,
306
- negative_prompt=final_negative_prompt,
307
- height=height,
308
- width=width,
309
- num_inference_steps=steps,
310
- generator=generator,
311
- true_cfg_scale=guidance_scale,
312
  ).images[0]
313
 
314
  if is_image_nsfw(result):
315
- return None, seed, get_error_html("تصویر تولید شده حاوی محتوای نامناسب بود و حذف شد.")
316
 
317
- success_msg = f"تصویر با موفقیت ویرایش شد."
318
- if subscription_status != 'paid':
319
- success_msg += f" (اعتبار باقی‌مانده امروز: {USAGE_LIMIT - usage_count})"
320
 
321
- return result, seed, get_success_html(success_msg)
322
 
323
  except Exception as e:
324
- error_str = str(e)
325
- if "quota" in error_str.lower() or "exceeded" in error_str.lower():
326
- raise e
327
- return None, seed, get_error_html(f"خطا در پردازش: {error_str}")
328
 
329
  @spaces.GPU(duration=30)
330
  def infer_example(input_image, prompt, lora_adapter):
331
- res, s, status = infer(input_image, prompt, lora_adapter, 0, True, 1.0, 4, "خودکار (پیش‌فرض)", 1024, 1024, "example_user", "paid")
 
332
  return res, s, status
333
 
334
- # --- جاوااسکریپت برای دانلود ---
335
  js_download_func = """
336
  async (image) => {
337
- if (!image) {
338
- alert("لطفاً ابتدا تصویر را تولید کنید.");
339
- return;
340
- }
341
  let fileUrl = image.url;
342
- if (fileUrl && !fileUrl.startsWith('http')) {
343
- fileUrl = window.location.origin + fileUrl;
344
- } else if (!fileUrl && image.path) {
345
- fileUrl = window.location.origin + "/file=" + image.path;
346
- }
347
- window.parent.postMessage({
348
- type: 'DOWNLOAD_REQUEST',
349
- url: fileUrl
350
- }, '*');
 
351
  }
352
  """
353
 
354
- # --- جاوااسکریپت جامع ---
355
  js_global_content = """
356
  <script>
357
  document.addEventListener('DOMContentLoaded', () => {
358
  // ---------------------------------------------
359
- // 1. Fingerprint & Subscription Logic
360
  // ---------------------------------------------
361
-
362
  async function getBrowserFingerprint() {
363
- const components = [
364
- navigator.userAgent,
365
- navigator.language,
366
- screen.width + 'x' + screen.height,
367
- new Date().getTimezoneOffset()
368
- ];
 
 
 
369
  try {
370
  const canvas = document.createElement('canvas');
371
  const ctx = canvas.getContext('2d');
372
  ctx.textBaseline = "top"; ctx.font = "14px 'Arial'"; ctx.textBaseline = "alphabetic";
373
  ctx.fillStyle = "#f60"; ctx.fillRect(125, 1, 62, 20);
374
- ctx.fillStyle = "#069"; ctx.fillText("a1b2c3d4e5f6g7h8i9j0_!@#$%^&*()", 2, 15);
375
  components.push(canvas.toDataURL());
376
- } catch (e) { components.push("canvas-error"); }
377
 
378
- const fingerprintString = components.join('~~~');
 
379
  let hash = 0;
380
- for (let i = 0; i < fingerprintString.length; i++) {
381
- const char = fingerprintString.charCodeAt(i);
382
  hash = ((hash << 5) - hash) + char;
383
  hash |= 0;
384
  }
@@ -398,21 +320,13 @@ document.addEventListener('DOMContentLoaded', () => {
398
  function updateHiddenInputs(fingerprint, status) {
399
  const fpInput = document.querySelector('#fingerprint_storage textarea');
400
  const stInput = document.querySelector('#status_storage textarea');
401
-
402
- if(fpInput) {
403
- fpInput.value = fingerprint;
404
- fpInput.dispatchEvent(new Event('input', { bubbles: true }));
405
- }
406
- if(stInput) {
407
- stInput.value = status;
408
- stInput.dispatchEvent(new Event('input', { bubbles: true }));
409
- }
410
  }
411
 
412
  function updateSubscriptionBadge(status) {
413
  const badge = document.getElementById('user-sub-badge');
414
  if (!badge) return;
415
-
416
  if (status === 'paid') {
417
  badge.innerHTML = '✨ اشتراک: <span style="color: #FFD700; font-weight: bold;">نامحدود (PRO)</span>';
418
  badge.style.background = 'linear-gradient(45deg, #1e3a8a, #3b82f6)';
@@ -421,317 +335,125 @@ document.addEventListener('DOMContentLoaded', () => {
421
  badge.style.background = 'linear-gradient(45deg, #4b5563, #6b7280)';
422
  }
423
  badge.style.display = 'inline-block';
424
-
425
- // اطمینان از آپدیت شدن اینپوت‌ها هر بار که وضعیت تغییر می‌کند
426
- updateHiddenInputs(window.userFingerprint || 'unknown', status);
427
  }
428
 
429
  async function initUserIdentity() {
430
  window.userFingerprint = await getBrowserFingerprint();
431
- window.userStatus = 'free'; // Default
432
-
433
  window.parent.postMessage({ type: 'REQUEST_USER_STATUS' }, '*');
434
  updateSubscriptionBadge('free');
435
 
436
- // Polling to ensure inputs are populated even if DOM is slow
437
  setInterval(() => {
438
- updateHiddenInputs(window.userFingerprint || 'unknown', window.userStatus || 'free');
439
- }, 2000);
440
  }
441
 
442
  window.addEventListener('message', (event) => {
443
  if (event.data && event.data.type === 'USER_STATUS_RESPONSE') {
444
  try {
445
- const userObject = typeof event.data.payload === 'string'
446
- ? JSON.parse(event.data.payload)
447
- : event.data.payload;
448
-
449
  const status = isUserPaid(userObject) ? 'paid' : 'free';
450
  window.userStatus = status;
451
  updateSubscriptionBadge(status);
452
- } catch (e) {
453
- console.error("Error parsing user status:", e);
454
- updateSubscriptionBadge('free');
455
- }
456
  }
457
  });
458
 
459
  initUserIdentity();
460
 
461
- // ---------------------------------------------
462
- // 2. UI Helper
463
- // ---------------------------------------------
464
  const forceLight = () => {
465
  const body = document.querySelector('body');
466
- if (body) {
467
- body.classList.remove('dark');
468
- body.style.backgroundColor = '#f5f7fa';
469
- body.style.color = '#333333';
470
- }
471
  document.querySelectorAll('.dark').forEach(el => el.classList.remove('dark'));
472
  };
473
- forceLight();
474
- setInterval(forceLight, 1000);
475
-
476
- window.closeErrorModal = function() {
477
- const modal = document.getElementById('custom-quota-modal');
478
- if (modal) modal.remove();
479
- };
480
-
481
- const showQuotaModal = () => {
482
- if (document.getElementById('custom-quota-modal')) return;
483
- const modalHtml = `
484
- <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;">
485
- <div class="ip-reset-guide-container">
486
- <div class="guide-header">
487
- <div style="font-size: 2rem;">💎</div>
488
- <div>
489
- <h2>پایان اعتبار رایگان امروز</h2>
490
- <p>برای ادامه، حساب خود را ارتقا دهید.</p>
491
- </div>
492
- </div>
493
- <div class="guide-content">
494
- <div class="info-card" style="border-color: #fbbf24; background: #fffbeb;">
495
- <p>شما از ۵ اعتبار رایگان امروز خود استفاده کرده‌اید.</p>
496
- </div>
497
- <div class="video-button-container">
498
- <button onclick="parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM' }, '*')" class="elegant-video-button" style="background: linear-gradient(135deg, #f59e0b, #d97706) !important; color: white !important;">
499
- <span>⭐️ خرید نسخه نامحدود</span>
500
- </button>
501
- </div>
502
- </div>
503
- <div class="guide-actions">
504
- <button class="action-button back-button" onclick="window.closeErrorModal()">
505
- <span>بستن</span>
506
- </button>
507
- </div>
508
- </div>
509
- </div>
510
- `;
511
- document.body.insertAdjacentHTML('beforeend', modalHtml);
512
- };
513
-
514
- setInterval(() => {
515
- const potentialErrors = document.querySelectorAll('.toast-body, .error, .toast-wrap, .eta-bar, div[class*="error"]');
516
- potentialErrors.forEach(el => {
517
- const text = el.innerText || "";
518
- if (text.includes('محدودیت') && text.includes('رایگان')) {
519
- showQuotaModal();
520
- el.style.display = 'none';
521
- const parentWrap = el.closest('.toast-wrap');
522
- if(parentWrap) parentWrap.style.display = 'none';
523
- }
524
- });
525
- }, 500);
526
  });
527
  </script>
528
  """
529
 
530
- # --- CSS Updated ---
531
  css_code = """
532
  <style>
533
  @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700&display=swap');
534
-
535
- :root, .dark, body, .gradio-container {
536
- --body-background-fill: #f5f7fa !important;
537
- --body-text-color: #1f2937 !important;
538
- font-family: 'Vazirmatn', sans-serif !important;
539
- }
540
-
541
- /* Hide the storage inputs but keep them in DOM */
542
- #fingerprint_storage, #status_storage {
543
- display: none !important;
544
- /* We use display:none with !important in CSS instead of visible=False in Python
545
- to ensure the element exists in DOM for JS to find it. */
546
- }
547
-
548
- /* Modal CSS */
549
- .ip-reset-guide-container {
550
- text-align: right;
551
- direction: rtl;
552
- background: #fff;
553
- padding: 25px;
554
- border-radius: 16px;
555
- box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
556
- width: 90%;
557
- max-width: 400px;
558
- font-family: 'Vazirmatn', sans-serif !important;
559
- }
560
- .guide-header { display: flex; align-items: center; margin-bottom: 15px; gap: 15px; }
561
- .guide-header h2 { margin: 0; font-size: 1.2rem; color: #1f2937; }
562
- .guide-content { font-size: 0.95rem; color: #4b5563; line-height: 1.6; }
563
- .info-card { padding: 12px; border-radius: 8px; border: 1px solid #e5e7eb; margin-bottom: 15px; }
564
- .elegant-video-button {
565
- display: inline-flex; width: 100%; justify-content: center; padding: 12px;
566
- border-radius: 12px; border: none; font-weight: bold; cursor: pointer; transition: transform 0.2s;
567
- }
568
- .elegant-video-button:hover { transform: translateY(-2px); }
569
- .action-button { width: 100%; padding: 10px; border-radius: 8px; border: 1px solid #d1d5db; background: white; cursor: pointer; }
570
-
571
- /* Main App Styles */
572
- #col-container {
573
- margin: 0 auto;
574
- max-width: 980px;
575
- direction: rtl;
576
- text-align: right;
577
- padding: 30px;
578
- background: #ffffff !important;
579
- border-radius: 24px;
580
- box-shadow: 0 10px 40px -10px rgba(0,0,0,0.08);
581
- border: 1px solid rgba(255,255,255,0.8);
582
- }
583
- #main-title h1 {
584
- font-size: 2.4em !important;
585
- text-align: center;
586
- color: #1a202c !important;
587
- margin-bottom: 5px;
588
- font-weight: 800;
589
- background: -webkit-linear-gradient(45deg, #2563eb, #1e40af);
590
- -webkit-background-clip: text;
591
- -webkit-text-fill-color: transparent;
592
- }
593
- #main-description {
594
- text-align: center;
595
- font-size: 1.15em;
596
- color: #4b5563 !important;
597
- margin-bottom: 10px;
598
- }
599
- #badge-container {
600
- text-align: center;
601
- margin-bottom: 30px;
602
- height: 30px;
603
- }
604
- .primary-btn {
605
- background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important;
606
- border: none !important;
607
- color: white !important;
608
- font-weight: 700 !important;
609
- padding: 14px 28px !important;
610
- border-radius: 14px !important;
611
- }
612
  </style>
613
  """
614
 
615
- combined_html = css_code + js_global_content
616
-
617
  with gr.Blocks() as demo:
618
- gr.HTML(combined_html)
619
-
620
- # تغییر مهم: visible=True قرار دادیم تا در DOM باشد، اما با CSS مخفی کردیم
621
  fingerprint_box = gr.Textbox(elem_id="fingerprint_storage", visible=True)
622
  status_box_input = gr.Textbox(elem_id="status_storage", visible=True)
623
 
624
  with gr.Column(elem_id="col-container"):
625
  gr.Markdown("# **ویرایشگر هوشمند آلفا**", elem_id="main-title")
626
- gr.Markdown(
627
- "با هوش مصنوعی آلفا تصاویر تونو به مدل های مختلف ویرایش کنید.",
628
- elem_id="main-description"
629
- )
630
-
631
- # محل قرارگیری بج وضعیت اشتراک
632
  gr.HTML('<div id="badge-container"><span id="user-sub-badge"></span></div>')
633
 
634
  with gr.Row(equal_height=True):
635
  with gr.Column():
636
  input_image = gr.Image(label="بارگذاری تصویر", type="pil", height=320)
637
-
638
- prompt = gr.Text(
639
- label="دستور ویرایش (به فارس��)",
640
- show_label=True,
641
- placeholder="مثال: تصویر را به سبک انیمه تبدیل کن...",
642
- rtl=True,
643
- lines=3
644
- )
645
-
646
  status_box = gr.HTML(label="وضعیت")
647
 
648
- run_button = gr.Button("✨ شروع پردازش و ساخت تصویر", variant="primary", elem_classes="primary-btn", elem_id="run-btn")
 
 
 
 
649
 
650
  with gr.Column():
651
  output_image = gr.Image(label="تصویر نهایی", interactive=False, format="png", height=380)
652
-
653
  download_button = gr.Button("📥 دانلود و ذخیره تصویر", variant="secondary", elem_id="download-btn", elem_classes="primary-btn")
654
-
655
  with gr.Row():
656
- lora_adapter = gr.Dropdown(
657
- label="انتخاب سبک ویرایش (LoRA)",
658
- choices=list(LORA_MAPPING.keys()),
659
- value="تبدیل عکس به انیمه"
660
- )
661
-
662
- with gr.Accordion("تنظیمات پیشرفته", open=False, visible=True):
663
- aspect_ratio_selection = gr.Dropdown(
664
- label="ابعاد تصویر خروجی",
665
- choices=ASPECT_RATIOS_LIST,
666
- value="خودکار (پیش‌فرض)",
667
- interactive=True
668
- )
669
-
670
  with gr.Row(visible=False) as custom_dims_row:
671
- custom_width = gr.Slider(
672
- label="عرض دلخواه (Width)",
673
- minimum=256, maximum=2048, step=8, value=1024
674
- )
675
- custom_height = gr.Slider(
676
- label="ارتفاع دلخواه (Height)",
677
- minimum=256, maximum=2048, step=8, value=1024
678
- )
679
-
680
- seed = gr.Slider(label="دانه تصادفی (Seed)", minimum=0, maximum=MAX_SEED, step=1, value=0)
681
- randomize_seed = gr.Checkbox(label="استفاده از Seed تصادفی", value=True)
682
- guidance_scale = gr.Slider(label="میزان وفاداری به متن (Guidance Scale)", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
683
- steps = gr.Slider(label="تعداد مراحل پردازش (Steps)", minimum=1, maximum=50, step=1, value=4)
684
 
685
- def toggle_row(choice):
686
- if choice == "شخصی‌سازی (Custom)":
687
- return gr.update(visible=True)
688
- return gr.update(visible=False)
689
-
690
- aspect_ratio_selection.change(
691
- fn=toggle_row,
692
- inputs=aspect_ratio_selection,
693
- outputs=custom_dims_row
694
  )
695
-
 
 
 
 
 
 
696
  gr.Examples(
697
  examples=[
698
  ["examples/1.jpg", "تبدیل به انیمه کن.", "تبدیل عکس به انیمه"],
699
- ["examples/5.jpg", "سایه‌ها را حذف کن و نورپردازی نرم به تصویر بده.", "اصلاح نور و سایه"],
700
- ["examples/4.jpg", "از فیلتر ساعت طلایی با پخش نور ملایم استفاده کن.", "نورپردازی مجدد (Relight)"],
701
- ["examples/2.jpeg", "دوربین را ۴۵ درجه به سمت چپ بچرخان.", "تغییر زاویه دید"],
702
- ["examples/7.jpg", "منبع نور را از سمت راست عقب قرار بده.", "نورپردازی چند زاویه‌ای"],
703
- ["examples/10.jpeg", "کیفیت تصویر را افزایش بده (Upscale).", "افزایش کیفیت (Upscale)"],
704
- ["examples/7.jpg", "منبع نور را از پایین بتابان.", "نورپردازی چند زاویه‌ای"],
705
- ["examples/2.jpeg", "زاویه دوربین را به نمای بالا گوشه راست تغییر بده.", "تغییر زاویه دید"],
706
- ["examples/9.jpg", "دوربین کمی به جلو حرکت می‌کند در حالی که نور خورشید از میان ابرها می‌تابد و درخششی نرم اطراف شبح شخصیت در مه ایجاد می‌کند. سبک سینمایی واقعی.", "صحنه بعدی (سینمایی)"],
707
- ["examples/8.jpg", "جزئیات پوست سوژه را برجسته‌تر و طبیعی‌تر کن.", "روتوش پوست"],
708
- ["examples/6.jpg", "دوربین را به نمای پایین به بالا تغییر بده.", "تغییر زاویه دید"],
709
  ],
710
  inputs=[input_image, prompt, lora_adapter],
711
- outputs=[output_image, seed, status_box],
712
- fn=infer_example,
713
- cache_examples=False,
714
- label="نمونه‌ها (برای تست کلیک کنید)"
715
  )
716
 
717
- run_button.click(
718
- fn=infer,
719
- inputs=[
720
- input_image, prompt, lora_adapter, seed, randomize_seed,
721
- guidance_scale, steps, aspect_ratio_selection,
722
- custom_width, custom_height,
723
- fingerprint_box, status_box_input
724
- ],
725
- outputs=[output_image, seed, status_box],
726
- api_name="predict"
727
- )
728
-
729
- download_button.click(
730
- fn=None,
731
- inputs=[output_image],
732
- outputs=None,
733
- js=js_download_func
734
- )
735
-
736
  if __name__ == "__main__":
737
  demo.queue(max_size=30).launch(show_error=True)
 
33
 
34
  # --- تنظیمات سیستم اعتبار ---
35
  USAGE_LIMIT = 5
36
+ DATA_FILE = "usage_data.json"
37
  PREMIUM_PAGE_ID = '1149636'
38
 
39
+ # --- مدیریت ذخیره‌سازی داده‌ها ---
40
  def load_usage_data():
41
  if os.path.exists(DATA_FILE):
42
  try:
 
53
  except Exception as e:
54
  print(f"Error saving data: {e}")
55
 
56
+ # بارگذاری اولیه
57
  usage_data_cache = load_usage_data()
58
 
59
+ # --- بارگذاری مدل NSFW ---
60
  print("Loading Safety Checker...")
61
  safety_classifier = pipeline("image-classification", model="Falconsai/nsfw_image_detection", device=-1)
62
 
 
117
  "افزایش کیفیت (Upscale)": "upscale-image"
118
  }
119
 
120
+ ASPECT_RATIOS_LIST = ["خودکار (پیش‌فرض)", "۱:۱ (مربع - 1024x1024)", "۱۶:۹ (افقی - 1344x768)", "۹:۱۶ (عمودی - 768x1344)", "شخصی‌سازی (Custom)"]
121
+ ASPECT_RATIOS_MAP = {"خودکار (پیش‌فرض)": "Auto", "۱:۱ (مربع - 1024x1024)": (1024, 1024), "۱۶:۹ (افقی - 1344x768)": (1344, 768), "۹:۱۶ (عمودی - 768x1344)": (768, 1344), "شخصی‌سازی (Custom)": "Custom"}
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
+ 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+"]
 
 
 
 
 
124
 
125
  def check_text_safety(text):
126
  text_lower = text.lower()
127
  for word in BANNED_WORDS:
128
+ if f" {word} " in f" {text_lower} ": return False
 
129
  return True
130
 
131
  def translate_prompt(text):
132
+ if not text: return ""
133
+ try: return GoogleTranslator(source='auto', target='en').translate(text)
134
+ except: return text
 
 
 
 
 
135
 
136
  def update_dimensions_on_upload(image):
137
+ if image is None: return 1024, 1024
138
+ w, h = image.size
139
+ if w > h: new_w = 1024; ar = h / w; new_h = int(new_w * ar)
140
+ else: new_h = 1024; ar = w / h; new_w = int(new_h * ar)
141
+ return (new_w // 8) * 8, (new_h // 8) * 8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
  def get_error_html(message):
144
  return f"""
145
  <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;">
146
+ <span style="font-size: 1.2em;">⛔</span>{message}
147
+ </div>"""
 
 
148
 
149
  def get_success_html(message):
150
  return f"""
151
  <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;">
152
+ <span style="font-size: 1.2em;">✅</span>{message}
153
+ </div>"""
154
+
155
+ def get_quota_exceeded_html():
156
+ return """
157
+ <div style="background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%); border: 2px solid #f59e0b; padding: 20px; border-radius: 16px; text-align: center; box-shadow: 0 4px 15px rgba(245, 158, 11, 0.1);">
158
+ <div style="font-size: 3rem; margin-bottom: 10px;">💎</div>
159
+ <h3 style="color: #92400e; margin: 0 0 10px 0; font-weight: 800;">اعتبار رایگان امروز تمام شد</h3>
160
+ <p style="color: #b45309; margin: 0; font-size: 0.95em;">شما از ۵ تصویر رایگان امروز استفاده کرده‌اید.<br>برای ساخت تصاویر نامحدود و حرفه‌ای، لطفا نسخه خود را ارتقا دهید.</p>
161
  </div>
162
  """
163
 
164
  def check_quota(fingerprint, subscription_status):
 
 
 
165
  global usage_data_cache
166
+ if subscription_status == 'paid': return True, "unlimited"
167
 
168
+ # اگر فینگرپرینت معتبر نبود (مثلا کاربر جاوا اسکریپت را بست)، جلوگیری کنیم
169
+ if not fingerprint or len(fingerprint) < 5 or fingerprint == "unknown":
170
+ return True, 0 # برای بار اول سختگیری نمی‌کنیم تا لود شود
 
 
 
 
 
 
 
 
171
 
172
  today_str = date.today().isoformat()
 
 
173
  usage_data_cache = load_usage_data()
 
174
  user_record = usage_data_cache.get(fingerprint)
175
 
 
176
  if not user_record or user_record.get("last_reset") != today_str:
177
  user_record = {"count": 0, "last_reset": today_str}
178
  usage_data_cache[fingerprint] = user_record
179
 
 
180
  if user_record["count"] >= USAGE_LIMIT:
181
  return False, user_record["count"]
182
 
 
183
  user_record["count"] += 1
 
 
184
  usage_data_cache[fingerprint] = user_record
185
  save_usage_data(usage_data_cache)
 
186
  return True, user_record["count"]
187
 
 
188
  @spaces.GPU(duration=30)
189
  def infer(
190
+ input_image, prompt, lora_adapter_persian, seed, randomize_seed,
191
+ guidance_scale, steps, aspect_ratio_selection, custom_width, custom_height,
192
+ fingerprint, subscription_status, progress=gr.Progress(track_tqdm=True)
 
 
 
 
 
 
 
 
 
 
193
  ):
 
 
194
  # --- بررسی اعتبار ---
195
  is_allowed, usage_count = check_quota(fingerprint, subscription_status)
196
 
197
+ # اگر اعتبار تمام شده باشد:
198
  if not is_allowed:
199
+ # خروجی‌ها: تصویر=هیچ، سید=سید، پیام=پیام زیبا، دکمه‌اجرا=مخفی، دکمه‌ارتقا=نمایان
200
+ return (
201
+ None,
202
+ seed,
203
+ get_quota_exceeded_html(),
204
+ gr.update(visible=False), # Run Button -> Hide
205
+ gr.update(visible=True) # Upgrade Button -> Show
206
+ )
207
 
208
+ # --- اگر اعتبار دارد، ادامه پردازش ---
209
  if input_image is None:
210
+ return None, seed, get_error_html("لطفاً ابتدا یک تصویر بارگذاری کنید."), gr.update(visible=True), gr.update(visible=False)
211
 
212
  if is_image_nsfw(input_image):
213
+ return None, seed, get_error_html("تصویر ورودی نامناسب است."), gr.update(visible=True), gr.update(visible=False)
214
 
215
  english_prompt = translate_prompt(prompt)
216
  if not check_text_safety(english_prompt):
217
+ return None, seed, get_error_html("متن شامل کلمات غیرمجاز است."), gr.update(visible=True), gr.update(visible=False)
218
 
219
  adapter_internal_name = LORA_MAPPING.get(lora_adapter_persian)
220
+ if adapter_internal_name: pipe.set_adapters([adapter_internal_name], adapter_weights=[1.0])
 
221
 
222
+ if randomize_seed: seed = random.randint(0, MAX_SEED)
 
 
223
  generator = torch.Generator(device=device).manual_seed(seed)
224
 
225
  safety_negative = "nsfw, nude, naked, porn, sexual, xxx, breast, nipple, genital, vagina, penis, ass, lingerie, bikini, swimwear, underwear, fetish, topless, gore, violence, blood"
226
  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"
 
 
 
227
 
228
+ original_image = input_image.convert("RGB")
229
  selection_value = ASPECT_RATIOS_MAP.get(aspect_ratio_selection)
230
+ if selection_value == "Custom": w, h = (int(custom_width)//8)*8, (int(custom_height)//8)*8
231
+ elif selection_value == "Auto" or selection_value is None: w, h = update_dimensions_on_upload(original_image)
232
+ else: w, h = selection_value
 
 
 
 
 
233
 
234
  try:
235
  result = pipe(
236
+ image=original_image, prompt=english_prompt, negative_prompt=f"{safety_negative}, {base_negative}",
237
+ height=h, width=w, num_inference_steps=steps, generator=generator, true_cfg_scale=guidance_scale,
 
 
 
 
 
 
238
  ).images[0]
239
 
240
  if is_image_nsfw(result):
241
+ return None, seed, get_error_html("تصویر خروجی نامناسب بود حذف شد."), gr.update(visible=True), gr.update(visible=False)
242
 
243
+ success_msg = f"تصویر ساخته شد."
244
+ if subscription_status != 'paid': success_msg += f" (باقی‌مانده: {USAGE_LIMIT - usage_count})"
 
245
 
246
+ return result, seed, get_success_html(success_msg), gr.update(visible=True), gr.update(visible=False)
247
 
248
  except Exception as e:
249
+ return None, seed, get_error_html(f"خطا: {str(e)}"), gr.update(visible=True), gr.update(visible=False)
 
 
 
250
 
251
  @spaces.GPU(duration=30)
252
  def infer_example(input_image, prompt, lora_adapter):
253
+ # مثال‌ها اعتبار کم نمی‌کنند
254
+ res, s, status, btn1, btn2 = infer(input_image, prompt, lora_adapter, 0, True, 1.0, 4, "خودکار (پیش‌فرض)", 1024, 1024, "example", "paid")
255
  return res, s, status
256
 
 
257
  js_download_func = """
258
  async (image) => {
259
+ if (!image) { alert("تصویر موجود نیست."); return; }
 
 
 
260
  let fileUrl = image.url;
261
+ if (fileUrl && !fileUrl.startsWith('http')) fileUrl = window.location.origin + fileUrl;
262
+ else if (!fileUrl && image.path) fileUrl = window.location.origin + "/file=" + image.path;
263
+ window.parent.postMessage({ type: 'DOWNLOAD_REQUEST', url: fileUrl }, '*');
264
+ }
265
+ """
266
+
267
+ # جاوا اسکریپت برای دکمه ارتقا
268
+ js_upgrade_func = """
269
+ () => {
270
+ window.parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM' }, '*');
271
  }
272
  """
273
 
 
274
  js_global_content = """
275
  <script>
276
  document.addEventListener('DOMContentLoaded', () => {
277
  // ---------------------------------------------
278
+ // Fingerprint Logic (CANVAS + SCREEN + UA)
279
  // ---------------------------------------------
 
280
  async function getBrowserFingerprint() {
281
+ const components = [];
282
+ // 1. User Agent & Language
283
+ components.push(navigator.userAgent);
284
+ components.push(navigator.language);
285
+ // 2. Screen Params
286
+ components.push(screen.colorDepth);
287
+ components.push(screen.width + 'x' + screen.height);
288
+ components.push(new Date().getTimezoneOffset());
289
+ // 3. Canvas Fingerprinting (Rendering differences)
290
  try {
291
  const canvas = document.createElement('canvas');
292
  const ctx = canvas.getContext('2d');
293
  ctx.textBaseline = "top"; ctx.font = "14px 'Arial'"; ctx.textBaseline = "alphabetic";
294
  ctx.fillStyle = "#f60"; ctx.fillRect(125, 1, 62, 20);
295
+ ctx.fillStyle = "#069"; ctx.fillText("Alpha_AI_Fingerprint_v1_!@#", 2, 15);
296
  components.push(canvas.toDataURL());
297
+ } catch (e) { components.push("canvas-err"); }
298
 
299
+ // Hashing
300
+ const str = components.join('~~~');
301
  let hash = 0;
302
+ for (let i = 0; i < str.length; i++) {
303
+ const char = str.charCodeAt(i);
304
  hash = ((hash << 5) - hash) + char;
305
  hash |= 0;
306
  }
 
320
  function updateHiddenInputs(fingerprint, status) {
321
  const fpInput = document.querySelector('#fingerprint_storage textarea');
322
  const stInput = document.querySelector('#status_storage textarea');
323
+ if(fpInput && fingerprint) { fpInput.value = fingerprint; fpInput.dispatchEvent(new Event('input', { bubbles: true })); }
324
+ if(stInput && status) { stInput.value = status; stInput.dispatchEvent(new Event('input', { bubbles: true })); }
 
 
 
 
 
 
 
325
  }
326
 
327
  function updateSubscriptionBadge(status) {
328
  const badge = document.getElementById('user-sub-badge');
329
  if (!badge) return;
 
330
  if (status === 'paid') {
331
  badge.innerHTML = '✨ اشتراک: <span style="color: #FFD700; font-weight: bold;">نامحدود (PRO)</span>';
332
  badge.style.background = 'linear-gradient(45deg, #1e3a8a, #3b82f6)';
 
335
  badge.style.background = 'linear-gradient(45deg, #4b5563, #6b7280)';
336
  }
337
  badge.style.display = 'inline-block';
 
 
 
338
  }
339
 
340
  async function initUserIdentity() {
341
  window.userFingerprint = await getBrowserFingerprint();
342
+ window.userStatus = 'free';
 
343
  window.parent.postMessage({ type: 'REQUEST_USER_STATUS' }, '*');
344
  updateSubscriptionBadge('free');
345
 
346
+ // Polling to make sure hidden inputs are always filled
347
  setInterval(() => {
348
+ updateHiddenInputs(window.userFingerprint, window.userStatus);
349
+ }, 1500);
350
  }
351
 
352
  window.addEventListener('message', (event) => {
353
  if (event.data && event.data.type === 'USER_STATUS_RESPONSE') {
354
  try {
355
+ const userObject = typeof event.data.payload === 'string' ? JSON.parse(event.data.payload) : event.data.payload;
 
 
 
356
  const status = isUserPaid(userObject) ? 'paid' : 'free';
357
  window.userStatus = status;
358
  updateSubscriptionBadge(status);
359
+ } catch (e) { updateSubscriptionBadge('free'); }
 
 
 
360
  }
361
  });
362
 
363
  initUserIdentity();
364
 
365
+ // UI Force Light Mode
 
 
366
  const forceLight = () => {
367
  const body = document.querySelector('body');
368
+ if (body) { body.classList.remove('dark'); body.style.backgroundColor = '#f5f7fa'; body.style.color = '#333333'; }
 
 
 
 
369
  document.querySelectorAll('.dark').forEach(el => el.classList.remove('dark'));
370
  };
371
+ forceLight(); setInterval(forceLight, 1000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  });
373
  </script>
374
  """
375
 
 
376
  css_code = """
377
  <style>
378
  @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700&display=swap');
379
+ :root, .dark, body, .gradio-container { --body-background-fill: #f5f7fa !important; --body-text-color: #1f2937 !important; font-family: 'Vazirmatn', sans-serif !important; }
380
+ #fingerprint_storage, #status_storage { display: none !important; }
381
+ #badge-container { text-align: center; margin-bottom: 30px; height: 30px; }
382
+ #user-sub-badge { padding: 6px 16px; border-radius: 20px; font-size: 0.9em; color: white; margin-top: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); display: none; transition: all 0.3s ease; }
383
+ /* Button Styles */
384
+ .primary-btn { background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important; border: none !important; color: white !important; font-weight: 700 !important; padding: 14px 28px !important; border-radius: 14px !important; }
385
+ .upgrade-btn { background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%) !important; border: none !important; color: white !important; font-weight: 700 !important; padding: 14px 28px !important; border-radius: 14px !important; box-shadow: 0 4px 15px rgba(245, 158, 11, 0.4) !important; animation: pulse 2s infinite; }
386
+ @keyframes pulse { 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.7); } 70% { transform: scale(1.02); box-shadow: 0 0 0 10px rgba(245, 158, 11, 0); } 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(245, 158, 11, 0); } }
387
+ /* Main Layout */
388
+ #col-container { margin: 0 auto; max-width: 980px; direction: rtl; text-align: right; padding: 30px; background: #ffffff !important; border-radius: 24px; box-shadow: 0 10px 40px -10px rgba(0,0,0,0.08); border: 1px solid rgba(255,255,255,0.8); }
389
+ #main-title h1 { font-size: 2.4em !important; text-align: center; color: #1a202c !important; margin-bottom: 5px; 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: 10px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  </style>
392
  """
393
 
 
 
394
  with gr.Blocks() as demo:
395
+ gr.HTML(css_code + js_global_content)
 
 
396
  fingerprint_box = gr.Textbox(elem_id="fingerprint_storage", visible=True)
397
  status_box_input = gr.Textbox(elem_id="status_storage", visible=True)
398
 
399
  with gr.Column(elem_id="col-container"):
400
  gr.Markdown("# **ویرایشگر هوشمند آلفا**", elem_id="main-title")
401
+ gr.Markdown("با هوش مصنوعی آلفا تصاویر تونو به مدل های مختلف ویرایش کنید.", elem_id="main-description")
 
 
 
 
 
402
  gr.HTML('<div id="badge-container"><span id="user-sub-badge"></span></div>')
403
 
404
  with gr.Row(equal_height=True):
405
  with gr.Column():
406
  input_image = gr.Image(label="بارگذاری تصویر", type="pil", height=320)
407
+ prompt = gr.Text(label="دستور ویرایش", placeholder="مثال: تصویر را به سبک انیمه تبدیل کن...", rtl=True, lines=3)
 
 
 
 
 
 
 
 
408
  status_box = gr.HTML(label="وضعیت")
409
 
410
+ # --- دکمه‌ها ---
411
+ # دکمه ساخت (پیش‌فرض نمایان)
412
+ run_button = gr.Button("✨ شروع پردازش و ساخت تصویر", variant="primary", elem_classes="primary-btn", elem_id="run-btn", visible=True)
413
+ # دکمه ارتقا (پیش‌فرض مخفی)
414
+ upgrade_button = gr.Button("💎 خرید نسخه نامحدود (کلیک کنید)", variant="primary", elem_classes="upgrade-btn", elem_id="upgrade-btn", visible=False)
415
 
416
  with gr.Column():
417
  output_image = gr.Image(label="تصویر نهایی", interactive=False, format="png", height=380)
 
418
  download_button = gr.Button("📥 دانلود و ذخیره تصویر", variant="secondary", elem_id="download-btn", elem_classes="primary-btn")
 
419
  with gr.Row():
420
+ lora_adapter = gr.Dropdown(label="انتخاب سبک (LoRA)", choices=list(LORA_MAPPING.keys()), value="تبدیل عکس به انیمه")
421
+ with gr.Accordion("تنظیمات پیشرفته", open=False):
422
+ aspect_ratio_selection = gr.Dropdown(label="ابعاد", choices=ASPECT_RATIOS_LIST, value="خودکار (پیش‌فرض)")
 
 
 
 
 
 
 
 
 
 
 
423
  with gr.Row(visible=False) as custom_dims_row:
424
+ custom_width = gr.Slider(label="عرض", minimum=256, maximum=2048, step=8, value=1024)
425
+ custom_height = gr.Slider(label="ارتفاع", minimum=256, maximum=2048, step=8, value=1024)
426
+ seed = gr.Slider(label="دانه تصادفی", minimum=0, maximum=MAX_SEED, step=1, value=0)
427
+ randomize_seed = gr.Checkbox(label="تصادفی", value=True)
428
+ guidance_scale = gr.Slider(label="وفاداری به متن", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
429
+ steps = gr.Slider(label="مراحل", minimum=1, maximum=50, step=1, value=4)
 
 
 
 
 
 
 
430
 
431
+ def toggle_row(choice): return gr.update(visible=(choice == "شخصی‌سازی (Custom)"))
432
+ aspect_ratio_selection.change(fn=toggle_row, inputs=aspect_ratio_selection, outputs=custom_dims_row)
433
+
434
+ # --- Event Listeners ---
435
+ # کلیک روی دکمه اجرا: خروجی شامل آپدیت دکمه‌هاست (run_button و upgrade_button)
436
+ run_button.click(
437
+ fn=infer,
438
+ inputs=[input_image, prompt, lora_adapter, seed, randomize_seed, guidance_scale, steps, aspect_ratio_selection, custom_width, custom_height, fingerprint_box, status_box_input],
439
+ outputs=[output_image, seed, status_box, run_button, upgrade_button]
440
  )
441
+
442
+ # کلیک روی دکمه ارتقا: اجرای JS برای هدایت به صفحه خرید
443
+ upgrade_button.click(fn=None, js=js_upgrade_func)
444
+
445
+ download_button.click(fn=None, inputs=[output_image], js=js_download_func)
446
+
447
+ # مثال‌ها
448
  gr.Examples(
449
  examples=[
450
  ["examples/1.jpg", "تبدیل به انیمه کن.", "تبدیل عکس به انیمه"],
451
+ ["examples/5.jpg", "سایه‌ها را حذف کن...", "اصلاح نور و سایه"],
 
 
 
 
 
 
 
 
 
452
  ],
453
  inputs=[input_image, prompt, lora_adapter],
454
+ # مثال‌ها دکمه‌ها را آپدیت نمی‌کنند که UI خراب نشود
455
+ fn=infer_example
 
 
456
  )
457
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
458
  if __name__ == "__main__":
459
  demo.queue(max_size=30).launch(show_error=True)