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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +253 -442
app.py CHANGED
@@ -4,14 +4,21 @@ import numpy as np
4
  import spaces
5
  import torch
6
  import random
7
- from PIL import Image, ImageFilter
8
- from typing import Iterable
9
- from gradio.themes import Soft
10
- from gradio.themes.utils import colors, fonts, sizes
 
11
  from deep_translator import GoogleTranslator
12
  from transformers import pipeline
13
 
14
- # --- تعریف تم ---
 
 
 
 
 
 
15
  colors.steel_blue = colors.Color(
16
  name="steel_blue",
17
  c50="#EBF3F8",
@@ -29,7 +36,7 @@ colors.steel_blue = colors.Color(
29
 
30
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
31
 
32
- # --- بارگذاری مدل تشخیص محتوای نامناسب (NSFW) ---
33
  print("Loading Safety Checker...")
34
  safety_classifier = pipeline("image-classification", model="Falconsai/nsfw_image_detection", device=-1)
35
 
@@ -45,7 +52,7 @@ def is_image_nsfw(image):
45
  print(f"Safety check error: {e}")
46
  return False
47
 
48
- # --- بارگذاری مدل اصلی ---
49
  from diffusers import FlowMatchEulerDiscreteScheduler
50
  from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
51
  from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
@@ -90,84 +97,35 @@ LORA_MAPPING = {
90
  "افزایش کیفیت (Upscale)": "upscale-image"
91
  }
92
 
93
- ASPECT_RATIOS_LIST = [
94
- "خودکار (پیش‌فرض)",
95
- "۱:۱ (مربع - 1024x1024)",
96
- "۱۶:۹ (افقی - 1344x768)",
97
- "۹:۱۶ (عمودی - 768x1344)",
98
- "شخصی‌سازی (Custom)"
99
- ]
100
-
101
- ASPECT_RATIOS_MAP = {
102
- "خودکار (پیش‌فرض)": "Auto",
103
- "۱:۱ (مربع - 1024x1024)": (1024, 1024),
104
- "۱۶:۹ (افقی - 1344x768)": (1344, 768),
105
- "۹:۱۶ (عمودی - 768x1344)": (768, 1344),
106
- "شخصی‌سازی (Custom)": "Custom"
107
- }
108
-
109
- BANNED_WORDS = [
110
- "nude", "naked", "sex", "porn", "undressed", "nsfw", "erotic", "xxx",
111
- "breast", "nipple", "genital", "vagina", "penis", "ass", "butt", "sexual",
112
- "lingerie", "bikini", "swimwear", "underwear", "fetish", "topless",
113
- "exhibitionism", "hentai", "ecchi", "18+"
114
- ]
115
 
116
  def check_text_safety(text):
117
  text_lower = text.lower()
118
  for word in BANNED_WORDS:
119
- if f" {word} " in f" {text_lower} ":
120
- return False
121
  return True
122
 
123
  def translate_prompt(text):
124
- if not text:
125
- return ""
126
- try:
127
- translated = GoogleTranslator(source='auto', target='en').translate(text)
128
- return translated
129
- except Exception as e:
130
- print(f"Translation Error: {e}")
131
- return text
132
 
133
  def update_dimensions_on_upload(image):
134
- if image is None:
135
- return 1024, 1024
136
- original_width, original_height = image.size
137
- if original_width > original_height:
138
- new_width = 1024
139
- aspect_ratio = original_height / original_width
140
- new_height = int(new_width * aspect_ratio)
141
- else:
142
- new_height = 1024
143
- aspect_ratio = original_width / original_height
144
- new_width = int(new_height * aspect_ratio)
145
- new_width = (new_width // 8) * 8
146
- new_height = (new_height // 8) * 8
147
- return new_width, new_height
148
-
149
- def update_sliders_visibility(choice):
150
- if choice == "شخصی‌سازی (Custom)":
151
- return gr.update(visible=True), gr.update(visible=True)
152
- else:
153
- return gr.update(visible=False), gr.update(visible=False)
154
 
155
  def get_error_html(message):
156
- return f"""
157
- <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;">
158
- <span style="font-size: 1.2em;">⛔</span>
159
- {message}
160
- </div>
161
- """
162
 
163
  def get_success_html(message):
164
- return f"""
165
- <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;">
166
- <span style="font-size: 1.2em;">✅</span>
167
- {message}
168
- </div>
169
- """
170
 
 
171
  @spaces.GPU(duration=30)
172
  def infer(
173
  input_image,
@@ -180,270 +138,176 @@ def infer(
180
  aspect_ratio_selection,
181
  custom_width,
182
  custom_height,
 
 
 
183
  progress=gr.Progress(track_tqdm=True)
184
  ):
185
- if input_image is None:
186
- return None, seed, get_error_html("لطفاً ابتدا یک تصویر بارگذاری کنید.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
 
188
- if is_image_nsfw(input_image):
189
- return None, seed, get_error_html("تصویر ورودی دارای محتوای نامناسب است و پردازش نمی‌شود.")
 
190
 
191
  english_prompt = translate_prompt(prompt)
192
- if not check_text_safety(english_prompt):
193
- return None, seed, get_error_html("متن درخواست شامل کلمات غیرمجاز یا غیراخلاقی است.")
194
 
195
  adapter_internal_name = LORA_MAPPING.get(lora_adapter_persian)
196
- if adapter_internal_name:
197
- pipe.set_adapters([adapter_internal_name], adapter_weights=[1.0])
198
 
199
- if randomize_seed:
200
- seed = random.randint(0, MAX_SEED)
201
-
202
  generator = torch.Generator(device=device).manual_seed(seed)
203
 
204
- safety_negative = "nsfw, nude, naked, porn, sexual, xxx, breast, nipple, genital, vagina, penis, ass, lingerie, bikini, swimwear, underwear, fetish, topless, gore, violence, blood"
205
- 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"
206
- final_negative_prompt = f"{safety_negative}, {base_negative}"
207
 
208
  original_image = input_image.convert("RGB")
209
-
210
  selection_value = ASPECT_RATIOS_MAP.get(aspect_ratio_selection)
211
-
212
- if selection_value == "Custom":
213
- width = (int(custom_width) // 8) * 8
214
- height = (int(custom_height) // 8) * 8
215
- elif selection_value == "Auto" or selection_value is None:
216
- width, height = update_dimensions_on_upload(original_image)
217
- else:
218
- width, height = selection_value
219
 
220
  try:
221
  result = pipe(
222
- image=original_image,
223
- prompt=english_prompt,
224
- negative_prompt=final_negative_prompt,
225
- height=height,
226
- width=width,
227
- num_inference_steps=steps,
228
- generator=generator,
229
- true_cfg_scale=guidance_scale,
230
  ).images[0]
231
 
232
- if is_image_nsfw(result):
233
- return None, seed, get_error_html("تصویر تولید شده حاوی محتوای نامناسب بود و حذف شد.")
234
 
235
  return result, seed, get_success_html("تصویر با موفقیت ویرایش شد.")
236
 
237
  except Exception as e:
238
  error_str = str(e)
239
- if "quota" in error_str.lower() or "exceeded" in error_str.lower():
240
- raise e
241
  return None, seed, get_error_html(f"خطا در پردازش: {error_str}")
242
 
243
  @spaces.GPU(duration=30)
244
  def infer_example(input_image, prompt, lora_adapter):
245
- res, s, status = infer(input_image, prompt, lora_adapter, 0, True, 1.0, 4, "خودکار (پیش‌فرض)", 1024, 1024)
 
246
  return res, s, status
247
 
248
- # --- جاوااسکریپت برای دکمه دانلود ---
 
 
249
  js_download_func = """
250
  async (image) => {
251
- if (!image) {
252
- alert("لطفاً ابتدا تصویر را تولید کنید.");
253
- return;
254
- }
255
  let fileUrl = image.url;
256
- if (fileUrl && !fileUrl.startsWith('http')) {
257
- fileUrl = window.location.origin + fileUrl;
258
- } else if (!fileUrl && image.path) {
259
- fileUrl = window.location.origin + "/file=" + image.path;
260
- }
261
- window.parent.postMessage({
262
- type: 'DOWNLOAD_REQUEST',
263
- url: fileUrl
264
- }, '*');
265
  }
266
  """
267
 
268
- # --- منطق جاوااسکریپت برای مدیریت اعتبار و اشتراک ---
269
- js_credit_logic = """
270
  <script>
271
- document.addEventListener('DOMContentLoaded', () => {
272
- const PREMIUM_PAGE_ID = '1149636';
273
- const PREMIUM_URL = '#/nav/online/news/getSingle/1149636/eyJpdiI6InZSVUdlLzBlR0FzOHZJdXFZeWhER0E9PSIsInZhbHVlIjoiWFhqRXBLc29vSFpHdk9nYmRjZGVuWHRHRHVSZHRlTG1BUENLaE5mNXBNVVRGWFg3ZWN0djJ5K1dIY1RqTHJGaCIsIm1hYyI6IjIzYzFlZTMwYmVmMTdkYjQ0YTQ4YWMxNmFhN2RmNWQ2OTc1NDIyNGVlZGI3ZjJjMjhkNmQxNjM4MDFlZTIxNmUiLCJ0YWciOiIifQ==/20934991';
274
-
275
- let isUserPaid = false;
276
- let dailyCredits = 5;
277
-
278
- // تابع فینگرپرینت ساده برای شناسایی مرورگر
279
- function getFingerprint() {
280
- const str = navigator.userAgent + navigator.language + screen.width + 'x' + screen.height;
281
- let hash = 0;
282
- for (let i = 0; i < str.length; i++) {
283
- hash = ((hash << 5) - hash) + str.charCodeAt(i);
284
- hash |= 0;
285
- }
286
- return 'fp_' + Math.abs(hash).toString(16);
287
- }
 
 
 
 
 
288
 
289
- const fingerprint = getFingerprint();
290
- const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
291
- const storageKey = `app_credits_${fingerprint}_${today}`;
292
-
293
- function checkCredits() {
294
- const stored = localStorage.getItem(storageKey);
295
- if (stored === null) {
296
- dailyCredits = 5;
297
- localStorage.setItem(storageKey, 5);
298
- } else {
299
- dailyCredits = parseInt(stored);
300
- }
301
- updateUI();
302
- }
303
 
304
- function updateUI() {
305
- const badgeEl = document.getElementById('sub-badge');
306
- const creditTextEl = document.getElementById('credit-text');
307
- const runBtn = document.getElementById('run-btn');
308
- const upgradeBtn = document.getElementById('upgrade-btn');
309
-
310
- if (!badgeEl || !runBtn) return;
311
-
312
- if (isUserPaid) {
313
- // حالت پولی
314
- badgeEl.innerHTML = '<span class="paid-badge">✨ نسخه نامحدود</span>';
315
- creditTextEl.style.display = 'none';
316
- runBtn.style.display = 'block';
317
- if(upgradeBtn) upgradeBtn.style.display = 'none';
318
- } else {
319
- // حالت رایگان
320
- badgeEl.innerHTML = '<span class="free-badge">نسخه رایگان</span>';
321
- creditTextEl.style.display = 'block';
322
- creditTextEl.innerHTML = `اعتبار باقی‌مانده امروز: <b>${dailyCredits}</b>`;
323
-
324
- if (dailyCredits > 0) {
325
- runBtn.style.display = 'block';
326
- if(upgradeBtn) upgradeBtn.style.display = 'none';
327
- } else {
328
- runBtn.style.display = 'none';
329
- if(upgradeBtn) upgradeBtn.style.display = 'flex';
330
- creditTextEl.innerHTML = `<span style="color:#ef4444">اعتبار امروز شما تمام شده است!</span>`;
331
- }
332
- }
333
  }
 
 
 
 
 
 
 
 
 
334
 
335
- // بررسی وضعیت کاربر از اپلیکیشن
336
  window.addEventListener('message', (event) => {
337
  if (event.data && event.data.type === 'USER_STATUS_RESPONSE') {
338
- try {
339
- const userObject = JSON.parse(event.data.payload);
340
- if (userObject && userObject.isLogin && userObject.accessible_pages &&
341
- (userObject.accessible_pages.includes(PREMIUM_PAGE_ID) ||
342
- userObject.accessible_pages.includes(parseInt(PREMIUM_PAGE_ID)))) {
343
- isUserPaid = true;
344
- } else {
345
- isUserPaid = false;
346
- }
347
- updateUI();
348
- } catch (e) {
349
- console.error("Error parsing user status", e);
350
- isUserPaid = false;
351
- updateUI();
352
  }
353
  }
354
- });
355
-
356
- // درخواست وضعیت کاربر هنگام لود
357
- parent.postMessage({ type: 'REQUEST_USER_STATUS' }, '*');
358
-
359
- // لود اولیه کردیت
360
- checkCredits();
361
-
362
- // تابع سراسری برای کسر اعتبار (قبل از اجرای پایتون)
363
- window.checkAndDeductCredit = async function() {
364
- if (isUserPaid) return []; // اگر پولی بود، اجازه بده
365
-
366
- checkCredits(); // رفرش کردن مقدار
367
- if (dailyCredits > 0) {
368
- dailyCredits--;
369
- localStorage.setItem(storageKey, dailyCredits);
370
- updateUI();
371
- return []; // اجازه اجرا
372
- } else {
373
- alert("اعتبار امروز شما به پایان رسیده است. لطفاً برای استفاده نامحدود ارتقا دهید.");
374
- throw new Error("Credit Exceeded"); // جلوگیری از اجرا
375
  }
376
- };
377
-
378
- // تابع برای دکمه ارتقا
379
- window.navigateToPremium = function() {
380
- parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM', payload: { url: PREMIUM_URL } }, '*');
381
- };
382
-
383
- // Force UI Refresh periodically
384
- setInterval(updateUI, 2000);
385
- });
386
- </script>
387
- """
388
 
389
- # --- جاوااسکریپت برای مدیریت خطا و تم (کد قبلی شما با کمی اصلاح) ---
390
- js_global_content = """
391
- <script>
392
- document.addEventListener('DOMContentLoaded', () => {
393
- // 1. Force Light Mode
394
  const forceLight = () => {
395
  const body = document.querySelector('body');
396
- if (body) {
397
- body.classList.remove('dark');
398
- body.style.backgroundColor = '#f5f7fa';
399
- body.style.color = '#333333';
400
- }
401
  document.querySelectorAll('.dark').forEach(el => el.classList.remove('dark'));
402
  };
403
- forceLight();
404
- setInterval(forceLight, 1000);
405
-
406
- // 2. RETRY FUNCTION
407
- window.retryGeneration = function() {
408
- const modal = document.getElementById('custom-quota-modal');
409
- if (modal) modal.remove();
410
- // تلاش مجدد نباید اعتبار کسر کند یا باید مدیریت شود، اما اینجا دکمه اصلی را کلیک می‌کنیم
411
- // چون تابع checkAndDeductCredit روی دکمه است، دوباره اعتبار چک می‌شود.
412
- const runBtn = document.getElementById('run-btn');
413
- if(runBtn) runBtn.click();
414
- };
415
-
416
- // Close function
417
- window.closeErrorModal = function() {
418
- const modal = document.getElementById('custom-quota-modal');
419
- if (modal) modal.remove();
420
- };
421
-
422
- // 3. SHOW MODAL FUNCTION (Quota Exceeded)
423
- const showQuotaModal = () => {
424
- if (document.getElementById('custom-quota-modal')) return;
425
-
426
- const modalHtml = `
427
- <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;">
428
- <div class="ip-reset-guide-container">
429
- <div class="guide-header">
430
- <h2>یک قدم تا ساخت تصاویر جدید</h2>
431
- <p>نیازمند تغییر نقطه دستیابی</p>
432
- </div>
433
- <div class="guide-content">
434
- <p>لطفاً IP خود را تغییر دهید (خاموش/روشن کردن حالت پرواز) و دوباره تلاش کنید.</p>
435
- <div class="guide-actions">
436
- <button class="action-button back-button" onclick="window.closeErrorModal()">بازگشت</button>
437
- <button class="action-button retry-button" onclick="window.retryGeneration()">تلاش مجدد</button>
438
- </div>
439
- </div>
440
- </div>
441
- </div>
442
- `;
443
- document.body.insertAdjacentHTML('beforeend', modalHtml);
444
- };
445
 
446
- // 4. SCANNER
447
  setInterval(() => {
448
  const potentialErrors = document.querySelectorAll('.toast-body, .error, .toast-wrap, .eta-bar, div[class*="error"]');
449
  potentialErrors.forEach(el => {
@@ -453,220 +317,167 @@ document.addEventListener('DOMContentLoaded', () => {
453
  el.style.display = 'none';
454
  }
455
  });
456
- }, 1000);
457
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
458
  </script>
459
  """
460
 
461
- # --- CSS Updated ---
462
  css_code = """
463
  <style>
464
  @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700&display=swap');
465
 
 
466
  :root, .dark, body, .gradio-container {
467
- --body-background-fill: #f5f7fa !important;
468
- --body-text-color: #1f2937 !important;
469
  font-family: 'Vazirmatn', sans-serif !important;
470
  }
471
 
472
- /* Badge Styles */
473
- .paid-badge {
474
- background: linear-gradient(45deg, #FFC107, #ffca2c);
475
- color: #333;
476
- padding: 6px 12px;
477
- border-radius: 20px;
478
- font-size: 0.9em;
479
- font-weight: 800;
480
- box-shadow: 0 4px 10px rgba(255, 193, 7, 0.3);
481
- display: inline-block;
482
- }
483
-
484
- .free-badge {
485
- background: linear-gradient(45deg, #6c757d, #495057);
486
- color: white;
487
- padding: 6px 12px;
488
- border-radius: 20px;
489
- font-size: 0.9em;
490
- font-weight: 700;
491
- box-shadow: 0 4px 10px rgba(108, 117, 125, 0.3);
492
- display: inline-block;
493
  }
494
-
495
- #subscription-status-container {
496
- text-align: center;
497
- margin-bottom: 20px;
498
- padding: 10px;
499
- background: #fff;
500
- border-radius: 12px;
501
- border: 1px solid #e5e7eb;
502
  }
503
-
504
- #credit-text {
505
- margin-top: 8px;
506
- color: #4b5563;
507
- font-size: 0.95em;
508
  }
509
-
510
- #upgrade-btn {
511
- display: none; /* Controlled by JS */
512
- width: 100%;
513
- margin-top: 10px;
514
- padding: 12px;
515
- font-family: 'Vazirmatn', sans-serif;
516
- font-size: 1.1em;
517
- font-weight: 800;
518
- color: #212529;
519
- background: linear-gradient(95deg, #FFD54F, #FFC107 100%);
520
- border: none;
521
- border-radius: 14px;
522
- cursor: pointer;
523
- box-shadow: 0 8px 20px -5px rgba(255, 193, 7, 0.4);
524
- animation: upgrade-pulse 2.5s infinite;
525
- justify-content: center;
526
- align-items: center;
527
  }
 
528
 
529
- @keyframes upgrade-pulse {
530
- 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 193, 7, 0.7); }
531
- 70% { transform: scale(1.02); box-shadow: 0 0 0 10px rgba(255, 193, 7, 0); }
532
- 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 193, 7, 0); }
533
  }
534
-
535
- /* Modal and other existing styles... */
536
- .ip-reset-guide-container {
537
  background: white; padding: 20px; border-radius: 16px; width: 90%; max-width: 400px; text-align: right; direction: rtl;
538
  }
539
- .action-button { margin: 5px; padding: 10px 20px; border-radius: 8px; cursor: pointer; border: none; }
540
- .retry-button { background: #667eea; color: white; }
541
- .back-button { background: #f0f0f0; }
542
-
543
- #main-title h1 {
544
- font-size: 2.2em !important;
545
- text-align: center;
546
- color: #1a202c !important;
547
- background: -webkit-linear-gradient(45deg, #2563eb, #1e40af);
548
- -webkit-background-clip: text;
549
- -webkit-text-fill-color: transparent;
550
- }
551
  </style>
552
  """
553
 
554
- # ادغام CSS و JS ها
555
- combined_html = css_code + js_global_content + js_credit_logic
556
 
557
- # استفاده از gr.Blocks
558
- with gr.Blocks() as demo:
559
- # تزریق کدها به عنوان HTML
560
  gr.HTML(combined_html)
561
 
562
  with gr.Column(elem_id="col-container"):
563
  gr.Markdown("# **ویرایشگر هوشمند آلفا**", elem_id="main-title")
564
- gr.Markdown(
565
- "با هوش مصنوعی آلفا تصاویر تونو به مدل های مختلف ویرایش کنید.",
566
- elem_id="main-description"
567
- )
568
-
569
- # --- بخش وضعیت اشتراک و اعتبار ---
570
  gr.HTML("""
571
- <div id="subscription-status-container">
572
- <div id="sub-badge"></div>
573
- <div id="credit-text">در حال بررسی اعتبار...</div>
574
- <button id="upgrade-btn" onclick="window.navigateToPremium()">⭐️ ارتقا به نسخه کامل و نامحدود</button>
 
 
 
575
  </div>
576
  """)
577
- # ------------------------------------
578
 
579
  with gr.Row(equal_height=True):
580
  with gr.Column():
581
  input_image = gr.Image(label="بارگذاری تصویر", type="pil", height=320)
582
-
583
- prompt = gr.Text(
584
- label="دستور ویرایش (به فارسی)",
585
- show_label=True,
586
- placeholder="مثال: تصویر را به سبک انیمه تبدیل کن...",
587
- rtl=True,
588
- lines=3
589
- )
590
-
591
  status_box = gr.HTML(label="وضعیت")
592
 
593
- # دکمه اصلی پردازش (elem_id="run-btn" مهم است)
594
  run_button = gr.Button("✨ شروع پردازش و ساخت تصویر", variant="primary", elem_classes="primary-btn", elem_id="run-btn")
595
 
596
  with gr.Column():
597
  output_image = gr.Image(label="تصویر نهایی", interactive=False, format="png", height=380)
 
598
 
599
- download_button = gr.Button("📥 دانلود و ذخیره تصویر", variant="secondary", elem_id="download-btn", elem_classes="primary-btn")
600
-
601
- with gr.Row():
602
- lora_adapter = gr.Dropdown(
603
- label="انتخاب سبک ویرایش (LoRA)",
604
- choices=list(LORA_MAPPING.keys()),
605
- value="تبدیل عکس به انیمه"
606
- )
607
 
608
- with gr.Accordion("تنظیمات پیشرفته", open=False, visible=True):
609
- aspect_ratio_selection = gr.Dropdown(
610
- label="ابعاد تصویر خروجی",
611
- choices=ASPECT_RATIOS_LIST,
612
- value="خودکار (پیش‌فرض)",
613
- interactive=True
614
- )
615
-
616
  with gr.Row(visible=False) as custom_dims_row:
617
- custom_width = gr.Slider(
618
- label="عرض دلخواه (Width)",
619
- minimum=256, maximum=2048, step=8, value=1024
620
- )
621
- custom_height = gr.Slider(
622
- label="ارتفاع دلخواه (Height)",
623
- minimum=256, maximum=2048, step=8, value=1024
624
- )
625
 
626
- seed = gr.Slider(label="دانه تصادفی (Seed)", minimum=0, maximum=MAX_SEED, step=1, value=0)
627
- randomize_seed = gr.Checkbox(label="استفاده از Seed تصادفی", value=True)
628
- guidance_scale = gr.Slider(label="میزان وفاداری به متن (Guidance Scale)", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
629
- steps = gr.Slider(label="تعداد مراحل پردازش (Steps)", minimum=1, maximum=50, step=1, value=4)
630
 
631
- def toggle_row(choice):
632
- if choice == "شخصی‌سازی (Custom)":
633
- return gr.update(visible=True)
634
- return gr.update(visible=False)
635
-
636
- aspect_ratio_selection.change(
637
- fn=toggle_row,
638
- inputs=aspect_ratio_selection,
639
- outputs=custom_dims_row
640
- )
641
 
 
642
  gr.Examples(
643
  examples=[
644
  ["examples/1.jpg", "تبدیل به انیمه کن.", "تبدیل عکس به انیمه"],
645
- ["examples/5.jpg", "سایه‌ها را حذف کن و نورپردازی نرم به تصویر بده.", "اصلاح نور و سایه"],
646
- ["examples/4.jpg", "از فیلتر ساعت طلایی با پخش نور ملایم استفاده کن.", "نورپردازی مجدد (Relight)"],
647
  ],
648
  inputs=[input_image, prompt, lora_adapter],
649
  outputs=[output_image, seed, status_box],
650
  fn=infer_example,
651
- cache_examples=False,
652
- label="نمونه‌ها (برای تست کلیک کنید)"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
653
  )
654
 
655
- # اتصال دکمه با تابع JS برای بررسی اعتبار قبل از اجرا
656
- run_button.click(
657
- fn=infer,
658
- inputs=[input_image, prompt, lora_adapter, seed, randomize_seed, guidance_scale, steps, aspect_ratio_selection, custom_width, custom_height],
659
- outputs=[output_image, seed, status_box],
660
- api_name="predict",
661
- js="window.checkAndDeductCredit" # این خط مهم است: ابتدا JS اجرا می‌شود
662
- )
663
-
664
- download_button.click(
665
- fn=None,
666
- inputs=[output_image],
667
- outputs=None,
668
- js=js_download_func
669
- )
670
 
671
  if __name__ == "__main__":
672
  demo.queue(max_size=30).launch(show_error=True)
 
4
  import spaces
5
  import torch
6
  import random
7
+ import json
8
+ import time
9
+ from datetime import date
10
+ from PIL import Image
11
+ from gradio.themes.utils import colors
12
  from deep_translator import GoogleTranslator
13
  from transformers import pipeline
14
 
15
+ # --- 1. تنظیمات سیستم اعتبار و اشتراک ---
16
+ USAGE_LIMIT = 5 # اعتبار رایگان روزانه
17
+ PREMIUM_PAGE_ID = '1149636' # آی‌دی صفحه محصول پریمیم
18
+ PREMIUM_URL = '#/nav/online/news/getSingle/1149636' # لینک خرید اشتراک
19
+ usage_data_cache = {} # حافظه موقت سرور برای نگهداری تعداد استفاده
20
+
21
+ # --- 2. تنظیمات تم و مدل‌ها ---
22
  colors.steel_blue = colors.Color(
23
  name="steel_blue",
24
  c50="#EBF3F8",
 
36
 
37
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
38
 
39
+ # --- بارگذاری مدل Safety Checker ---
40
  print("Loading Safety Checker...")
41
  safety_classifier = pipeline("image-classification", model="Falconsai/nsfw_image_detection", device=-1)
42
 
 
52
  print(f"Safety check error: {e}")
53
  return False
54
 
55
+ # --- بارگذاری مدل Qwen ---
56
  from diffusers import FlowMatchEulerDiscreteScheduler
57
  from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
58
  from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
 
97
  "افزایش کیفیت (Upscale)": "upscale-image"
98
  }
99
 
100
+ ASPECT_RATIOS_LIST = ["خودکار (پیش‌فرض)", "۱:۱ (مربع - 1024x1024)", "۱۶:۹ (افقی - 1344x768)", "۹:۱۶ (عمودی - 768x1344)", "شخصی‌سازی (Custom)"]
101
+ ASPECT_RATIOS_MAP = {"خودکار (پیش‌فرض)": "Auto", "۱:۱ (مربع - 1024x1024)": (1024, 1024), "۱۶:۹ (افقی - 1344x768)": (1344, 768), "۹:۱۶ (عمودی - 768x1344)": (768, 1344), "شخصی‌سازی (Custom)": "Custom"}
102
+ BANNED_WORDS = ["nude", "naked", "sex", "porn", "undressed", "nsfw", "erotic", "xxx", "breast", "nipple", "genital", "vagina", "penis", "ass", "butt", "sexual", "lingerie", "bikini", "swimwear", "underwear", "fetish", "topless", "exhibitionism", "hentai", "ecchi", "18+"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
  def check_text_safety(text):
105
  text_lower = text.lower()
106
  for word in BANNED_WORDS:
107
+ if f" {word} " in f" {text_lower} ": return False
 
108
  return True
109
 
110
  def translate_prompt(text):
111
+ if not text: return ""
112
+ try: return GoogleTranslator(source='auto', target='en').translate(text)
113
+ except: return text
 
 
 
 
 
114
 
115
  def update_dimensions_on_upload(image):
116
+ if image is None: return 1024, 1024
117
+ w, h = image.size
118
+ if w > h: new_w = 1024; ar = h/w; new_h = int(new_w * ar)
119
+ else: new_h = 1024; ar = w/h; new_w = int(new_h * ar)
120
+ return (new_w // 8) * 8, (new_h // 8) * 8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
  def get_error_html(message):
123
+ return f"""<div style="background-color: #fee2e2; border: 1px solid #ef4444; color: #b91c1c; padding: 12px; border-radius: 8px; text-align: center; margin-bottom: 10px; font-weight: bold; display: flex; align-items: center; justify-content: center; gap: 8px;"><span style="font-size: 1.2em;">⛔</span>{message}</div>"""
 
 
 
 
 
124
 
125
  def get_success_html(message):
126
+ return f"""<div style="background-color: #dcfce7; border: 1px solid #22c55e; color: #15803d; padding: 12px; border-radius: 8px; text-align: center; margin-bottom: 10px; font-weight: bold; display: flex; align-items: center; justify-content: center; gap: 8px;"><span style="font-size: 1.2em;">✅</span>{message}</div>"""
 
 
 
 
 
127
 
128
+ # --- 3. تابع اصلی پردازش (شامل بررسی اعتبار) ---
129
  @spaces.GPU(duration=30)
130
  def infer(
131
  input_image,
 
138
  aspect_ratio_selection,
139
  custom_width,
140
  custom_height,
141
+ # ورودی‌های جدید برای سیستم اعتبار که از JS می‌آیند
142
+ user_fingerprint,
143
+ user_subscription_status,
144
  progress=gr.Progress(track_tqdm=True)
145
  ):
146
+ # --- بررسی اعتبار کاربر ---
147
+ # اگر Fingerprint نرسد یعنی کاربر مستقیماً API را صدا زده یا JS لود نشده
148
+ if not user_fingerprint:
149
+ # برای جلوگیری از خطا در تست‌های لوکال، اگر نال بود یک مقدار موقت می‌دهیم،
150
+ # اما در پروداکشن واقعی می‌توانید ریترن ارور کنید.
151
+ user_fingerprint = "unknown_user"
152
+
153
+ # اگر کاربر پولی نباشد، اعتبار چک می‌شود
154
+ if user_subscription_status != 'paid':
155
+ today_str = date.today().isoformat()
156
+
157
+ # بازیابی یا ایجاد رکورد کاربر
158
+ user_record = usage_data_cache.get(user_fingerprint)
159
+ if not user_record or user_record.get("last_reset") != today_str:
160
+ user_record = {"count": 0, "last_reset": today_str}
161
+ usage_data_cache[user_fingerprint] = user_record
162
+
163
+ # چک کردن لیمیت
164
+ if user_record["count"] >= USAGE_LIMIT:
165
+ msg = f"اعتبار رایگان روزانه شما ({USAGE_LIMIT} تصویر) به پایان رسیده است. برای استفاده نامحدود ارتقا دهید."
166
+ return None, seed, get_error_html(msg)
167
+
168
+ # کسر اعتبار (افزایش شمارنده)
169
+ user_record["count"] += 1
170
+ print(f"User {user_fingerprint} usage: {user_record['count']}/{USAGE_LIMIT}")
171
 
172
+ # --- ادامه منطق اصلی برنامه ---
173
+ if input_image is None: return None, seed, get_error_html("لطفاً ابتدا یک تصویر بارگذاری کنید.")
174
+ if is_image_nsfw(input_image): return None, seed, get_error_html("تصویر ورودی دارای محتوای نامناسب است.")
175
 
176
  english_prompt = translate_prompt(prompt)
177
+ if not check_text_safety(english_prompt): return None, seed, get_error_html("متن درخواست شامل کلمات غیرمجاز است.")
 
178
 
179
  adapter_internal_name = LORA_MAPPING.get(lora_adapter_persian)
180
+ if adapter_internal_name: pipe.set_adapters([adapter_internal_name], adapter_weights=[1.0])
 
181
 
182
+ if randomize_seed: seed = random.randint(0, MAX_SEED)
 
 
183
  generator = torch.Generator(device=device).manual_seed(seed)
184
 
185
+ final_negative_prompt = "nsfw, nude, naked, porn, sexual, xxx, breast, nipple, genital, vagina, penis, ass, lingerie, bikini, swimwear, underwear, fetish, topless, gore, violence, blood, worst quality, low quality, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, jpeg artifacts, signature, watermark, username, blurry"
 
 
186
 
187
  original_image = input_image.convert("RGB")
 
188
  selection_value = ASPECT_RATIOS_MAP.get(aspect_ratio_selection)
189
+ if selection_value == "Custom": width, height = (int(custom_width)//8)*8, (int(custom_height)//8)*8
190
+ elif selection_value == "Auto" or selection_value is None: width, height = update_dimensions_on_upload(original_image)
191
+ else: width, height = selection_value
 
 
 
 
 
192
 
193
  try:
194
  result = pipe(
195
+ image=original_image, prompt=english_prompt, negative_prompt=final_negative_prompt,
196
+ height=height, width=width, num_inference_steps=steps, generator=generator, true_cfg_scale=guidance_scale,
 
 
 
 
 
 
197
  ).images[0]
198
 
199
+ if is_image_nsfw(result): return None, seed, get_error_html("تصویر تولید شده حاوی محتوای نامناسب بود.")
 
200
 
201
  return result, seed, get_success_html("تصویر با موفقیت ویرایش شد.")
202
 
203
  except Exception as e:
204
  error_str = str(e)
205
+ if "quota" in error_str.lower() or "exceeded" in error_str.lower(): raise e
 
206
  return None, seed, get_error_html(f"خطا در پردازش: {error_str}")
207
 
208
  @spaces.GPU(duration=30)
209
  def infer_example(input_image, prompt, lora_adapter):
210
+ # برای مثال‌ها بررسی اعتبار را رد می‌کنیم یا فیک می‌فرستیم
211
+ res, s, status = infer(input_image, prompt, lora_adapter, 0, True, 1.0, 4, "خودکار (پیش‌فرض)", 1024, 1024, "example_user", "paid")
212
  return res, s, status
213
 
214
+ # --- 4. کدهای جاوا اسکریپت ---
215
+
216
+ # کد دانلود فایل
217
  js_download_func = """
218
  async (image) => {
219
+ if (!image) { alert("لطفاً ابتدا تصویر را تولید کنید."); return; }
 
 
 
220
  let fileUrl = image.url;
221
+ if (fileUrl && !fileUrl.startsWith('http')) { fileUrl = window.location.origin + fileUrl; }
222
+ else if (!fileUrl && image.path) { fileUrl = window.location.origin + "/file=" + image.path; }
223
+ window.parent.postMessage({ type: 'DOWNLOAD_REQUEST', url: fileUrl }, '*');
 
 
 
 
 
 
224
  }
225
  """
226
 
227
+ # جاوا اسکریپت اصلی (اعتبار، فینگرپرینت، تم، ارور)
228
+ js_global_content = """
229
  <script>
230
+ // --- تنظیمات ثابت ---
231
+ const PREMIUM_PAGE_ID = '1149636';
232
+ const PREMIUM_URL = '#/nav/online/news/getSingle/1149636';
233
+
234
+ // متغیرهای گلوبال برای نگهداری وضعیت
235
+ window.userFingerprint = null;
236
+ window.userSubscriptionStatus = 'free';
237
+
238
+ // --- 1. تابع ساخت فینگرپرینت ---
239
+ async function getBrowserFingerprint() {
240
+ const components = [navigator.userAgent, navigator.language, screen.width + 'x' + screen.height, new Date().getTimezoneOffset()];
241
+ try {
242
+ const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d');
243
+ ctx.textBaseline = "top"; ctx.font = "14px 'Arial'"; ctx.textBaseline = "alphabetic";
244
+ ctx.fillStyle = "#f60"; ctx.fillRect(125, 1, 62, 20);
245
+ ctx.fillStyle = "#069"; ctx.fillText("a1b2c3d4e5f6g7h8i9j0_!@#$%^&*()", 2, 15);
246
+ components.push(canvas.toDataURL());
247
+ } catch (e) { components.push("canvas-error"); }
248
+ const fingerprintString = components.join('~~~'); let hash = 0;
249
+ for (let i = 0; i < fingerprintString.length; i++) { const char = fingerprintString.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash |= 0; }
250
+ return 'fp_' + Math.abs(hash).toString(16);
251
+ }
252
 
253
+ // --- 2. توابع بررسی اشتراک ---
254
+ function isUserPaid(userObject) {
255
+ return userObject && userObject.isLogin && userObject.accessible_pages &&
256
+ (userObject.accessible_pages.includes(PREMIUM_PAGE_ID) || userObject.accessible_pages.includes(parseInt(PREMIUM_PAGE_ID)));
257
+ }
 
 
 
 
 
 
 
 
 
258
 
259
+ function updateUIForSubscriptionStatus(status) {
260
+ window.userSubscriptionStatus = status;
261
+ const badge = document.getElementById('subscription-badge');
262
+ const infoText = document.getElementById('credit-info-text');
263
+ const upgradeBtn = document.getElementById('upgrade-btn-custom');
264
+
265
+ if (status === 'paid') {
266
+ if(badge) { badge.textContent = '⭐️ نسخه نامحدود'; badge.className = 'badge-paid'; }
267
+ if(infoText) infoText.innerHTML = 'شما به صورت <b>نامحدود</b> می‌توانید تصویر بسازید.';
268
+ if(upgradeBtn) upgradeBtn.style.display = 'none';
269
+ } else {
270
+ if(badge) { badge.textContent = 'نسخه رایگان'; badge.className = 'badge-free'; }
271
+ if(infoText) infoText.innerHTML = 'اعتبار رایگان روزانه: <b>۵ تصویر</b>. برای استفاده نامحدود ارتقا دهید.';
272
+ if(upgradeBtn) upgradeBtn.style.display = 'block';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  }
274
+ }
275
+
276
+ // --- 3. اجرای اولیه و لیسنرها ---
277
+ document.addEventListener('DOMContentLoaded', async () => {
278
+ // تولید فینگرپرینت
279
+ window.userFingerprint = await getBrowserFingerprint();
280
+
281
+ // درخواست وضعیت از والد
282
+ window.parent.postMessage({ type: 'REQUEST_USER_STATUS' }, '*');
283
 
284
+ // شنیدن پاسخ والد
285
  window.addEventListener('message', (event) => {
286
  if (event.data && event.data.type === 'USER_STATUS_RESPONSE') {
287
+ if (event.data.error || !event.data.payload) {
288
+ updateUIForSubscriptionStatus('free');
289
+ } else {
290
+ try {
291
+ const userObject = JSON.parse(event.data.payload);
292
+ const status = isUserPaid(userObject) ? 'paid' : 'free';
293
+ updateUIForSubscriptionStatus(status);
294
+ } catch (e) { updateUIForSubscriptionStatus('free'); }
 
 
 
 
 
 
295
  }
296
  }
297
+ if (event.data && event.data.type === 'NAVIGATE_TO_PREMIUM') {
298
+ // این رویداد توسط دکمه ارتقا تریگر میشه
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  }
300
+ });
 
 
 
 
 
 
 
 
 
 
 
301
 
302
+ // فورس تم روشن
 
 
 
 
303
  const forceLight = () => {
304
  const body = document.querySelector('body');
305
+ if (body) { body.classList.remove('dark'); body.style.backgroundColor = '#f5f7fa'; body.style.color = '#333333'; }
 
 
 
 
306
  document.querySelectorAll('.dark').forEach(el => el.classList.remove('dark'));
307
  };
308
+ forceLight(); setInterval(forceLight, 1000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
 
310
+ // اسکنر خطای Quota
311
  setInterval(() => {
312
  const potentialErrors = document.querySelectorAll('.toast-body, .error, .toast-wrap, .eta-bar, div[class*="error"]');
313
  potentialErrors.forEach(el => {
 
317
  el.style.display = 'none';
318
  }
319
  });
320
+ }, 100);
321
  });
322
+
323
+ // --- 4. توابع کمکی ---
324
+ window.retryGeneration = function() {
325
+ const modal = document.getElementById('custom-quota-modal'); if (modal) modal.remove();
326
+ const runBtn = document.getElementById('run-btn'); if(runBtn) runBtn.click();
327
+ };
328
+ window.closeErrorModal = function() {
329
+ const modal = document.getElementById('custom-quota-modal'); if (modal) modal.remove();
330
+ };
331
+ window.navigateToPremium = function() {
332
+ window.parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM', payload: { url: PREMIUM_URL } }, '*');
333
+ };
334
+
335
+ const showQuotaModal = () => {
336
+ if (document.getElementById('custom-quota-modal')) return;
337
+ // (HTML مودال خطا همانند قبل است، خلاصه شده)
338
+ const modalHtml = `<div id="custom-quota-modal" style="position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.6);backdrop-filter:blur(5px);z-index:99999;display:flex;align-items:center;justify-content:center;">
339
+ <div class="ip-reset-guide-container">
340
+ <h3>خطای محدودیت سرور</h3>
341
+ <p>لطفاً IP خود را تغییر دهید یا چند دقیقه صبر کنید.</p>
342
+ <button onclick="window.closeErrorModal()" class="action-button back-button">بازگشت</button>
343
+ <button onclick="window.retryGeneration()" class="action-button retry-button">تلاش مجدد</button>
344
+ </div>
345
+ </div>`;
346
+ document.body.insertAdjacentHTML('beforeend', modalHtml);
347
+ setTimeout(() => window.closeErrorModal(), 10000);
348
+ };
349
  </script>
350
  """
351
 
 
352
  css_code = """
353
  <style>
354
  @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700&display=swap');
355
 
356
+ /* Main Styling */
357
  :root, .dark, body, .gradio-container {
358
+ --body-background-fill: #f5f7fa !important; --body-text-color: #1f2937 !important;
 
359
  font-family: 'Vazirmatn', sans-serif !important;
360
  }
361
 
362
+ /* Subscription Badge & Info */
363
+ .subscription-info-container {
364
+ text-align: center; margin-bottom: 20px; padding: 15px;
365
+ background: white; border-radius: 16px; border: 1px solid #e5e7eb;
366
+ box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);
367
+ display: flex; flex-direction: column; align-items: center; gap: 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
  }
369
+ .badge-free {
370
+ background: linear-gradient(45deg, #6b7280, #4b5563); color: white;
371
+ padding: 6px 16px; border-radius: 20px; font-size: 0.9em; font-weight: 700;
 
 
 
 
 
372
  }
373
+ .badge-paid {
374
+ background: linear-gradient(45deg, #f59e0b, #d97706); color: white;
375
+ padding: 6px 16px; border-radius: 20px; font-size: 0.9em; font-weight: 700;
376
+ box-shadow: 0 4px 10px rgba(245, 158, 11, 0.3);
 
377
  }
378
+ .credit-text { color: #4b5563; font-size: 1rem; }
379
+ .upgrade-btn {
380
+ background: linear-gradient(95deg, #FFD54F, #FFC107); color: #212529;
381
+ border: none; padding: 8px 20px; border-radius: 12px; font-weight: 800; cursor: pointer;
382
+ box-shadow: 0 4px 15px rgba(255, 193, 7, 0.3); transition: transform 0.2s;
383
+ display: none; /* Hidden by default, shown by JS */
 
 
 
 
 
 
 
 
 
 
 
 
384
  }
385
+ .upgrade-btn:hover { transform: translateY(-2px); }
386
 
387
+ /* Buttons & Inputs */
388
+ .primary-btn {
389
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important; color: white !important;
390
+ border-radius: 14px !important; box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3) !important;
391
  }
392
+ /* Modal Styles (Simplified for brevity) */
393
+ .ip-reset-guide-container {
 
394
  background: white; padding: 20px; border-radius: 16px; width: 90%; max-width: 400px; text-align: right; direction: rtl;
395
  }
 
 
 
 
 
 
 
 
 
 
 
 
396
  </style>
397
  """
398
 
399
+ # ترکیب کدها
400
+ combined_html = css_code + js_global_content
401
 
402
+ # --- 5. رابط کاربری Gradio ---
403
+ with gr.Blocks(title="ویرایشگر هوشمند آلفا") as demo:
 
404
  gr.HTML(combined_html)
405
 
406
  with gr.Column(elem_id="col-container"):
407
  gr.Markdown("# **ویرایشگر هوشمند آلفا**", elem_id="main-title")
408
+ gr.Markdown("با هوش مصنوعی آلفا تصاویر تونو به مدل های مختلف ویرایش کنید.", elem_id="main-description")
409
+
410
+ # --- بخش جدید: نمایش وضعیت اشتراک و توضیحات ---
 
 
 
411
  gr.HTML("""
412
+ <div class="subscription-info-container">
413
+ <div id="subscription-badge" class="badge-free">نسخه رایگان</div>
414
+ <div id="credit-info-text" class="credit-text">در حال بررسی اعتبار...</div>
415
+ <button id="upgrade-btn-custom" class="upgrade-btn" onclick="navigateToPremium()">⭐️ ارتقا به نسخه کامل و نامحدود</button>
416
+ <div style="font-size: 0.85em; color: #6b7280; margin-top: 5px;">
417
+ نسخه رایگان: ۵ تصویر در روز | نسخه نامحدود: بدون محدودیت
418
+ </div>
419
  </div>
420
  """)
421
+ # ----------------------------------------------------
422
 
423
  with gr.Row(equal_height=True):
424
  with gr.Column():
425
  input_image = gr.Image(label="بارگذاری تصویر", type="pil", height=320)
426
+ prompt = gr.Text(label="دستور ویرایش (به فارسی)", placeholder="مثال: تصویر را به سبک انیمه تبدیل کن...", rtl=True, lines=3)
 
 
 
 
 
 
 
 
427
  status_box = gr.HTML(label="وضعیت")
428
 
429
+ # دکمه اجرا
430
  run_button = gr.Button("✨ شروع پردازش و ساخت تصویر", variant="primary", elem_classes="primary-btn", elem_id="run-btn")
431
 
432
  with gr.Column():
433
  output_image = gr.Image(label="تصویر نهایی", interactive=False, format="png", height=380)
434
+ download_button = gr.Button("📥 دانلود تصویر", variant="secondary", elem_id="download-btn")
435
 
436
+ lora_adapter = gr.Dropdown(label="انتخاب سبک ویرایش (LoRA)", choices=list(LORA_MAPPING.keys()), value="تبدیل عکس به انیمه")
 
 
 
 
 
 
 
437
 
438
+ with gr.Accordion("تنظیمات پیشرفته", open=False):
439
+ aspect_ratio_selection = gr.Dropdown(label="ابعاد تصویر", choices=ASPECT_RATIOS_LIST, value="خودکار (پیش‌فرض)")
 
 
 
 
 
 
440
  with gr.Row(visible=False) as custom_dims_row:
441
+ custom_width = gr.Slider(label="عرض", minimum=256, maximum=2048, step=8, value=1024)
442
+ custom_height = gr.Slider(label="ارتفاع", minimum=256, maximum=2048, step=8, value=1024)
 
 
 
 
 
 
443
 
444
+ seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
445
+ randomize_seed = gr.Checkbox(label="Seed تصادفی", value=True)
446
+ guidance_scale = gr.Slider(label="Guidance Scale", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
447
+ steps = gr.Slider(label="Steps", minimum=1, maximum=50, step=1, value=4)
448
 
449
+ # مدیریت نمایش اسلایدرهای ابعاد
450
+ def toggle_row(choice): return gr.update(visible=(choice == "شخصی‌سازی (Custom)"))
451
+ aspect_ratio_selection.change(fn=toggle_row, inputs=aspect_ratio_selection, outputs=custom_dims_row)
 
 
 
 
 
 
 
452
 
453
+ # مثال‌ها
454
  gr.Examples(
455
  examples=[
456
  ["examples/1.jpg", "تبدیل به انیمه کن.", "تبدیل عکس به انیمه"],
457
+ ["examples/5.jpg", "سایه‌ها را حذف کن.", "اصلاح نور و سایه"],
 
458
  ],
459
  inputs=[input_image, prompt, lora_adapter],
460
  outputs=[output_image, seed, status_box],
461
  fn=infer_example,
462
+ label="نمونه‌ها"
463
+ )
464
+
465
+ # --- اتصال JS به دکمه اجرا برای ارسال Fingerprint و Status ---
466
+ # این تابع JS مقادیر ورودی‌ها + متغیرهای گلوبال (Fingerprint, Status) را می‌خواند و به تابع پایتون می‌فرستد
467
+ js_click_handler = """
468
+ (img, p, lora, s, rs, gs, st, ar, cw, ch) => {
469
+ return [img, p, lora, s, rs, gs, st, ar, cw, ch, window.userFingerprint, window.userSubscriptionStatus];
470
+ }
471
+ """
472
+
473
+ run_button.click(
474
+ fn=infer,
475
+ inputs=[input_image, prompt, lora_adapter, seed, randomize_seed, guidance_scale, steps, aspect_ratio_selection, custom_width, custom_height],
476
+ outputs=[output_image, seed, status_box],
477
+ js=js_click_handler
478
  )
479
 
480
+ download_button.click(fn=None, inputs=[output_image], js=js_download_func)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
481
 
482
  if __name__ == "__main__":
483
  demo.queue(max_size=30).launch(show_error=True)