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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +258 -774
app.py CHANGED
@@ -13,7 +13,7 @@ from gradio.themes.utils import colors, fonts, sizes
13
  from deep_translator import GoogleTranslator
14
  from transformers import pipeline
15
 
16
- # --- تعریف تم ---
17
  colors.steel_blue = colors.Color(
18
  name="steel_blue",
19
  c50="#EBF3F8",
@@ -31,13 +31,13 @@ colors.steel_blue = colors.Color(
31
 
32
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
33
 
34
- # --- مدیریت کاربران و اعتبار (در حافظه) ---
35
  # ساختار: { 'fingerprint_hash': {'date': '2023-10-27', 'count': 0} }
36
  USER_CREDITS_DB = {}
37
  MAX_FREE_DAILY_CREDITS = 5
38
- PREMIUM_PAGE_ID = '1149636' # شناسه صفحه پرمیوم برای تشخیص
39
 
40
- # --- بارگذاری مدل تشخیص محتوای نامناسب (NSFW) ---
41
  print("Loading Safety Checker...")
42
  safety_classifier = pipeline("image-classification", model="Falconsai/nsfw_image_detection", device=-1)
43
 
@@ -53,7 +53,7 @@ def is_image_nsfw(image):
53
  print(f"Safety check error: {e}")
54
  return False
55
 
56
- # --- لیست کلمات ممنوعه بسیار سختگیرانه (انگلیسی) ---
57
  BANNED_WORDS = [
58
  "nsfw", "nude", "naked", "sex", "porn", "erotic", "xxx", "18+", "uncensored",
59
  "breast", "nipple", "genital", "vagina", "penis", "ass", "butt", "buttocks",
@@ -74,18 +74,14 @@ BANNED_WORDS = [
74
  def check_text_safety(text):
75
  if not text: return True
76
  text_lower = text.lower()
77
- # بررسی کلمات دقیق به همراه فاصله برای جلوگیری از اشتباه (مثلا ass در class)
78
- # و همچنین بررسی کلمات بدون فاصله برای اطمینان
79
  for word in BANNED_WORDS:
80
- # حالت اول: کلمه مستقل
81
  if f" {word} " in f" {text_lower} ":
82
  return False
83
- # حالت دوم: کلمات خاص که حتی چسبیده هم خطرناکند
84
  if word in ["porn", "hentai", "xxx", "nude", "sex"] and word in text_lower:
85
  return False
86
  return True
87
 
88
- # --- بارگذاری مدل اصلی ---
89
  from diffusers import FlowMatchEulerDiscreteScheduler
90
  from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
91
  from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
@@ -106,7 +102,7 @@ pipe = QwenImageEditPlusPipeline.from_pretrained(
106
  torch_dtype=dtype
107
  ).to(device)
108
 
109
- # بارگذاری LoRA ها
110
  pipe.load_lora_weights("autoweeb/Qwen-Image-Edit-2509-Photo-to-Anime", weight_name="Qwen-Image-Edit-2509-Photo-to-Anime_000001000.safetensors", adapter_name="anime")
111
  pipe.load_lora_weights("dx8152/Qwen-Edit-2509-Multiple-angles", weight_name="镜头转换.safetensors", adapter_name="multiple-angles")
112
  pipe.load_lora_weights("dx8152/Qwen-Image-Edit-2509-Light_restoration", weight_name="移除光影.safetensors", adapter_name="light-restoration")
@@ -130,14 +126,6 @@ LORA_MAPPING = {
130
  "افزایش کیفیت (Upscale)": "upscale-image"
131
  }
132
 
133
- ASPECT_RATIOS_LIST = [
134
- "خودکار (پیش‌فرض)",
135
- "۱:۱ (مربع - 1024x1024)",
136
- "۱۶:۹ (افقی - 1344x768)",
137
- "۹:۱۶ (عمودی - 768x1344)",
138
- "شخصی‌سازی (Custom)"
139
- ]
140
-
141
  ASPECT_RATIOS_MAP = {
142
  "خودکار (پیش‌فرض)": "Auto",
143
  "۱:۱ (مربع - 1024x1024)": (1024, 1024),
@@ -146,6 +134,8 @@ ASPECT_RATIOS_MAP = {
146
  "شخصی‌سازی (Custom)": "Custom"
147
  }
148
 
 
 
149
  def translate_prompt(text):
150
  if not text:
151
  return ""
@@ -168,16 +158,12 @@ def update_dimensions_on_upload(image):
168
  new_height = 1024
169
  aspect_ratio = original_width / original_height
170
  new_width = int(new_height * aspect_ratio)
 
 
171
  new_width = (new_width // 8) * 8
172
  new_height = (new_height // 8) * 8
173
  return new_width, new_height
174
 
175
- def update_sliders_visibility(choice):
176
- if choice == "شخصی‌سازی (Custom)":
177
- return gr.update(visible=True), gr.update(visible=True)
178
- else:
179
- return gr.update(visible=False), gr.update(visible=False)
180
-
181
  def get_error_html(message):
182
  return f"""
183
  <div style="background-color: #fee2e2; border: 1px solid #ef4444; color: #b91c1c; padding: 12px; border-radius: 8px; text-align: center; margin-bottom: 10px; font-weight: bold; display: flex; align-items: center; justify-content: center; gap: 8px; direction: rtl;">
@@ -194,40 +180,40 @@ def get_success_html(message):
194
  </div>
195
  """
196
 
197
- # --- منطق بررسی اعتبار کاربر ---
198
- def check_and_update_credits(fingerprint, is_premium_user):
199
- # اگر کاربر پولی است، محدودیت ندارد
200
- if is_premium_user:
201
  return True, "Unlimited"
202
 
203
  if not fingerprint:
204
- # اگر اثر انگشت دریافت نشد (مثلاً مرورگر خاص)، موقتا اجازه می‌دهیم اما هشدار لاگ می‌کنیم
205
- print("Warning: No fingerprint received.")
206
- return True, "Unknown"
207
 
208
  today_str = datetime.now().strftime("%Y-%m-%d")
209
 
210
- # ایجاد رکورد برای کاربر جدید
211
  if fingerprint not in USER_CREDITS_DB:
212
  USER_CREDITS_DB[fingerprint] = {'date': today_str, 'count': 0}
213
 
214
  user_data = USER_CREDITS_DB[fingerprint]
215
 
216
- # ریست کردن اعتبار اگر روز تغییر کرده باشد
217
  if user_data['date'] != today_str:
218
  user_data['date'] = today_str
219
  user_data['count'] = 0
220
 
221
- # بررسی تعداد استفاده
222
  if user_data['count'] >= MAX_FREE_DAILY_CREDITS:
223
  return False, 0
224
 
225
- # کسر اعتبار (افزایش شمارنده)
226
- user_data['count'] += 1
227
  remaining = MAX_FREE_DAILY_CREDITS - user_data['count']
228
  return True, remaining
229
 
230
- @spaces.GPU(duration=30)
 
 
 
 
 
 
 
231
  def infer(
232
  input_image,
233
  prompt,
@@ -239,890 +225,388 @@ def infer(
239
  aspect_ratio_selection,
240
  custom_width,
241
  custom_height,
242
- user_fingerprint, # ورودی مخفی
243
- user_is_premium, # ورودی مخفی (رشته "true" یا "false")
244
  progress=gr.Progress(track_tqdm=True)
245
  ):
246
- # تبدیل وضعیت پرمیوم به بولین
 
 
 
 
 
 
 
 
247
  is_premium = str(user_is_premium).lower() == 'true'
248
 
249
  if input_image is None:
250
  return None, seed, get_error_html("لطفاً ابتدا یک تصویر بارگذاری کنید.")
251
 
252
- # 1. بررسی سهمیه کاربر (قبل از هر پردازش سنگین)
253
- allowed, remaining_credits = check_and_update_credits(user_fingerprint, is_premium)
254
  if not allowed:
255
- msg = "اعتبار رایگان امروز شما (۵ تصویر) به پایان رسیده است. برای استفاده نامحدود حساب خود را ارتقا دهید."
256
- # تریگر کردن نمایش دکمه ارتقا در فرانت‌اند با یک کلاس خاص یا پیام
257
  return None, seed, get_error_html(msg + " <span class='quota-limit-reached' style='display:none'></span>")
258
 
259
- # 2. بررسی تصویر ورودی برای NSFW
260
  if is_image_nsfw(input_image):
261
- return None, seed, get_error_html("تصویر ورودی دارای محتوای نامناسب است و پردازش نمی‌شود.")
262
 
263
- # 3. ترجمه و بررسی متن
264
  english_prompt = translate_prompt(prompt)
265
  if not check_text_safety(english_prompt):
266
- return None, seed, get_error_html("متن درخواست شامل کلمات غیرمجاز، غیراخلاقی یا جنسی است.")
267
 
268
- adapter_internal_name = LORA_MAPPING.get(lora_adapter_persian)
269
- if adapter_internal_name:
270
- pipe.set_adapters([adapter_internal_name], adapter_weights=[1.0])
 
271
 
272
  if randomize_seed:
273
  seed = random.randint(0, MAX_SEED)
274
 
275
  generator = torch.Generator(device=device).manual_seed(seed)
276
 
277
- safety_negative = "nsfw, nude, naked, porn, sexual, xxx, breast, nipple, genital, vagina, penis, ass, lingerie, bikini, swimwear, underwear, fetish, topless, gore, violence, blood, child abuse"
278
- 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"
279
- final_negative_prompt = f"{safety_negative}, {base_negative}"
280
-
281
- original_image = input_image.convert("RGB")
282
-
283
- selection_value = ASPECT_RATIOS_MAP.get(aspect_ratio_selection)
284
-
285
- if selection_value == "Custom":
286
- width = (int(custom_width) // 8) * 8
287
- height = (int(custom_height) // 8) * 8
288
- elif selection_value == "Auto" or selection_value is None:
289
- width, height = update_dimensions_on_upload(original_image)
290
  else:
291
- width, height = selection_value
292
 
293
  try:
 
 
294
  result = pipe(
295
- image=original_image,
296
  prompt=english_prompt,
297
- negative_prompt=final_negative_prompt,
298
- height=height,
299
- width=width,
300
- num_inference_steps=steps,
301
  generator=generator,
302
- true_cfg_scale=guidance_scale,
303
  ).images[0]
304
 
305
- # 4. بررسی تصویر خروجی
306
  if is_image_nsfw(result):
307
- return None, seed, get_error_html("تصویر تولید شده حاوی محتوای نامناسب بود و حذف شد.")
308
 
309
- # پیام موفقیت با نمایش اعتبار باقی‌مانده برای کاربران رایگان
310
- success_msg = "تصویر با موفقیت ویرایش شد."
 
 
 
311
  if not is_premium:
312
- success_msg += f"<br><small>اعتبار باقی‌مانده امروز: {remaining_credits} تصویر</small>"
 
 
313
 
314
  return result, seed, get_success_html(success_msg)
315
 
316
  except Exception as e:
317
  error_str = str(e)
318
- if "quota" in error_str.lower() or "exceeded" in error_str.lower():
319
- raise e
320
- return None, seed, get_error_html(f"خطا در پردازش: {error_str}")
321
 
322
- @spaces.GPU(duration=30)
323
  def infer_example(input_image, prompt, lora_adapter):
324
- # برای مثال‌ها، کاربر فرضی رایگان با اعتبار بی‌نهایت در نظر گرفته می‌شود یا اثر انگشت تست
325
- res, s, status = infer(input_image, prompt, lora_adapter, 0, True, 1.0, 4, "خودکار (پیش‌فرض)", 1024, 1024, "example_user", "true")
326
- return res, s, status
327
-
328
- # --- جاوااسکریپت برای دکمه دانلود ---
329
- js_download_func = """
330
- async (image) => {
331
- if (!image) {
332
- alert("لطفاً ابتدا تصویر را تولید کنید.");
333
- return;
334
- }
335
- let fileUrl = image.url;
336
- if (fileUrl && !fileUrl.startsWith('http')) {
337
- fileUrl = window.location.origin + fileUrl;
338
- } else if (!fileUrl && image.path) {
339
- fileUrl = window.location.origin + "/file=" + image.path;
340
- }
341
- window.parent.postMessage({
342
- type: 'DOWNLOAD_REQUEST',
343
- url: fileUrl
344
- }, '*');
345
- }
346
- """
347
 
348
- # --- جاوااسکریپت سراسری (شناسایی کاربر، اثر انگشت، UI) ---
349
- js_global_content = """
350
  <script>
351
- // --- پیکربندی ---
352
  const PREMIUM_PAGE_ID = '1149636';
353
  const PREMIUM_URL = '#/nav/online/news/getSingle/1149636/eyJpdiI6InZSVUdlLzBlR0FzOHZJdXFZeWhER0E9PSIsInZhbHVlIjoiWFhqRXBLc29vSFpHdk9nYmRjZGVuWHRHRHVSZHRlTG1BUENLaE5mNXBNVVRGWFg3ZWN0djJ5K1dIY1RqTHJGaCIsIm1hYyI6IjIzYzFlZTMwYmVmMTdkYjQ0YTQ4YWMxNmFhN2RmNWQ2OTc1NDIyNGVlZGI3ZjJjMjhkNmQxNjM4MDFlZTIxNmUiLCJ0YWciOiIifQ==/20934991';
354
 
355
- // --- تولید اثر انگشت مرورگر ---
356
  function getBrowserFingerprint() {
357
  const components = [navigator.userAgent, navigator.language, screen.width + 'x' + screen.height, new Date().getTimezoneOffset()];
358
  try {
359
  const canvas = document.createElement('canvas');
360
  const ctx = canvas.getContext('2d');
361
- ctx.textBaseline = "top";
362
- ctx.font = "14px 'Arial'";
363
- ctx.textBaseline = "alphabetic";
364
- ctx.fillStyle = "#f60";
365
- ctx.fillRect(125, 1, 62, 20);
366
- ctx.fillStyle = "#069";
367
- ctx.fillText("a1b2c3d4e5f6g7h8i9j0_!@#$%^&*()", 2, 15);
368
  components.push(canvas.toDataURL());
369
- } catch (e) {
370
- components.push("canvas-error");
371
- }
372
- const fingerprintString = components.join('~~~');
373
  let hash = 0;
374
- for (let i = 0; i < fingerprintString.length; i++) {
375
- const char = fingerprintString.charCodeAt(i);
376
- hash = ((hash << 5) - hash) + char;
377
  hash |= 0;
378
  }
379
  return 'fp_' + Math.abs(hash).toString(16);
380
  }
381
 
382
- // --- مدیریت وضعیت اشتراک ---
383
  let userFingerprint = getBrowserFingerprint();
384
  let isUserPremium = false;
385
 
386
- function isUserPaid(userObject) {
387
- return userObject && userObject.isLogin && userObject.accessible_pages && (userObject.accessible_pages.includes(PREMIUM_PAGE_ID) || userObject.accessible_pages.includes(parseInt(PREMIUM_PAGE_ID)));
388
- }
389
-
390
- function updateUIForSubscription(status) {
391
- const badge = document.getElementById('user-badge');
392
- const descText = document.getElementById('user-desc-text');
393
 
394
  if (status === 'paid') {
395
  isUserPremium = true;
396
  if(badge) {
397
- badge.textContent = 'نسخه نامحدود 💎';
398
- badge.className = 'status-badge paid-badge';
399
  }
400
- if(descText) descText.textContent = 'برای کاربران پولی: استفاده نامحدود';
401
- document.getElementById('upgrade-btn-container').style.display = 'none';
402
  } else {
403
  isUserPremium = false;
404
  if(badge) {
405
- badge.textContent = 'نسخه رایگان';
406
- badge.className = 'status-badge free-badge';
407
  }
408
- if(descText) descText.textContent = 'برای کاربران رایگان: ۵ تصویر در روز';
409
  }
410
-
411
- // آپدیت کردن اینپوت‌های مخفی گرادیو
412
- updateGradioHiddenInputs();
413
  }
414
 
415
- function updateGradioHiddenInputs() {
416
- // پیدا کردن تکست‌باکس‌های مخفی و مقداردهی
417
- // ما از کلاس‌های خاص استفاده می‌کنیم که در پایتون تعریف شده‌اند
418
- const fpInput = document.querySelector('.hidden-fingerprint textarea');
419
- const premiumInput = document.querySelector('.hidden-premium textarea');
420
 
421
- if (fpInput) {
422
- fpInput.value = userFingerprint;
423
- fpInput.dispatchEvent(new Event('input', { bubbles: true }));
424
  }
425
- if (premiumInput) {
426
- premiumInput.value = isUserPremium ? "true" : "false";
427
- premiumInput.dispatchEvent(new Event('input', { bubbles: true }));
428
  }
429
  }
430
 
431
- // گوش دادن به پیام از اپلیکیشن مادر
 
 
 
 
 
432
  window.addEventListener('message', (event) => {
433
  if (event.data && event.data.type === 'USER_STATUS_RESPONSE') {
434
- if (event.data.error || !event.data.payload) {
435
- updateUIForSubscription('free');
436
- return;
437
- }
438
  try {
439
- const userObject = JSON.parse(event.data.payload);
440
- const status = isUserPaid(userObject) ? 'paid' : 'free';
441
- updateUIForSubscription(status);
442
- } catch (e) {
443
- updateUIForSubscription('free');
444
- }
 
445
  }
446
  });
447
 
448
- // درخواست وضعیت کاربر هنگام لود
449
  document.addEventListener('DOMContentLoaded', () => {
450
- // 1. Force Light Mode
451
- const forceLight = () => {
452
- const body = document.querySelector('body');
453
- if (body) {
454
- body.classList.remove('dark');
455
- body.style.backgroundColor = '#f5f7fa';
456
- body.style.color = '#333333';
457
- }
458
- document.querySelectorAll('.dark').forEach(el => el.classList.remove('dark'));
459
- };
460
- forceLight();
461
- setInterval(forceLight, 1000);
462
-
463
- // درخواست وضعیت از پرنت
464
  parent.postMessage({ type: 'REQUEST_USER_STATUS' }, '*');
465
 
466
- // اگر پاسخی نیامد پیش‌فرض رایگان
467
  setTimeout(() => {
468
- if (!document.getElementById('user-badge').textContent) {
469
- updateUIForSubscription('free');
 
470
  }
471
- }, 2000);
472
-
473
- // 2. RETRY FUNCTION
474
- window.retryGeneration = function() {
475
- const modal = document.getElementById('custom-quota-modal');
476
- if (modal) modal.remove();
477
-
478
- const runBtn = document.getElementById('run-btn');
479
- if(runBtn) runBtn.click();
480
- };
481
-
482
- // Close function
483
- window.closeErrorModal = function() {
484
- const modal = document.getElementById('custom-quota-modal');
485
- if (modal) modal.remove();
486
- };
487
-
488
- // هدایت به صفحه خرید
489
- window.navigateToPremium = function() {
490
- parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM', payload: { url: PREMIUM_URL } }, '*');
491
- };
492
-
493
- // 3. SHOW MODAL FUNCTION (IP LIMIT)
494
- const showQuotaModal = () => {
495
- if (document.getElementById('custom-quota-modal')) return;
496
 
497
- const modalHtml = `
498
- <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;">
499
- <div class="ip-reset-guide-container">
500
- <div class="guide-header">
501
- <svg class="guide-header-icon" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
502
- <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>
503
- <circle cx="50" cy="50" r="45" fill="url(#grad1)" opacity="0.1"></circle>
504
- <circle cx="50" cy="50" r="35" fill="none" stroke="url(#grad1)" stroke-width="2" opacity="0.3"></circle>
505
- <path d="M35 50 L45 60 L65 40" stroke="url(#grad1)" stroke-width="4" fill="none" stroke-linecap="round" stroke-linejoin="round"></path>
506
- <circle cx="65" cy="35" r="8" fill="#fee140"></circle>
507
- <path d="M62 35 L68 35 M65 32 L65 38" stroke="white" stroke-width="2" stroke-linecap="round"></path>
508
- </svg>
509
- <div>
510
- <h2>یک قدم تا ساخت تصاویر جدید</h2>
511
- <p>نیازمند تغییر نقطه دستیابی</p>
512
- </div>
513
- </div>
514
-
515
- <div class="guide-content">
516
- <div class="info-card">
517
- <div class="info-card-header">
518
- <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>
519
- <span class="info-card-title">راه حل سریع</span>
520
- </div>
521
- <p>طبق ویدیو آموزشی پایین بین نقطه دستیابی جابجا شوید تلاش مجدد بزنید تا تصاویر مجدداً تولید بشه.</p>
522
- </div>
523
-
524
- <div class="summary-section">
525
- <div class="summary-header">
526
- <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>
527
- <span class="summary-title">خلاصه راهنما</span>
528
- </div>
529
- <div class="summary-text">هربار که این صفحه را مشاهده کردید: از اینترنت سیم‌کارت استفاده کنید، VPN را خاموش کرده و طبق ویدیو آموزشی پایین نقطه دستیابی رو تغییر دهید. «تلاش مجدد» کلیک کنید. با این روش ساده می‌توانید به صورت نامحدود تصویر بسازید! ☘️</div>
530
- </div>
531
-
532
- <div class="video-button-container">
533
- <button onclick="parent.postMessage({ type: 'NAVIGATE_TO_URL', url: '#/nav/online/news/getSingle/1149635/eyJpdiI6IjhHVGhPQWJwb3E0cjRXbnFWTW5BaUE9PSIsInZhbHVlIjoiS1V0dTdvT21wbXAwSXZaK1RCTG1pVXZqdlFJa1hXV1RKa2FLem9zU3pXMjd5MmlVOGc2YWY0NVdNR3h3Smp1aSIsIm1hYyI6IjY1NTA5ZDYzMjAzMTJhMGQyMWQ4NjA4ZDgyNGZjZDVlY2MyNjdiMjA2NWYzOWRjY2M4ZmVjYWRlMWNlMWQ3ODEiLCJ0YWciOiIifQ==/21135210' }, '*')" class="elegant-video-button">
534
- <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>
535
- <span>دیدن ویدیو آموزشی استفاده نامحدود</span>
536
- </button>
537
- </div>
538
- </div>
539
-
540
- <div class="guide-actions">
541
- <button class="action-button back-button" onclick="window.closeErrorModal()">
542
- <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>
543
- <span>بازگشت</span>
544
- </button>
545
- <button class="action-button retry-button" onclick="window.retryGeneration()">
546
- <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>
547
- <span>تلاش مجدد</span>
548
- </button>
549
- </div>
550
- </div>
551
- </div>
552
- `;
553
-
554
- document.body.insertAdjacentHTML('beforeend', modalHtml);
555
-
556
- // Auto close after 10 seconds
557
- setTimeout(() => {
558
- window.closeErrorModal();
559
- }, 10000);
560
- };
561
-
562
- // 4. SCANNER (Monitor for errors)
563
  setInterval(() => {
564
- // الف) چک کردن خطای Quota
565
- const potentialErrors = document.querySelectorAll('.toast-body, .error, .toast-wrap, .eta-bar, div[class*="error"]');
566
- potentialErrors.forEach(el => {
567
- const text = el.innerText || "";
568
- if (text.toLowerCase().includes('quota') || text.toLowerCase().includes('exceeded')) {
569
- showQuotaModal();
570
- el.style.display = 'none';
571
- el.style.opacity = '0';
572
- const parentWrap = el.closest('.toast-wrap');
573
- if(parentWrap) parentWrap.style.display = 'none';
 
574
  }
575
  });
576
-
577
- // ب) چک کردن اتمام اعتبار روزانه (از طریق کلاس HTML که در پایتون برگرداندیم)
578
- const limitReached = document.querySelector('.quota-limit-reached');
579
- if (limitReached) {
580
- document.getElementById('upgrade-btn-container').style.display = 'block';
581
- limitReached.remove(); // جلوگیری از اجرای مکرر
582
- }
583
-
584
- }, 200);
585
  });
586
 
587
- // تابع کمکی برای اطمینان از ارسال مقادیر قبل از کلیک
588
- function prepareSubmission() {
589
- updateGradioHiddenInputs();
 
 
 
 
590
  return [];
591
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
592
  </script>
593
- """
594
 
595
- # --- CSS Updated ---
596
- css_code = """
597
  <style>
598
- @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700;900&display=swap');
599
 
600
- :root, .dark, body, .gradio-container {
601
- --body-background-fill: #f5f7fa !important;
602
- --body-text-color: #1f2937 !important;
603
- --background-fill-primary: #ffffff !important;
604
- --background-fill-secondary: #f3f4f6 !important;
605
- --border-color-primary: #e5e7eb !important;
606
- --block-background-fill: #ffffff !important;
607
- --block-label-text-color: #374151 !important;
608
- --block-title-text-color: #111827 !important;
609
- --input-background-fill: #ffffff !important;
610
- color-scheme: light !important;
611
- }
612
 
613
- /* User Status Badge */
614
- #user-status-container {
615
  display: flex;
616
  flex-direction: column;
617
  align-items: center;
618
  justify-content: center;
 
 
 
619
  margin-bottom: 20px;
620
- animation: fadeIn 0.8s ease-out;
621
- }
622
- .status-badge {
623
- padding: 6px 16px;
624
- border-radius: 20px;
625
- font-size: 0.9em;
626
- font-weight: 700;
627
- margin-bottom: 8px;
628
- letter-spacing: 0.5px;
629
- text-shadow: 0 1px 2px rgba(0,0,0,0.1);
630
- }
631
- .free-badge {
632
- background: linear-gradient(45deg, #6c757d, #495057);
633
- color: white;
634
- box-shadow: 0 4px 10px rgba(108, 117, 125, 0.3);
635
- }
636
- .paid-badge {
637
- background: linear-gradient(45deg, #FFD700, #FFA500);
638
- color: #333;
639
- box-shadow: 0 4px 10px rgba(255, 193, 7, 0.4);
640
- }
641
- #user-desc-text {
642
- font-size: 0.9em;
643
- color: #4b5563;
644
- }
645
-
646
- /* Upgrade Button */
647
- @keyframes pulse-gold {
648
- 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 193, 7, 0.7); }
649
- 70% { transform: scale(1.02); box-shadow: 0 0 0 10px rgba(255, 193, 7, 0); }
650
- 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 193, 7, 0); }
651
- }
652
- #upgrade-premium-btn {
653
- width: 100%;
654
- margin-top: 10px;
655
- padding: 12px;
656
- font-family: 'Vazirmatn', sans-serif;
657
- font-size: 1.1em;
658
- font-weight: 800;
659
- color: #212529;
660
- background: linear-gradient(95deg, #FFD54F, #FFC107 100%);
661
- border: none;
662
- border-radius: 14px;
663
- cursor: pointer;
664
- box-shadow: 0 8px 20px -5px rgba(255, 193, 7, 0.5);
665
- animation: pulse-gold 2s infinite;
666
- display: block;
667
- }
668
- #upgrade-premium-btn:hover {
669
- transform: translateY(-2px);
670
- box-shadow: 0 12px 25px -5px rgba(255, 193, 7, 0.6);
671
- }
672
-
673
- /* --- IP Reset Guide CSS --- */
674
- :root {
675
- --guide-bg: rgba(255, 255, 255, 0.98);
676
- --guide-border: rgba(102, 126, 234, 0.2);
677
- --guide-text-title: #2d3748;
678
- --guide-text-body: #4a5568;
679
- --guide-accent: #667eea;
680
- --primary-gradient-guide: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
681
- --success-gradient-guide: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%);
682
- --radius-md-guide: 12px;
683
- --radius-lg-guide: 16px;
684
- --shadow-sm: 0 1px 2px 0 rgba(26, 32, 44, 0.03);
685
- --shadow-md: 0 4px 6px -1px rgba(26, 32, 44, 0.05), 0 2px 4px -2px rgba(26, 32, 44, 0.04);
686
- --shadow-xl: 0 20px 25px -5px rgba(26, 32, 44, 0.07), 0 8px 10px -6px rgba(26, 32, 44, 0.05);
687
  }
688
 
689
- @keyframes float {
690
- 0%, 100% { transform: translateY(0px); }
691
- 50% { transform: translateY(-10px); }
692
- }
693
- @keyframes slideInUp {
694
- from { opacity: 0; transform: translateY(30px); }
695
- to { opacity: 1; transform: translateY(0); }
696
- }
697
-
698
- .ip-reset-guide-container {
699
- text-align: right;
700
- direction: rtl;
701
- background: var(--guide-bg);
702
- backdrop-filter: blur(10px);
703
- padding: 20px; /* Increased Padding */
704
- border-radius: var(--radius-lg-guide);
705
- box-shadow: var(--shadow-xl);
706
- border: 1px solid var(--guide-border);
707
- animation: slideInUp 0.6s cubic-bezier(0.4, 0, 0.2, 1) both;
708
- width: 90%;
709
- max-width: 420px; /* Increased width slightly */
710
- max-height: 90vh; /* Prevent overflow on small screens */
711
- overflow-y: auto; /* Enable scroll if needed */
712
- position: relative;
713
- box-sizing: border-box;
714
- font-family: 'Vazirmatn', sans-serif !important;
715
- }
716
- .ip-reset-guide-container::before {
717
- content: ''; position: absolute; top: 0; left: 0; right: 0; height: 4px; background: var(--primary-gradient-guide);
718
- }
719
- .guide-header { display: flex; align-items: center; margin-bottom: 15px; }
720
- .guide-header-icon { width: 45px; height: 45px; margin-left: 15px; animation: float 3s ease-in-out infinite; flex-shrink: 0; }
721
- .guide-header h2 { font-size: 1.2rem; color: var(--guide-text-title); font-weight: 700; margin: 0; }
722
- .guide-header p { color: var(--guide-text-body); font-size: 0.8rem; margin-top: 3px; margin-bottom: 0; }
723
-
724
- .guide-content { font-size: 0.9rem; color: var(--guide-text-body); line-height: 1.6; }
725
-
726
- .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; }
727
- .info-card p { font-size: 0.85rem; line-height: 1.6; margin: 0; }
728
- .info-card::before { content: ''; position: absolute; top: 0; right: 0; width: 3px; height: 100%; background: var(--primary-gradient-guide); }
729
- .info-card-header { display: flex; align-items: center; margin-bottom: 8px; }
730
- .info-card-icon { width: 18px; height: 18px; margin-left: 8px; }
731
- .info-card-title { font-weight: 600; color: var(--guide-text-title); font-size: 0.95rem; }
732
-
733
- .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; }
734
- .summary-section::before { content: ''; position: absolute; top: 0; right: 0; width: 3px; height: 100%; background: var(--success-gradient-guide); }
735
- .summary-header { display: flex; align-items: center; margin-bottom: 8px; }
736
- .summary-icon { width: 18px; height: 18px; margin-left: 8px; }
737
- .summary-title { font-weight: 600; color: #2f5a33; font-size: 0.95rem; }
738
- .summary-text { color: #2f5a33; font-size: 0.85rem; line-height: 1.6; }
739
-
740
- /* Tutorial Button */
741
- .video-button-container { text-align: center; margin: 20px 0 15px 0; width: 100%; }
742
- .elegant-video-button {
743
- display: inline-flex !important;
744
- align-items: center;
745
- justify-content: center;
746
- padding: 10px 20px !important;
747
- background-color: #fff !important;
748
- color: var(--guide-accent) !important;
749
- border: 1px solid #e2e8f0 !important;
750
- text-decoration: none;
751
- border-radius: 50px !important;
752
- font-weight: 600 !important;
753
- font-size: 0.9rem !important;
754
- cursor: pointer !important;
755
- font-family: inherit;
756
- transition: all 0.3s ease !important;
757
- box-shadow: 0 2px 10px rgba(0,0,0,0.05) !important;
758
- width: auto !important;
759
- }
760
- .elegant-video-button:hover {
761
- background: var(--primary-gradient-guide) !important;
762
- color: white !important;
763
- border-color: transparent !important;
764
- transform: translateY(-2px);
765
- box-shadow: 0 6px 16px rgba(102, 126, 234, 0.3) !important;
766
- }
767
- .elegant-video-button-icon { width: 18px; height: 18px; margin-left: 8px; fill: currentColor; }
768
-
769
- /* Action Buttons */
770
- .guide-actions {
771
- display: flex !important;
772
- gap: 12px !important;
773
- margin-top: 20px;
774
- padding-top: 20px;
775
- border-top: 1px solid #e2e8f0;
776
- width: 100% !important;
777
- }
778
- .action-button {
779
- padding: 12px 15px !important;
780
- border: none !important;
781
- border-radius: 12px !important;
782
- font-size: 0.95rem !important;
783
- font-weight: 600 !important;
784
- cursor: pointer !important;
785
- flex: 1 !important;
786
- transition: all 0.3s ease !important;
787
- display: flex !important;
788
- align-items: center;
789
- justify-content: center;
790
- font-family: inherit;
791
- height: 48px !important;
792
- }
793
- .action-button-icon { width: 20px; height: 20px; margin-right: 0; margin-left: 8px; }
794
-
795
- .back-button {
796
- background: white !important;
797
- color: var(--guide-text-body) !important;
798
- border: 2px solid #e2e8f0 !important;
799
- }
800
- .back-button:hover {
801
- background: #f7fafc !important;
802
- border-color: var(--guide-accent) !important;
803
- transform: translateY(-2px);
804
- box-shadow: var(--shadow-md) !important;
805
- }
806
-
807
- .retry-button {
808
- background: var(--primary-gradient-guide) !important;
809
- color: white !important;
810
- box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3) !important;
811
- }
812
- .retry-button:hover {
813
- transform: translateY(-2px);
814
- box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4) !important;
815
- }
816
-
817
- /* --- Main App CSS --- */
818
- body {
819
- font-family: 'Vazirmatn', sans-serif !important;
820
- background-color: #f5f7fa !important;
821
- margin: 0;
822
- padding: 10px;
823
- }
824
-
825
- #col-container {
826
- margin: 0 auto;
827
- max-width: 980px;
828
- direction: rtl;
829
- text-align: right;
830
- padding: 30px;
831
- background: #ffffff !important;
832
- border-radius: 24px;
833
- box-shadow: 0 10px 40px -10px rgba(0,0,0,0.08);
834
- border: 1px solid rgba(255,255,255,0.8);
835
- }
836
-
837
- #main-title h1 {
838
- font-size: 2.4em !important;
839
- text-align: center;
840
- color: #1a202c !important;
841
- margin-bottom: 15px;
842
  font-weight: 800;
843
- background: -webkit-linear-gradient(45deg, #2563eb, #1e40af);
844
- -webkit-background-clip: text;
845
- -webkit-text-fill-color: transparent;
846
- }
847
-
848
- #main-description {
849
- text-align: center;
850
- font-size: 1.15em;
851
- color: #4b5563 !important;
852
- margin-bottom: 20px;
853
- line-height: 1.6;
854
- }
855
-
856
- .gr-input-label, span.label-wrap, label span {
857
- font-weight: 700 !important;
858
- color: #374151 !important;
859
- font-size: 0.95em !important;
860
- margin-bottom: 8px !important;
861
  }
862
 
863
- textarea, input[type="text"] {
864
- border: 2px solid #e2e8f0 !important;
865
- border-radius: 12px !important;
866
- background-color: #ffffff !important;
867
- color: #111827 !important;
868
- padding: 12px !important;
869
- transition: all 0.3s ease;
870
- font-family: 'Vazirmatn', sans-serif !important;
871
  }
872
 
873
- textarea:focus, input[type="text"]:focus {
874
- border-color: #3b82f6 !important;
875
- box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1) !important;
876
- outline: none;
877
  }
878
 
879
- .gr-dropdown {
880
- background: #ffffff !important;
881
- border-radius: 12px !important;
882
  }
883
 
884
- .primary-btn, button.primary {
885
- background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important;
886
- border: none !important;
887
- color: white !important;
888
- font-weight: 700 !important;
889
- font-size: 1.1em !important;
890
- padding: 14px 28px !important;
891
- border-radius: 14px !important;
892
- box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3) !important;
893
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
894
- cursor: pointer !important;
895
  width: 100%;
896
- margin-top: 15px;
897
- }
898
-
899
- .primary-btn:hover, button.primary:hover {
900
- transform: translateY(-2px);
901
- box-shadow: 0 8px 25px rgba(16, 185, 129, 0.45) !important;
902
- }
903
-
904
- .primary-btn:active, button.primary:active {
905
- transform: translateY(1px);
906
- }
907
-
908
- #download-btn {
909
- background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
910
- box-shadow: 0 4px 15px rgba(59, 130, 246, 0.3) !important;
911
- }
912
- #download-btn:hover {
913
- box-shadow: 0 8px 25px rgba(59, 130, 246, 0.45) !important;
914
- }
915
-
916
- .gradio-container .prose table,
917
- .gradio-container table {
918
- background-color: #ffffff !important;
919
- color: #111827 !important;
920
- border: 1px solid #e5e7eb !important;
921
- border-radius: 12px !important;
922
- overflow: hidden !important;
923
- width: 100% !important;
924
- margin-top: 20px !important;
925
- }
926
-
927
- .gradio-container thead th {
928
- background-color: #f3f4f6 !important;
929
- color: #374151 !important;
930
- font-weight: 700 !important;
931
- border-bottom: 2px solid #e5e7eb !important;
932
- padding: 12px !important;
933
- text-align: right !important;
934
  }
935
 
936
- .gradio-container tbody tr {
937
- background-color: #ffffff !important;
938
- border-bottom: 1px solid #f3f4f6 !important;
939
- }
940
-
941
- .gradio-container tbody tr:hover {
942
- background-color: #f9fafb !important;
 
 
 
 
943
  }
944
-
945
- .gradio-container tbody td {
946
- background-color: #ffffff !important;
947
- color: #374151 !important;
948
- padding: 10px !important;
949
  }
950
 
951
- .gradio-container tbody td span,
952
- .gradio-container tbody td p {
953
- color: #374151 !important;
954
- }
955
 
 
956
  footer { display: none !important; }
957
- .flagging { display: none !important; }
958
-
959
- /* Force toast transparency */
960
- .toast-body {
961
- direction: rtl !important;
962
- text-align: right !important;
963
- background: transparent !important;
964
- box-shadow: none !important;
965
- border: none !important;
966
- padding: 0 !important;
967
- max-width: 100% !important;
968
- width: auto !important;
969
- }
970
- .toast-wrap {
971
- background: transparent !important;
972
- border: none !important;
973
- box-shadow: none !important;
974
- }
975
-
976
- /* Hidden inputs logic */
977
- .hidden-fingerprint, .hidden-premium {
978
- display: none !important;
979
- }
980
-
981
- @media (prefers-color-scheme: dark) {
982
- body, .gradio-container, .prose, table, tr, td, th {
983
- background-color: #ffffff !important;
984
- color: #333333 !important;
985
- }
986
- }
987
  </style>
988
  """
989
 
990
- # ادغام CSS و JS
991
- combined_html = css_code + js_global_content
992
-
993
- # استفاده از gr.Blocks
994
- with gr.Blocks() as demo:
995
- # تزریق کدها به عنوان HTML
996
- gr.HTML(combined_html)
997
 
998
  with gr.Column(elem_id="col-container"):
999
  gr.Markdown("# **ویرایشگر هوشمند آلفا**", elem_id="main-title")
1000
- gr.Markdown(
1001
- "با هوش مصنوعی آلفا تصاویر تونو به مدل های مختلف ویرایش کنید.",
1002
- elem_id="main-description"
1003
- )
1004
 
1005
- # --- بج وضعیت کاربر و دکمه ارتقا ---
1006
  gr.HTML("""
1007
- <div id="user-status-container">
1008
- <span id="user-badge" class="status-badge"></span>
1009
- <span id="user-desc-text"></span>
1010
- <div id="upgrade-btn-container" style="display:none; width: 100%;">
1011
- <button id="upgrade-premium-btn" onclick="navigateToPremium()">
1012
- ⭐️ ارتقا به نسخه کامل و نامحدود
1013
- </button>
1014
  </div>
1015
  </div>
1016
  """)
 
 
 
 
1017
 
1018
- # --- ورودی‌های مخفی برای انتقال اطلاعات از JS به Python ---
1019
- hidden_fingerprint = gr.Textbox(elem_classes="hidden-fingerprint", visible=False)
1020
- hidden_premium = gr.Textbox(elem_classes="hidden-premium", visible=False)
1021
-
1022
- with gr.Row(equal_height=True):
1023
  with gr.Column():
1024
- input_image = gr.Image(label="بارگذاری تصویر", type="pil", height=320)
1025
-
1026
- prompt = gr.Text(
1027
- label="دستور ویرایش (به فارسی)",
1028
- show_label=True,
1029
- placeholder="مثال: تصویر را به سبک انیمه تبدیل کن...",
1030
- rtl=True,
1031
- lines=3
1032
- )
1033
-
1034
- status_box = gr.HTML(label="وضعیت")
1035
 
1036
- # IMPORTANT: Added elem_id="run-btn" here for JS targeting
1037
- run_button = gr.Button("✨ شروع پردازش و ساخت تصویر", variant="primary", elem_classes="primary-btn", elem_id="run-btn")
1038
-
1039
  with gr.Column():
1040
- output_image = gr.Image(label="تصویر نهایی", interactive=False, format="png", height=380)
1041
-
1042
- download_button = gr.Button("📥 دانلود و ذخیره تصویر", variant="secondary", elem_id="download-btn", elem_classes="primary-btn")
1043
-
1044
- with gr.Row():
1045
- lora_adapter = gr.Dropdown(
1046
- label="انتخاب سبک ویرایش (LoRA)",
1047
- choices=list(LORA_MAPPING.keys()),
1048
- value="تبدیل عکس به انیمه"
1049
- )
1050
 
1051
- with gr.Accordion("تنظیمات پیشرفته", open=False, visible=True):
1052
- aspect_ratio_selection = gr.Dropdown(
1053
- label="ابعاد تصویر خروجی",
1054
- choices=ASPECT_RATIOS_LIST,
1055
- value="خودکار (پیش‌فرض)",
1056
- interactive=True
1057
- )
1058
 
1059
- with gr.Row(visible=False) as custom_dims_row:
1060
- custom_width = gr.Slider(
1061
- label="عرض دلخواه (Width)",
1062
- minimum=256, maximum=2048, step=8, value=1024
1063
- )
1064
- custom_height = gr.Slider(
1065
- label="ارتفاع دلخواه (Height)",
1066
- minimum=256, maximum=2048, step=8, value=1024
1067
- )
1068
 
1069
- seed = gr.Slider(label="دانه تصادفی (Seed)", minimum=0, maximum=MAX_SEED, step=1, value=0)
1070
- randomize_seed = gr.Checkbox(label="استفاده از Seed تصادفی", value=True)
1071
- guidance_scale = gr.Slider(label="میزان وفاداری به متن (Guidance Scale)", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
1072
- steps = gr.Slider(label="تعداد مراحل پردازش (Steps)", minimum=1, maximum=50, step=1, value=4)
1073
 
1074
- # اصلاح تابع نمایش ردیف اسلایدرها
1075
- def toggle_row(choice):
1076
- if choice == "شخصی‌سازی (Custom)":
1077
- return gr.update(visible=True)
1078
- return gr.update(visible=False)
1079
-
1080
- aspect_ratio_selection.change(
1081
- fn=toggle_row,
1082
- inputs=aspect_ratio_selection,
1083
- outputs=custom_dims_row
1084
- )
1085
 
1086
- gr.Examples(
1087
- examples=[
1088
- ["examples/1.jpg", "تبدیل به انیمه کن.", "تبدیل عکس به انیمه"],
1089
- ["examples/5.jpg", "سایه‌ها را حذف کن و نورپردازی نرم به تصویر بده.", "اصلاح نور و سایه"],
1090
- ["examples/4.jpg", "از فیلتر ساعت طلایی با پخش نور ملایم استفاده کن.", "نورپردازی مجدد (Relight)"],
1091
- ["examples/2.jpeg", "دوربین را ۴۵ درجه به سمت چپ بچرخان.", "تغییر زاویه دید"],
1092
- ["examples/7.jpg", "منبع نور را از سمت راست عقب قرار بده.", "نورپردازی چند زاویه‌ای"],
1093
- ["examples/10.jpeg", "کیفیت تصویر را افزایش بده (Upscale).", "افزایش کیفیت (Upscale)"],
1094
- ["examples/7.jpg", "منبع نور را از پایین بتابان.", "نورپردازی چند زاویه‌ای"],
1095
- ["examples/2.jpeg", "زاویه دوربین را به نمای بالا گوشه راست تغییر بده.", "تغییر زاویه دید"],
1096
- ["examples/9.jpg", "دوربین کمی به جلو حرکت می‌کند در حالی که نور خورشید از میان ابرها می‌تابد و درخششی نرم اطراف شبح شخصیت در مه ایجاد می‌کند. سبک سینمایی واقعی.", "صحنه بعدی (سینمایی)"],
1097
- ["examples/8.jpg", "جزئیات پوست سوژه را برجسته‌تر و طبیعی‌تر کن.", "روتوش پوست"],
1098
- ["examples/6.jpg", "دوربین را به نمای پایین به بالا تغییر بده.", "تغییر زاویه دید"],
1099
- ],
1100
- inputs=[input_image, prompt, lora_adapter],
1101
- outputs=[output_image, seed, status_box],
1102
- fn=infer_example,
1103
- cache_examples=False,
1104
- label="نمونه‌ها (برای تست کلیک کنید)"
1105
- )
1106
-
1107
- # اجرای پروسه با انتقال اطلاعات کاربر
1108
- run_button.click(
1109
  fn=infer,
1110
  inputs=[
1111
- input_image, prompt, lora_adapter, seed, randomize_seed,
1112
- guidance_scale, steps, aspect_ratio_selection, custom_width, custom_height,
1113
- hidden_fingerprint, hidden_premium # ورودی‌های جدید
1114
  ],
1115
  outputs=[output_image, seed, status_box],
1116
- api_name="predict",
1117
- js="prepareSubmission" # فراخوانی تابع JS قبل از ارسال به پایتون
1118
- )
1119
-
1120
- download_button.click(
1121
- fn=None,
1122
- inputs=[output_image],
1123
- outputs=None,
1124
- js=js_download_func
1125
  )
 
 
1126
 
1127
  if __name__ == "__main__":
1128
- demo.queue(max_size=30).launch(show_error=True)
 
13
  from deep_translator import GoogleTranslator
14
  from transformers import pipeline
15
 
16
+ # --- تنظیمات تم و رنگ ---
17
  colors.steel_blue = colors.Color(
18
  name="steel_blue",
19
  c50="#EBF3F8",
 
31
 
32
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
33
 
34
+ # --- دیتابیس موقت حافظه برای کاربران رایگان ---
35
  # ساختار: { 'fingerprint_hash': {'date': '2023-10-27', 'count': 0} }
36
  USER_CREDITS_DB = {}
37
  MAX_FREE_DAILY_CREDITS = 5
38
+ PREMIUM_PAGE_ID = '1149636'
39
 
40
+ # --- مدل تشخیص محتوای نامناسب (NSFW) ---
41
  print("Loading Safety Checker...")
42
  safety_classifier = pipeline("image-classification", model="Falconsai/nsfw_image_detection", device=-1)
43
 
 
53
  print(f"Safety check error: {e}")
54
  return False
55
 
56
+ # --- لیست کلمات ممنوعه (انگلیسی) ---
57
  BANNED_WORDS = [
58
  "nsfw", "nude", "naked", "sex", "porn", "erotic", "xxx", "18+", "uncensored",
59
  "breast", "nipple", "genital", "vagina", "penis", "ass", "butt", "buttocks",
 
74
  def check_text_safety(text):
75
  if not text: return True
76
  text_lower = text.lower()
 
 
77
  for word in BANNED_WORDS:
 
78
  if f" {word} " in f" {text_lower} ":
79
  return False
 
80
  if word in ["porn", "hentai", "xxx", "nude", "sex"] and word in text_lower:
81
  return False
82
  return True
83
 
84
+ # --- بارگذاری مدل Qwen ---
85
  from diffusers import FlowMatchEulerDiscreteScheduler
86
  from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
87
  from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
 
102
  torch_dtype=dtype
103
  ).to(device)
104
 
105
+ # LoRA Loading
106
  pipe.load_lora_weights("autoweeb/Qwen-Image-Edit-2509-Photo-to-Anime", weight_name="Qwen-Image-Edit-2509-Photo-to-Anime_000001000.safetensors", adapter_name="anime")
107
  pipe.load_lora_weights("dx8152/Qwen-Edit-2509-Multiple-angles", weight_name="镜头转换.safetensors", adapter_name="multiple-angles")
108
  pipe.load_lora_weights("dx8152/Qwen-Image-Edit-2509-Light_restoration", weight_name="移除光影.safetensors", adapter_name="light-restoration")
 
126
  "افزایش کیفیت (Upscale)": "upscale-image"
127
  }
128
 
 
 
 
 
 
 
 
 
129
  ASPECT_RATIOS_MAP = {
130
  "خودکار (پیش‌فرض)": "Auto",
131
  "۱:۱ (مربع - 1024x1024)": (1024, 1024),
 
134
  "شخصی‌سازی (Custom)": "Custom"
135
  }
136
 
137
+ ASPECT_RATIOS_LIST = list(ASPECT_RATIOS_MAP.keys())
138
+
139
  def translate_prompt(text):
140
  if not text:
141
  return ""
 
158
  new_height = 1024
159
  aspect_ratio = original_width / original_height
160
  new_width = int(new_height * aspect_ratio)
161
+
162
+ # Ensure divisible by 8
163
  new_width = (new_width // 8) * 8
164
  new_height = (new_height // 8) * 8
165
  return new_width, new_height
166
 
 
 
 
 
 
 
167
  def get_error_html(message):
168
  return f"""
169
  <div style="background-color: #fee2e2; border: 1px solid #ef4444; color: #b91c1c; padding: 12px; border-radius: 8px; text-align: center; margin-bottom: 10px; font-weight: bold; display: flex; align-items: center; justify-content: center; gap: 8px; direction: rtl;">
 
180
  </div>
181
  """
182
 
183
+ # --- مدیریت اعتبار کاربر ---
184
+ def check_credits(fingerprint, is_premium):
185
+ if is_premium:
 
186
  return True, "Unlimited"
187
 
188
  if not fingerprint:
189
+ return True, "Unknown" # Fallback if fingerprint missing
 
 
190
 
191
  today_str = datetime.now().strftime("%Y-%m-%d")
192
 
 
193
  if fingerprint not in USER_CREDITS_DB:
194
  USER_CREDITS_DB[fingerprint] = {'date': today_str, 'count': 0}
195
 
196
  user_data = USER_CREDITS_DB[fingerprint]
197
 
 
198
  if user_data['date'] != today_str:
199
  user_data['date'] = today_str
200
  user_data['count'] = 0
201
 
 
202
  if user_data['count'] >= MAX_FREE_DAILY_CREDITS:
203
  return False, 0
204
 
205
+ # محاسبه اعتبار باقی‌مانده بدون کسر (کسر بعد از موفقیت انجام میشه)
 
206
  remaining = MAX_FREE_DAILY_CREDITS - user_data['count']
207
  return True, remaining
208
 
209
+ def deduct_credit(fingerprint, is_premium):
210
+ if is_premium or not fingerprint:
211
+ return
212
+
213
+ if fingerprint in USER_CREDITS_DB:
214
+ USER_CREDITS_DB[fingerprint]['count'] += 1
215
+
216
+ @spaces.GPU(duration=45)
217
  def infer(
218
  input_image,
219
  prompt,
 
225
  aspect_ratio_selection,
226
  custom_width,
227
  custom_height,
228
+ user_fingerprint,
229
+ user_is_premium,
230
  progress=gr.Progress(track_tqdm=True)
231
  ):
232
+ # --- رفع خطای NoneType vs Int ---
233
+ # اگر مقادیر عددی نال باشند، مقدار پیش‌فرض ست می‌کنیم
234
+ seed = seed if seed is not None else 0
235
+ steps = steps if steps is not None else 4
236
+ guidance_scale = guidance_scale if guidance_scale is not None else 1.0
237
+ custom_width = custom_width if custom_width is not None else 1024
238
+ custom_height = custom_height if custom_height is not None else 1024
239
+
240
+ # تبدیل وضعیت پرمیوم
241
  is_premium = str(user_is_premium).lower() == 'true'
242
 
243
  if input_image is None:
244
  return None, seed, get_error_html("لطفاً ابتدا یک تصویر بارگذاری کنید.")
245
 
246
+ # 1. بررسی اعتبار
247
+ allowed, credit_info = check_credits(user_fingerprint, is_premium)
248
  if not allowed:
249
+ msg = "اعتبار رایگان امروز شما (۵ تصویر) به پایان رسیده است."
 
250
  return None, seed, get_error_html(msg + " <span class='quota-limit-reached' style='display:none'></span>")
251
 
252
+ # 2. بررسی تصویر ورودی (NSFW)
253
  if is_image_nsfw(input_image):
254
+ return None, seed, get_error_html("تصویر ورودی دارای محتوای نامناسب است.")
255
 
256
+ # 3. بررسی متن
257
  english_prompt = translate_prompt(prompt)
258
  if not check_text_safety(english_prompt):
259
+ return None, seed, get_error_html("متن درخواست شامل کلمات غیرمجاز است.")
260
 
261
+ # تنظیمات مدل
262
+ adapter_name = LORA_MAPPING.get(lora_adapter_persian)
263
+ if adapter_name:
264
+ pipe.set_adapters([adapter_name], adapter_weights=[1.0])
265
 
266
  if randomize_seed:
267
  seed = random.randint(0, MAX_SEED)
268
 
269
  generator = torch.Generator(device=device).manual_seed(seed)
270
 
271
+ # ابعاد
272
+ dims = ASPECT_RATIOS_MAP.get(aspect_ratio_selection)
273
+ if dims == "Custom":
274
+ w = (int(custom_width) // 8) * 8
275
+ h = (int(custom_height) // 8) * 8
276
+ elif dims == "Auto" or dims is None:
277
+ w, h = update_dimensions_on_upload(input_image)
 
 
 
 
 
 
278
  else:
279
+ w, h = dims
280
 
281
  try:
282
+ final_negative = "nsfw, nude, naked, porn, sexual, xxx, breast, nipple, genital, vagina, penis, ass, lingerie, bikini, swimwear, underwear, fetish, topless, gore, violence, blood, bad anatomy, bad hands, missing fingers, blurry"
283
+
284
  result = pipe(
285
+ image=input_image.convert("RGB"),
286
  prompt=english_prompt,
287
+ negative_prompt=final_negative,
288
+ height=h,
289
+ width=w,
290
+ num_inference_steps=int(steps), # اطمینان از int بودن
291
  generator=generator,
292
+ true_cfg_scale=float(guidance_scale), # اطمینان از float بودن
293
  ).images[0]
294
 
295
+ # 4. بررسی خروجی (NSFW)
296
  if is_image_nsfw(result):
297
+ return None, seed, get_error_html("تصویر خروجی حاوی محتوای نامناسب بود و حذف شد.")
298
 
299
+ # کسر اعتبار پس از موفقیت
300
+ deduct_credit(user_fingerprint, is_premium)
301
+
302
+ # پیام موفقیت
303
+ success_msg = "تصویر با موفقیت ساخته شد."
304
  if not is_premium:
305
+ # محاسبه مجدد باقیمانده
306
+ _, new_rem = check_credits(user_fingerprint, is_premium)
307
+ success_msg += f"<br><small>اعتبار باقی‌مانده امروز: {new_rem}</small>"
308
 
309
  return result, seed, get_success_html(success_msg)
310
 
311
  except Exception as e:
312
  error_str = str(e)
313
+ if "quota" in error_str.lower(): raise e
314
+ return None, seed, get_error_html(f"خطا: {error_str}")
 
315
 
316
+ # --- تابع نمونه‌ها (بدون کسر اعتبار) ---
317
  def infer_example(input_image, prompt, lora_adapter):
318
+ # کاربر پرمیوم فیک برای اجرای نمونه‌ها
319
+ return infer(input_image, prompt, lora_adapter, 0, True, 1.0, 4, "خودکار (پیش‌فرض)", 1024, 1024, "test_fp", "true")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
 
321
+ # --- جاوااسکریپت و HTML ---
322
+ js_code = """
323
  <script>
324
+ // --- تنظیمات ---
325
  const PREMIUM_PAGE_ID = '1149636';
326
  const PREMIUM_URL = '#/nav/online/news/getSingle/1149636/eyJpdiI6InZSVUdlLzBlR0FzOHZJdXFZeWhER0E9PSIsInZhbHVlIjoiWFhqRXBLc29vSFpHdk9nYmRjZGVuWHRHRHVSZHRlTG1BUENLaE5mNXBNVVRGWFg3ZWN0djJ5K1dIY1RqTHJGaCIsIm1hYyI6IjIzYzFlZTMwYmVmMTdkYjQ0YTQ4YWMxNmFhN2RmNWQ2OTc1NDIyNGVlZGI3ZjJjMjhkNmQxNjM4MDFlZTIxNmUiLCJ0YWciOiIifQ==/20934991';
327
 
328
+ // --- تولید اثر انگشت ---
329
  function getBrowserFingerprint() {
330
  const components = [navigator.userAgent, navigator.language, screen.width + 'x' + screen.height, new Date().getTimezoneOffset()];
331
  try {
332
  const canvas = document.createElement('canvas');
333
  const ctx = canvas.getContext('2d');
334
+ ctx.textBaseline = "top"; ctx.font = "14px 'Arial'"; ctx.textBaseline = "alphabetic";
335
+ ctx.fillStyle = "#f60"; ctx.fillRect(125, 1, 62, 20);
336
+ ctx.fillStyle = "#069"; ctx.fillText("val", 2, 15);
 
 
 
 
337
  components.push(canvas.toDataURL());
338
+ } catch(e) { components.push("err"); }
339
+ const str = components.join('~~~');
 
 
340
  let hash = 0;
341
+ for (let i = 0; i < str.length; i++) {
342
+ hash = ((hash << 5) - hash) + str.charCodeAt(i);
 
343
  hash |= 0;
344
  }
345
  return 'fp_' + Math.abs(hash).toString(16);
346
  }
347
 
 
348
  let userFingerprint = getBrowserFingerprint();
349
  let isUserPremium = false;
350
 
351
+ // --- بروزرسانی UI وضعیت کاربر ---
352
+ function updateStatusUI(status) {
353
+ const badge = document.getElementById('status-badge-text');
354
+ const desc = document.getElementById('status-desc');
355
+ const upgradeBox = document.getElementById('upgrade-box');
 
 
356
 
357
  if (status === 'paid') {
358
  isUserPremium = true;
359
  if(badge) {
360
+ badge.innerText = 'نسخه نامحدود 💎';
361
+ badge.className = 'badge paid';
362
  }
363
+ if(desc) desc.innerText = 'وضعیت: کاربر ویژه (بدون محدودیت)';
364
+ if(upgradeBox) upgradeBox.style.display = 'none';
365
  } else {
366
  isUserPremium = false;
367
  if(badge) {
368
+ badge.innerText = 'نسخه رایگان';
369
+ badge.className = 'badge free';
370
  }
371
+ if(desc) desc.innerText = '۵ تصویر رایگان در روز دارید';
372
  }
373
+ syncHiddenInputs();
 
 
374
  }
375
 
376
+ function syncHiddenInputs() {
377
+ // پیدا کردن تکست‌باکس‌های مخفی گرادیو و پر کردن آن‌ها
378
+ const fpArea = document.querySelector('.hidden-fingerprint textarea');
379
+ const premArea = document.querySelector('.hidden-premium textarea');
 
380
 
381
+ if (fpArea) {
382
+ fpArea.value = userFingerprint;
383
+ fpArea.dispatchEvent(new Event('input', { bubbles: true }));
384
  }
385
+ if (premArea) {
386
+ premArea.value = isUserPremium ? "true" : "false";
387
+ premArea.dispatchEvent(new Event('input', { bubbles: true }));
388
  }
389
  }
390
 
391
+ function isUserPaid(userObject) {
392
+ return userObject && userObject.isLogin && userObject.accessible_pages &&
393
+ (userObject.accessible_pages.includes(PREMIUM_PAGE_ID) || userObject.accessible_pages.includes(parseInt(PREMIUM_PAGE_ID)));
394
+ }
395
+
396
+ // --- شنود پیام از اپلیکیشن مادر ---
397
  window.addEventListener('message', (event) => {
398
  if (event.data && event.data.type === 'USER_STATUS_RESPONSE') {
 
 
 
 
399
  try {
400
+ if (event.data.error || !event.data.payload) {
401
+ updateStatusUI('free');
402
+ } else {
403
+ const userObj = JSON.parse(event.data.payload);
404
+ updateStatusUI(isUserPaid(userObj) ? 'paid' : 'free');
405
+ }
406
+ } catch (e) { updateStatusUI('free'); }
407
  }
408
  });
409
 
410
+ // --- شروع کار ---
411
  document.addEventListener('DOMContentLoaded', () => {
412
+ // درخواست وضعیت کاربر به محض لود
 
 
 
 
 
 
 
 
 
 
 
 
 
413
  parent.postMessage({ type: 'REQUEST_USER_STATUS' }, '*');
414
 
415
+ // Fallback: اگر پاسخی نیامد بعد از 2 ثانیه رایگان فرض کن
416
  setTimeout(() => {
417
+ const badge = document.getElementById('status-badge-text');
418
+ if (!badge || badge.innerText === '') {
419
+ updateStatusUI('free');
420
  }
421
+ }, 2500);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
 
423
+ // اسکن برای پیام‌های خطا و لیمیت
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
  setInterval(() => {
425
+ // Quota Limit from Python
426
+ if (document.querySelector('.quota-limit-reached')) {
427
+ document.getElementById('upgrade-box').style.display = 'block';
428
+ document.querySelector('.quota-limit-reached').remove();
429
+ }
430
+
431
+ // Hide default Gradio toasts if needed
432
+ document.querySelectorAll('.toast-wrap').forEach(el => {
433
+ if (el.innerText.includes('quota') || el.innerText.includes('exceeded')) {
434
+ el.style.display = 'none';
435
+ showQuotaModal();
436
  }
437
  });
438
+ }, 500);
 
 
 
 
 
 
 
 
439
  });
440
 
441
+ // توابع کمکی
442
+ window.openPremium = function() {
443
+ parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM', payload: { url: PREMIUM_URL } }, '*');
444
+ };
445
+
446
+ window.prepareSubmission = function() {
447
+ syncHiddenInputs();
448
  return [];
449
  }
450
+
451
+ // دانلود
452
+ window.downloadImage = async (image) => {
453
+ if (!image) return alert("تصویری وجود ندارد!");
454
+ let url = image.url;
455
+ if (!url.startsWith('http')) url = window.location.origin + url;
456
+ window.parent.postMessage({ type: 'DOWNLOAD_REQUEST', url: url }, '*');
457
+ };
458
+
459
+ // مودال خطای IP
460
+ function showQuotaModal() {
461
+ if (document.getElementById('custom-quota-modal')) return;
462
+ const html = `<div id="custom-quota-modal" style="position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);z-index:99999;display:flex;justify-content:center;align-items:center;color:#fff;font-family:'Vazirmatn';">
463
+ <div style="background:#2d3748;padding:2rem;border-radius:1rem;text-align:center;max-width:90%;">
464
+ <h3>خطای محدودیت سرور</h3>
465
+ <p>لطفاً IP خود را تغییر دهید (VPN را خاموش/روشن کنید) و دوباره تلاش کنید.</p>
466
+ <button onclick="document.getElementById('custom-quota-modal').remove();" style="padding:10px 20px;margin-top:10px;border-radius:8px;border:none;background:#4a5568;color:white;cursor:pointer;">باشه</button>
467
+ </div>
468
+ </div>`;
469
+ document.body.insertAdjacentHTML('beforeend', html);
470
+ }
471
  </script>
 
472
 
 
 
473
  <style>
474
+ @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;700&display=swap');
475
 
476
+ body { font-family: 'Vazirmatn', sans-serif !important; }
 
 
 
 
 
 
 
 
 
 
 
477
 
478
+ /* استایل بج و هدر */
479
+ .user-info-header {
480
  display: flex;
481
  flex-direction: column;
482
  align-items: center;
483
  justify-content: center;
484
+ background: #ffffff;
485
+ padding: 15px;
486
+ border-radius: 16px;
487
  margin-bottom: 20px;
488
+ box-shadow: 0 4px 15px rgba(0,0,0,0.05);
489
+ border: 1px solid #e2e8f0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
490
  }
491
 
492
+ .badge {
493
+ padding: 6px 18px;
494
+ border-radius: 50px;
495
+ font-size: 0.95rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
496
  font-weight: 800;
497
+ margin-bottom: 8px;
498
+ display: inline-block;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
  }
500
 
501
+ .badge.free {
502
+ background: #e2e8f0;
503
+ color: #4a5568;
504
+ border: 1px solid #cbd5e0;
 
 
 
 
505
  }
506
 
507
+ .badge.paid {
508
+ background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
509
+ color: #744210;
510
+ box-shadow: 0 4px 12px rgba(255, 165, 0, 0.3);
511
  }
512
 
513
+ #status-desc {
514
+ font-size: 0.9rem;
515
+ color: #718096;
516
  }
517
 
518
+ /* دکمه ارتقا */
519
+ #upgrade-box {
 
 
 
 
 
 
 
 
 
520
  width: 100%;
521
+ margin-top: 12px;
522
+ display: none; /* پیش‌فرض مخفی */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
523
  }
524
 
525
+ .upgrade-btn {
526
+ width: 100%;
527
+ background: linear-gradient(90deg, #F59E0B, #D97706);
528
+ color: white;
529
+ border: none;
530
+ padding: 12px;
531
+ border-radius: 12px;
532
+ font-weight: 700;
533
+ cursor: pointer;
534
+ font-family: inherit;
535
+ animation: pulse 2s infinite;
536
  }
537
+ @keyframes pulse {
538
+ 0% { transform: scale(1); }
539
+ 50% { transform: scale(1.02); }
540
+ 100% { transform: scale(1); }
 
541
  }
542
 
543
+ .hidden-fingerprint, .hidden-premium { display: none !important; }
 
 
 
544
 
545
+ /* مخفی کردن فوتر پیشفرض */
546
  footer { display: none !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
547
  </style>
548
  """
549
 
550
+ with gr.Blocks(theme=Soft()) as demo:
551
+ gr.HTML(js_code)
 
 
 
 
 
552
 
553
  with gr.Column(elem_id="col-container"):
554
  gr.Markdown("# **ویرایشگر هوشمند آلفا**", elem_id="main-title")
 
 
 
 
555
 
556
+ # بخش نمایش وضعیت کاربر
557
  gr.HTML("""
558
+ <div class="user-info-header">
559
+ <span id="status-badge-text" class="badge"></span>
560
+ <span id="status-desc">در حال بررسی اشتراک...</span>
561
+ <div id="upgrade-box">
562
+ <button class="upgrade-btn" onclick="openPremium()">⭐️ ارتقا به نسخه نامحدود</button>
 
 
563
  </div>
564
  </div>
565
  """)
566
+
567
+ # ورودی‌های مخفی برای ارتباط JS -> Python
568
+ user_fingerprint = gr.Textbox(elem_classes="hidden-fingerprint", visible=False)
569
+ user_is_premium = gr.Textbox(elem_classes="hidden-premium", visible=False)
570
 
571
+ with gr.Row():
 
 
 
 
572
  with gr.Column():
573
+ input_image = gr.Image(label="تصویر اصلی", type="pil", height=320)
574
+ prompt = gr.Text(label="دستور ویرایش (فارسی)", placeholder="مثلاً: تصویر را کارتونی کن", rtl=True, lines=3)
575
+ run_btn = gr.Button("✨ شروع پردازش", variant="primary")
 
 
 
 
 
 
 
 
576
 
 
 
 
577
  with gr.Column():
578
+ output_image = gr.Image(label="نتیجه", interactive=False)
579
+ dl_btn = gr.Button("📥 دانلود تصویر")
 
 
 
 
 
 
 
 
580
 
581
+ with gr.Accordion("تنظیمات پیشرفته", open=False):
582
+ lora_adapter = gr.Dropdown(list(LORA_MAPPING.keys()), label="سبک", value="تبدیل عکس به انیمه")
583
+ ratio = gr.Dropdown(ASPECT_RATIOS_LIST, label="ابعاد", value="خودکار (پیش‌فرض)")
 
 
 
 
584
 
585
+ with gr.Row(visible=False) as custom_row:
586
+ w_slider = gr.Slider(256, 2048, value=1024, step=8, label="عرض")
587
+ h_slider = gr.Slider(256, 2048, value=1024, step=8, label="ارتفاع")
588
+
589
+ ratio.change(lambda x: gr.update(visible=(x == "شخصی‌سازی (Custom)")), ratio, custom_row)
 
 
 
 
590
 
591
+ seed = gr.Slider(0, MAX_SEED, label="Seed", value=0)
592
+ rand_seed = gr.Checkbox(True, label="Seed تصادفی")
593
+ cfg = gr.Slider(1.0, 10.0, value=5.0, label="Guidance Scale") # مقدار پیشفرض مطمئن
594
+ steps = gr.Slider(1, 50, value=30, label="Steps") # مقدار پیشفرض مطمئن
595
 
596
+ status_box = gr.HTML()
 
 
 
 
 
 
 
 
 
 
597
 
598
+ # اتصال دکمه‌ها
599
+ run_btn.click(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
600
  fn=infer,
601
  inputs=[
602
+ input_image, prompt, lora_adapter, seed, rand_seed, cfg, steps,
603
+ ratio, w_slider, h_slider, user_fingerprint, user_is_premium
 
604
  ],
605
  outputs=[output_image, seed, status_box],
606
+ js="window.prepareSubmission" # فراخوانی JS قبل از اجرا برای سینک کردن وضعیت
 
 
 
 
 
 
 
 
607
  )
608
+
609
+ dl_btn.click(fn=None, inputs=[output_image], js="window.downloadImage")
610
 
611
  if __name__ == "__main__":
612
+ demo.queue().launch(show_error=True)