Ezmary commited on
Commit
b4d840c
·
verified ·
1 Parent(s): 195e11e

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +140 -150
templates/index.html CHANGED
@@ -112,13 +112,24 @@
112
  .status-message { display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem 1rem; margin-bottom: 0.5rem; border-radius: var(--radius-input); font-size: 0.9rem; background: var(--panel-bg); border: 1px solid var(--panel-border); color: var(--text-secondary); }
113
  .status-message.success { border-left: 4px solid var(--success-color); color: var(--success-color); }
114
  .status-message.error { border-left: 4px solid var(--danger-color); color: var(--danger-color); }
115
- .status-icon { width: 18px; height: 18px; flex-shrink: 0; }
116
  #aiLoader { display: none; align-items: center; justify-content: center; }
117
  .generator-container { position: relative; width: 400px; max-width: 100%; height: 300px; border: 2px solid #38bdf8; border-radius: 20px; overflow: hidden; box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); animation: pulse-loader 5s infinite cubic-bezier(0.4, 0, 0.6, 1); background-color: #161b22; color: #f0f6fc; }
118
  @keyframes pulse-loader { 0% { box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); } 50% { box-shadow: 0 0 60px rgba(56, 189, 248, 0.7); } 100% { box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); } }
119
- .text-overlay { position: absolute; top: 45%; left: 50%; transform: translate(-50%, -50%); font-size: 24px; font-weight: 700; text-shadow: 0 0 20px rgba(56, 189, 248, 0.8); animation: glow-text 7s infinite ease-in-out; font-family: var(--app-font); width: 90%; text-align: center; transition: var(--transition-smooth); }
 
 
 
 
 
 
 
 
 
 
 
120
  @keyframes glow-text { 0% { opacity: 0.7; } 50% { opacity: 1; } 100% { opacity: 0.7; } }
121
- .progress-bar-loader { position: absolute; bottom: 0; left: 0; width: 0%; height: 6px; background: linear-gradient(to right, #38bdf8, #bb86fc, #facc15); transition: width 0.4s ease-out; }
122
  #outputVideo { width: 100%; max-width: 500px; border-radius: var(--radius-input); margin: 1rem auto; display: block; background-color: #000; box-shadow: var(--shadow-md); }
123
  #finalSeed { text-align: center; color: var(--text-tertiary); font-size: 0.85rem; margin-top: 1rem; }
124
  .video-controls { display: flex; justify-content: center; gap: 1rem; margin-top: 1.5rem; }
@@ -137,18 +148,14 @@
137
  .gpu-error-actions { display: flex; gap: 15px; margin-top: 25px; }
138
  .gpu-error-actions .action-button.back-button { background-color: var(--panel-bg); color: var(--text-tertiary); border: 1px solid var(--panel-border); flex-grow: 0.45; }
139
  .gpu-error-actions .action-button.retry-button { background: linear-gradient(60deg, var(--igadlm-alpha-color-primary) 0%, var(--igadlm-alpha-color-secondary) 100%); color: white; flex-grow: 1; }
 
140
  #subscription-status-badge { display: none; padding: 6px 16px; border-radius: 20px; font-size: 0.9em; font-weight: 700; margin-top: 1.25rem; letter-spacing: 0.5px; animation: badge-fade-in 0.6s 0.5s ease-out backwards; }
141
  #subscription-status-badge.free-badge { background: linear-gradient(45deg, #6c757d, #495057); color: white; box-shadow: 0 4px 10px rgba(108, 117, 125, 0.3); }
142
  #subscription-status-badge.paid-badge { background: linear-gradient(45deg, var(--accent-premium), #ffca2c); color: #333; box-shadow: 0 4px 10px var(--accent-premium-glow); }
 
143
  #credit-info-section { margin-top: 1.5rem; margin-bottom: 0.5rem; padding: 1rem; background-color: var(--input-bg); border-radius: var(--radius-input); text-align: center; font-size: 0.95rem; font-weight: 500; color: var(--text-secondary); border: 1px solid var(--input-border); display: none; }
144
  #upgrade-button { margin-top: 1rem; width: 100%; padding: 1rem; font-size: 1.1rem; font-weight: 700; background: linear-gradient(95deg, #FFD54F, #FFC107 100%); color: #212529; border: none; border-radius: var(--radius-btn); cursor: pointer; box-shadow: 0 8px 20px -5px rgba(255, 193, 7, 0.3); display: none; }
145
 
146
- .json-log-container { display: none; margin-top: 1.5rem; }
147
- .json-log-container h3 { font-size: 1rem; color: var(--text-secondary); margin-bottom: 0.75rem; }
148
- .json-log-pre { background-color: #0d1117; color: #c9d1d9; padding: 1rem; border-radius: var(--radius-input); max-height: 250px; overflow-y: auto; font-family: monospace; font-size: 0.8rem; white-space: pre-wrap; word-break: break-all; }
149
- #copyLogBtn { margin-top: 0.75rem; background-color: var(--text-secondary); }
150
- #copyLogBtn:hover { background-color: var(--text-primary); }
151
-
152
  @media (max-width: 768px) { main { padding: 1.5rem; } h1 { font-size: 2.2rem; } .generator-container { height: 250px; } .text-overlay { font-size: 18px; } }
153
  </style>
154
  </head>
@@ -200,12 +207,12 @@
200
 
201
  <div class="slider-group">
202
  <div class="slider-label-container">
203
- <label for="durationSlider" class="slider-label">مدت زمان ویدیو</label>
204
- <span id="durationValue" class="slider-value">4.3 ثانیه</span>
205
  </div>
206
  <div class="slider-wrapper">
207
  <svg class="slider-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.72"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.72-1.72"></path></svg>
208
- <input type="range" id="durationSlider" min="0.5" max="5.1" value="4.3" step="0.1">
209
  <svg class="slider-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
210
  </div>
211
  </div>
@@ -229,8 +236,13 @@
229
  <div id="statusMessages"></div>
230
  <div id="aiLoader" style="display: none;">
231
  <div class="generator-container">
 
 
 
 
 
232
  <div class="text-overlay">در حال پردازش ویدیو...</div>
233
- <div class="progress-bar-loader"></div>
234
  </div>
235
  </div>
236
  </div>
@@ -251,13 +263,6 @@
251
  </div>
252
 
253
  <div id="criticalErrorSection"></div>
254
-
255
- <div id="json-log-container" class="json-log-container">
256
- <h3>لاگ کامل JSON</h3>
257
- <pre id="json-log-pre" class="json-log-pre"></pre>
258
- <button id="copyLogBtn" class="video-button">کپی لاگ</button>
259
- </div>
260
-
261
  </div>
262
  </div>
263
  </main>
@@ -267,8 +272,8 @@
267
  import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
268
 
269
  (function() {
270
- // === START: بخش مدیریت اعتبار و ارتباط با والد ===
271
- let userSubscriptionStatus = 'free'; // 'free' or 'paid'
272
  let userFingerprint = null;
273
  let countdownInterval = null;
274
  const PREMIUM_PAGE_ID = '1149636';
@@ -342,8 +347,8 @@
342
  creditInfoDiv.textContent = `شما ${result.credits_remaining} اعتبار ساخت ویدیوی رایگان در این هفته دارید.`;
343
  }
344
  } catch (error) {
345
- creditInfoDiv.textContent = "توجه: سیستم اعتبار در این نسخه آزمایشی فعال است و نیازی به پرداخت نیست.";
346
- genBtn.disabled = false;
347
  }
348
  }
349
 
@@ -402,31 +407,26 @@
402
  const statusSection = document.getElementById('statusSection');
403
  const statusMessagesDiv = document.getElementById('statusMessages');
404
  const aiLoader = document.getElementById('aiLoader');
405
- const loaderProgressBar = document.querySelector('#aiLoader .progress-bar-loader');
406
  const loaderTextOverlay = document.querySelector('#aiLoader .text-overlay');
407
  const outputSection = document.getElementById('outputSection');
408
  const btnRestart = document.getElementById('btnRestart');
409
  const btnDownloadVideo = document.getElementById('btnDownloadVideo');
410
  const durationSlider = document.getElementById('durationSlider');
411
  const durationValueDisplay = document.getElementById('durationValue');
412
- const jsonLogContainer = document.getElementById('json-log-container');
413
- const jsonLogPre = document.getElementById('json-log-pre');
414
- const copyLogBtn = document.getElementById('copyLogBtn');
415
 
416
  // ====== تنظیمات مدل‌ها ======
417
  const VIDEO_SPACE_NAME = "mrbui1990/wan2-2-fp8da-aoti-faster";
418
  const TRANSLATOR_SPACE_NAME = "hamed744/translate-tts-aloha";
419
- const FN_INDEX_TRANSLATE_SPEAK = 1;
420
- const DEFAULT_TTS_VOICE_TRANSLATOR = "انگلیسی (آمریکا) - جنی (زن)";
421
 
422
  const DEFAULT_NEGATIVE_PROMPT = "色调艳丽, 过曝, 静态, 细节模糊不清, 字幕, 风格, 作品, 画作, 画面, 静止, 整体发灰, 最差质量, 低质量, JPEG压缩残留, 丑陋的, 残缺的, 多余的手指, 画得不好的手部, 画得不好的脸部, 畸形的, 毁容的, 形态畸形的肢体, 手指融合, 静止不动的画面, 杂乱的背景, 三条腿, 背景人很多, 倒着走";
423
  const DEFAULT_PROMPT_TEXT = "";
424
  const TRANSPARENT_PIXEL_SRC = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
425
- const DEFAULT_VIDEO_DURATION = 4.3;
426
 
427
  let lastAttemptedVideoPayload = null;
428
- let currentSubmission = null;
429
-
430
  function updateSliderStyle(slider) {
431
  const percentage = ((slider.value - slider.min) / (slider.max - slider.min)) * 100;
432
  const colorStops = `var(--accent-primary), var(--accent-secondary)`;
@@ -438,19 +438,19 @@
438
  showCriticalError("اطلاعات کافی برای تلاش مجدد وجود ندارد. لطفاً از نو شروع کنید.");
439
  return;
440
  }
441
- proceedWithVideoGeneration(true);
442
  }
443
 
444
  function showCriticalError(message) {
445
- const lowerCaseMessage = (message || "").toLowerCase();
446
  if (lowerCaseMessage.includes("gpu") || lowerCaseMessage.includes("quota") || lowerCaseMessage.includes("exceeded") || lowerCaseMessage.includes("limit") || lowerCaseMessage.includes("capacity") || lowerCaseMessage.includes("queue full")) {
447
  const detailedGpuErrorHtml = `<div class="gpu-error-message-container"><h2><span class="icon">💡</span> راهنمای رفع محدودیت ویدیو</h2><div class="error-content"><strong class="error-message-title">ارور محدودیت استفاده از GPU</strong><p>مدل‌های هوش مصنوعی برای اجرا به کارت‌های گرافیک بسیار قدرتمند نیاز دارند که این امر گاهی محدودیت‌هایی جی پی یو را در استفاده از آن‌ها ایجاد می‌کند. با این حال، خبر خوب این است که این محدودیت‌ها با استفاده از یک ترفند ساده قابل رفع هستند.</p><p><strong>برای رفع محدودیت هر بار اینگونه عمل کنید:</strong></p><ul><li>اگر از اینترنت سیم‌کارت استفاده می‌کنید، فیلترشکن خود را حتما خاموش کنید.</li><li>به مدت ۱۰ تا ۱۵ ثانیه، گوشی خود را در حالت هواپیما قرار دهید و سپس آن را غیرفعال کنید. این محدودیت با همین روش برداشته میشود</li><li>اگر از وای فای استفاده می‌کنید، مودم خود را یک بار خاموش و روشن کنید (بهتر است برای استفاده نامحدود از برنامه از اینترنت سیم کارت استفاده کنید).</li></ul><p>خلاصه: هربار این صفحه اومد اینگونه عمل کنید از اینترنت سیم کارت استفاده کنید، فیلتر شکن خاموش باشه، یک یا دوبار حالت هواپیما رو روشن و خاموش کنید. تلاش مجدداً بزنید این ارور بر طرف میشه، با این روش میشه بصورت نامحدود با این برنامه ویدیو ساخت☘️</p></div><div class="gpu-error-actions"><button class="action-button back-button" id="gpuErrorGoBackBtn">بازگشت</button><button class="action-button retry-button" id="gpuErrorRetryBtn">تلاش مجدد</button></div></div>`;
448
  criticalErrorSection.innerHTML = detailedGpuErrorHtml;
449
- document.getElementById('gpuErrorGoBackBtn').addEventListener('click', initializeForm);
450
  document.getElementById('gpuErrorRetryBtn').addEventListener('click', retryLastAttempt);
451
  } else {
452
  criticalErrorSection.innerHTML = `<div id="criticalErrorMessage" style="color: var(--danger-color); font-weight: 500; margin-bottom: 1.5rem; line-height: 1.6;"><p>${message}</p></div><div class="video-controls"><button id="simpleErrorGoBackBtn" class="video-button">بازگشت</button><button id="simpleErrorRetryBtn" class="video-button primary">تلاش مجدد</button></div>`;
453
- document.getElementById('simpleErrorGoBackBtn').addEventListener('click', initializeForm);
454
  document.getElementById('simpleErrorRetryBtn').addEventListener('click', retryLastAttempt);
455
  }
456
  resultContainer.classList.add('active');
@@ -459,24 +459,18 @@
459
  outputSection.classList.remove('active');
460
  aiLoader.style.display = 'none';
461
  generateButton.disabled = false;
462
- jsonLogContainer.style.display = 'block';
 
 
 
 
463
  }
464
 
465
  function setAiLoaderState(text, percentage = null) {
466
  if (loaderTextOverlay) loaderTextOverlay.textContent = text;
467
- if (loaderProgressBar) {
468
- if (percentage !== null) {
469
- loaderProgressBar.style.width = `${percentage}%`;
470
- }
471
- }
472
- }
473
-
474
- function appendJsonToLog(data) {
475
- if (jsonLogPre.textContent) {
476
- jsonLogPre.textContent += '\n\n// ---- پیام جدید ---- //\n\n';
477
  }
478
- jsonLogPre.textContent += JSON.stringify(data, null, 2);
479
- jsonLogPre.parentElement.scrollTop = jsonLogPre.parentElement.scrollHeight;
480
  }
481
 
482
  function addStatusMessage(message, type = 'info') {
@@ -492,7 +486,7 @@
492
 
493
  function clearStatusMessages() {
494
  statusMessagesDiv.innerHTML = '';
495
- setAiLoaderState("در حال پردازش...", 0);
496
  aiLoader.style.display = 'none';
497
  if (outputVideo.src && outputVideo.src.startsWith('blob:')) {
498
  URL.revokeObjectURL(outputVideo.src);
@@ -500,17 +494,10 @@
500
  outputVideo.src = '';
501
  finalSeedElement.textContent = '';
502
  btnDownloadVideo.style.display = 'none';
503
- jsonLogContainer.style.display = 'none';
504
- jsonLogPre.textContent = '';
505
  }
506
 
507
  function initializeForm() {
508
- if (currentSubmission) {
509
- try { currentSubmission.cancel(); } catch (e) {}
510
- currentSubmission = null;
511
- }
512
- criticalErrorSection.classList.remove('active');
513
- criticalErrorSection.innerHTML = '';
514
  resultContainer.classList.remove('active');
515
  statusSection.classList.remove('active');
516
  outputSection.classList.remove('active');
@@ -521,37 +508,43 @@
521
  imageDropZone.classList.remove('has-image');
522
  promptInput.value = DEFAULT_PROMPT_TEXT;
523
  generateButton.disabled = false;
524
- durationSlider.value = DEFAULT_VIDEO_DURATION;
525
- durationValueDisplay.textContent = `${parseFloat(DEFAULT_VIDEO_DURATION).toFixed(1)} ثانیه`;
526
  updateSliderStyle(durationSlider);
527
  updateUIForSubscriptionStatus(userSubscriptionStatus);
528
  }
529
 
530
  async function translatePersianToEnglish(persianText) {
531
- if (!persianText?.trim()) throw new Error("متن ترجمه خالی است.");
532
- setAiLoaderState("در حال ترجمه دستور شما...");
533
- const translator = await Client.connect(TRANSLATOR_SPACE_NAME);
534
- const resp = await translator.predict(FN_INDEX_TRANSLATE_SPEAK, [persianText, DEFAULT_TTS_VOICE_TRANSLATOR, 0, 0, 0]);
535
- const english = resp?.data?.[0];
536
- if (!english) throw new Error("ترجمه نامعتبر بود.");
537
- return english;
 
 
 
 
 
 
 
538
  }
539
 
540
- function handleSuccessfulVideoGeneration(resultData) {
541
  aiLoader.style.display = 'none';
542
- if (!resultData?.[0]?.video?.url) {
543
  showCriticalError('پاسخ دریافتی از سرور ویدیو معتبر نیست.');
544
  return;
545
  }
546
 
547
  if (userSubscriptionStatus === 'free') {
548
- // Mock credit usage
549
- checkFreeUserCredit();
550
  }
551
 
552
  addStatusMessage('ویدیو آماده شد! 🎉', 'success');
553
- const videoUrl = resultData[0].video.url;
554
- const usedSeed = resultData[1];
555
  setTimeout(() => {
556
  statusSection.classList.remove('active');
557
  outputSection.classList.add('active');
@@ -559,8 +552,6 @@
559
  finalSeedElement.textContent = `Seed: ${usedSeed}`;
560
  prepareCustomDownloadLink(videoUrl);
561
  }, 300);
562
- generateButton.disabled = false;
563
- jsonLogContainer.style.display = 'block';
564
  }
565
 
566
  function prepareCustomDownloadLink(videoUrl) {
@@ -578,8 +569,7 @@
578
  });
579
 
580
  durationSlider.addEventListener('input', () => {
581
- const value = parseFloat(durationSlider.value).toFixed(1);
582
- durationValueDisplay.textContent = `${value} ثانیه`;
583
  updateSliderStyle(durationSlider);
584
  });
585
 
@@ -591,13 +581,12 @@
591
 
592
  imageFileInput.addEventListener('change', function(event) {
593
  const file = event.target.files[0];
 
594
  if (file) {
595
  if (!file.type.startsWith('image/')) {
596
- showCriticalError('فرمت فایل نامعتبر است. لطفاً یک تصویر انتخاب کنید.');
597
- initializeForm();
598
  } else {
599
- criticalErrorSection.classList.remove('active');
600
- criticalErrorSection.innerHTML = '';
601
  const reader = new FileReader();
602
  reader.onload = (e) => { imagePreview.src = e.target.result; imageDropZone.classList.add('has-image'); };
603
  reader.readAsDataURL(file);
@@ -607,34 +596,17 @@
607
  imageDropZone.classList.remove('has-image');
608
  }
609
  });
610
-
611
- copyLogBtn.addEventListener('click', () => {
612
- navigator.clipboard.writeText(jsonLogPre.textContent).then(() => {
613
- const originalText = copyLogBtn.textContent;
614
- copyLogBtn.textContent = 'کپی شد!';
615
- setTimeout(() => { copyLogBtn.textContent = originalText; }, 2000);
616
- });
617
- });
618
-
619
- const proceedWithVideoGeneration = async (isRetry = false) => {
620
- let persianPrompt, imageFile, duration;
621
-
622
- if (isRetry) {
623
- persianPrompt = lastAttemptedVideoPayload.persianPrompt;
624
- imageFile = lastAttemptedVideoPayload.imageSourceFile;
625
- duration = lastAttemptedVideoPayload.duration;
626
- } else {
627
- persianPrompt = promptInput.value.trim();
628
- imageFile = imageFileInput.files[0];
629
- duration = parseFloat(durationSlider.value);
630
- }
631
 
632
  if (!persianPrompt) { addStatusMessage('لطفاً توضیحات را وارد کنید.', 'error'); return; }
633
  if (!imageFile) { addStatusMessage('لطفاً تصویر انتخاب کنید.', 'error'); return; }
634
 
635
  clearStatusMessages();
636
- criticalErrorSection.classList.remove('active');
637
- criticalErrorSection.innerHTML = '';
638
  generateButton.disabled = true;
639
  resultContainer.classList.add('active');
640
  statusSection.classList.add('active');
@@ -642,58 +614,76 @@
642
  outputSection.classList.remove('active');
643
  addStatusMessage('در حال آماده سازی...', 'info');
644
 
645
- if (!isRetry) {
646
- lastAttemptedVideoPayload = { persianPrompt, imageSourceFile: imageFile, duration };
647
- }
648
 
649
- let promptForVideo = persianPrompt;
650
- try {
651
- addStatusMessage('در حال ترجمه متن به انگلیسی...', 'info');
652
- const englishPrompt = await translatePersianToEnglish(persianPrompt);
653
- addStatusMessage(`ترجمه: ${englishPrompt}`, 'info');
654
- promptForVideo = englishPrompt;
655
- } catch (e) {
656
- addStatusMessage('ترجمه ناموفق بود؛ با متن فارسی ادامه می‌دهیم.', 'error');
657
- }
658
 
659
  try {
660
  const client = await Client.connect(VIDEO_SPACE_NAME);
661
- currentSubmission = client.submit(0, [
662
- imageFile, promptForVideo, 6, DEFAULT_NEGATIVE_PROMPT, duration, 1, 1, 0, true
 
 
 
 
 
 
 
 
663
  ]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
664
 
665
- currentSubmission.on("status", (status) => {
666
- appendJsonToLog(status); // Always log the raw data
667
- switch(status.code) {
668
- case "QUEUED":
669
- setAiLoaderState(`در صف... نفر جلوی شما: ${status.queue_size - 1}. زمان تخمینی: ${Math.round(status.eta)} ثانیه`, 0);
670
- break;
671
- case "ESTIMATING":
672
- setAiLoaderState(`در حال تخمین زمان...`, 0);
673
- break;
674
- case "SENDING_DATA":
675
- case "PROCESSING":
676
- if (status.progress_data) {
677
- const p = status.progress_data[0];
678
- const percentage = p.total ? (p.index / p.total) * 100 : 0;
679
- const desc = p.desc || 'در حال پردازش...';
680
- setAiLoaderState(`${desc} (${Math.round(percentage)}%)`, percentage);
681
- } else {
682
- setAiLoaderState("پردازش در سرور شروع شد...");
683
- }
684
- break;
685
- case "COMPLETE":
686
- handleSuccessfulVideoGeneration(status.data);
687
- break;
688
- case "GENERIC_ERROR":
689
- showCriticalError(`خطا در پردازش: ${status.message}`);
690
- break;
691
  }
692
- });
 
 
693
 
694
  } catch (errorCaught) {
695
- showCriticalError(`خطای اتصال: ${errorCaught.message}`);
 
696
  generateButton.disabled = false;
 
697
  }
698
  };
699
 
@@ -703,15 +693,15 @@
703
  if (userSubscriptionStatus === 'paid') {
704
  await proceedWithVideoGeneration();
705
  } else {
706
- // In demo mode, we assume credit is available
707
- await proceedWithVideoGeneration();
708
  }
709
  });
710
  })();
711
  </script>
712
 
713
  <script>
714
- // --- Header Animation Script ---
715
  document.addEventListener('DOMContentLoaded', () => {
716
  const canvas = document.getElementById('neural-network-canvas');
717
  if (!canvas) return;
 
112
  .status-message { display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem 1rem; margin-bottom: 0.5rem; border-radius: var(--radius-input); font-size: 0.9rem; background: var(--panel-bg); border: 1px solid var(--panel-border); color: var(--text-secondary); }
113
  .status-message.success { border-left: 4px solid var(--success-color); color: var(--success-color); }
114
  .status-message.error { border-left: 4px solid var(--danger-color); color: var(--danger-color); }
115
+ .status-icon { width: 18px; height: 18px; }
116
  #aiLoader { display: none; align-items: center; justify-content: center; }
117
  .generator-container { position: relative; width: 400px; max-width: 100%; height: 300px; border: 2px solid #38bdf8; border-radius: 20px; overflow: hidden; box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); animation: pulse-loader 5s infinite cubic-bezier(0.4, 0, 0.6, 1); background-color: #161b22; color: #f0f6fc; }
118
  @keyframes pulse-loader { 0% { box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); } 50% { box-shadow: 0 0 60px rgba(56, 189, 248, 0.7); } 100% { box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); } }
119
+ .noise-layer, .sketch-layer, .building-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
120
+ .noise-layer { background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><rect width="100" height="100" fill="none"/><filter id="noise"><feTurbulence type="fractalNoise" baseFrequency="0.5" numOctaves="4" stitchTiles="stitch"/></filter><rect width="100%" height="100%" filter="url(%23noise)" opacity="0.6"/></svg>') repeat; opacity: 1; animation: fade-noise 7s infinite ease-in-out; }
121
+ @keyframes fade-noise { 0% { opacity: 1; filter: blur(5px); } 30% { opacity: 0.8; filter: blur(2px); } 100% { opacity: 0; filter: blur(0px); } }
122
+ .sketch-layer { filter: grayscale(1) contrast(1.5) blur(3px); opacity: 0; animation: reveal-sketch 7s infinite ease-in-out; }
123
+ @keyframes reveal-sketch { 0% { opacity: 0; } 20% { opacity: 1; } 60% { opacity: 0.5; } 100% { opacity: 0; } }
124
+ .building-layer { filter: blur(15px); opacity: 0; animation: denoise-color 7s infinite ease-in-out; }
125
+ @keyframes denoise-color { 0% { opacity: 0; } 40% { opacity: 0.6; filter: blur(5px); } 100% { opacity: 1; filter: blur(0px); } }
126
+ .pixel-grid { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: repeating-linear-gradient(0deg, transparent 0 1px, rgba(255,255,255,0.1) 1px 2px), repeating-linear-gradient(90deg, transparent 0 1px, rgba(255,255,255,0.1) 1px 2px); opacity: 1; animation: dissolve-grid 7s infinite ease-in-out; }
127
+ @keyframes dissolve-grid { 0% { opacity: 1; } 70% { opacity: 0.5; } 100% { opacity: 0; } }
128
+ .particles { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(circle, rgba(56, 189, 248, 0.2) 0%, transparent 50%); animation: flow-particles 7s infinite cubic-bezier(0.4, 0, 0.6, 1); }
129
+ @keyframes flow-particles { 0% { transform: translate(0, 0) scale(1); opacity: 0.5; } 50% { transform: translate(10px, -15px) scale(1.05); opacity: 0.8; } 100% { transform: translate(0, 0) scale(1); opacity: 0.5; } }
130
+ .text-overlay { position: absolute; top: 45%; left: 50%; transform: translate(-50%, -50%); font-size: 24px; font-weight: 700; text-shadow: 0 0 20px rgba(56, 189, 248, 0.8); animation: glow-text 7s infinite ease-in-out; font-family: var(--app-font); width: 90%; text-align: center;}
131
  @keyframes glow-text { 0% { opacity: 0.7; } 50% { opacity: 1; } 100% { opacity: 0.7; } }
132
+ .progress-bar { position: absolute; bottom: 0; left: 0; width: 0%; height: 6px; background: linear-gradient(to right, #38bdf8, #bb86fc, #facc15); transition: width 0.5s ease; /* плавный переход */ }
133
  #outputVideo { width: 100%; max-width: 500px; border-radius: var(--radius-input); margin: 1rem auto; display: block; background-color: #000; box-shadow: var(--shadow-md); }
134
  #finalSeed { text-align: center; color: var(--text-tertiary); font-size: 0.85rem; margin-top: 1rem; }
135
  .video-controls { display: flex; justify-content: center; gap: 1rem; margin-top: 1.5rem; }
 
148
  .gpu-error-actions { display: flex; gap: 15px; margin-top: 25px; }
149
  .gpu-error-actions .action-button.back-button { background-color: var(--panel-bg); color: var(--text-tertiary); border: 1px solid var(--panel-border); flex-grow: 0.45; }
150
  .gpu-error-actions .action-button.retry-button { background: linear-gradient(60deg, var(--igadlm-alpha-color-primary) 0%, var(--igadlm-alpha-color-secondary) 100%); color: white; flex-grow: 1; }
151
+
152
  #subscription-status-badge { display: none; padding: 6px 16px; border-radius: 20px; font-size: 0.9em; font-weight: 700; margin-top: 1.25rem; letter-spacing: 0.5px; animation: badge-fade-in 0.6s 0.5s ease-out backwards; }
153
  #subscription-status-badge.free-badge { background: linear-gradient(45deg, #6c757d, #495057); color: white; box-shadow: 0 4px 10px rgba(108, 117, 125, 0.3); }
154
  #subscription-status-badge.paid-badge { background: linear-gradient(45deg, var(--accent-premium), #ffca2c); color: #333; box-shadow: 0 4px 10px var(--accent-premium-glow); }
155
+
156
  #credit-info-section { margin-top: 1.5rem; margin-bottom: 0.5rem; padding: 1rem; background-color: var(--input-bg); border-radius: var(--radius-input); text-align: center; font-size: 0.95rem; font-weight: 500; color: var(--text-secondary); border: 1px solid var(--input-border); display: none; }
157
  #upgrade-button { margin-top: 1rem; width: 100%; padding: 1rem; font-size: 1.1rem; font-weight: 700; background: linear-gradient(95deg, #FFD54F, #FFC107 100%); color: #212529; border: none; border-radius: var(--radius-btn); cursor: pointer; box-shadow: 0 8px 20px -5px rgba(255, 193, 7, 0.3); display: none; }
158
 
 
 
 
 
 
 
159
  @media (max-width: 768px) { main { padding: 1.5rem; } h1 { font-size: 2.2rem; } .generator-container { height: 250px; } .text-overlay { font-size: 18px; } }
160
  </style>
161
  </head>
 
207
 
208
  <div class="slider-group">
209
  <div class="slider-label-container">
210
+ <label for="durationSlider" class="slider-label">میزان حرکت</label>
211
+ <span id="durationValue" class="slider-value">4.3</span>
212
  </div>
213
  <div class="slider-wrapper">
214
  <svg class="slider-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.72"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.72-1.72"></path></svg>
215
+ <input type="range" id="durationSlider" min="0" max="255" value="128" step="1">
216
  <svg class="slider-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
217
  </div>
218
  </div>
 
236
  <div id="statusMessages"></div>
237
  <div id="aiLoader" style="display: none;">
238
  <div class="generator-container">
239
+ <div class="noise-layer"></div>
240
+ <div class="sketch-layer"></div>
241
+ <div class="building-layer"></div>
242
+ <div class="pixel-grid"></div>
243
+ <div class="particles"></div>
244
  <div class="text-overlay">در حال پردازش ویدیو...</div>
245
+ <div class="progress-bar"></div>
246
  </div>
247
  </div>
248
  </div>
 
263
  </div>
264
 
265
  <div id="criticalErrorSection"></div>
 
 
 
 
 
 
 
266
  </div>
267
  </div>
268
  </main>
 
272
  import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
273
 
274
  (function() {
275
+ // === START: بخش مدیریت اعتبار و ارتباط با والد (بدون تغییر) ===
276
+ let userSubscriptionStatus = 'free';
277
  let userFingerprint = null;
278
  let countdownInterval = null;
279
  const PREMIUM_PAGE_ID = '1149636';
 
347
  creditInfoDiv.textContent = `شما ${result.credits_remaining} اعتبار ساخت ویدیوی رایگان در این هفته دارید.`;
348
  }
349
  } catch (error) {
350
+ creditInfoDiv.textContent = "خطا در بررسی اعتبار.";
351
+ genBtn.disabled = true;
352
  }
353
  }
354
 
 
407
  const statusSection = document.getElementById('statusSection');
408
  const statusMessagesDiv = document.getElementById('statusMessages');
409
  const aiLoader = document.getElementById('aiLoader');
410
+ const loaderProgressBar = document.querySelector('#aiLoader .progress-bar');
411
  const loaderTextOverlay = document.querySelector('#aiLoader .text-overlay');
412
  const outputSection = document.getElementById('outputSection');
413
  const btnRestart = document.getElementById('btnRestart');
414
  const btnDownloadVideo = document.getElementById('btnDownloadVideo');
415
  const durationSlider = document.getElementById('durationSlider');
416
  const durationValueDisplay = document.getElementById('durationValue');
 
 
 
417
 
418
  // ====== تنظیمات مدل‌ها ======
419
  const VIDEO_SPACE_NAME = "mrbui1990/wan2-2-fp8da-aoti-faster";
420
  const TRANSLATOR_SPACE_NAME = "hamed744/translate-tts-aloha";
 
 
421
 
422
  const DEFAULT_NEGATIVE_PROMPT = "色调艳丽, 过曝, 静态, 细节模糊不清, 字幕, 风格, 作品, 画作, 画面, 静止, 整体发灰, 最差质量, 低质量, JPEG压缩残留, 丑陋的, 残缺的, 多余的手指, 画得不好的手部, 画得不好的脸部, 畸形的, 毁容的, 形态畸形的肢体, 手指融合, 静止不动的画面, 杂乱的背景, 三条腿, 背景人很多, 倒着走";
423
  const DEFAULT_PROMPT_TEXT = "";
424
  const TRANSPARENT_PIXEL_SRC = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
425
+ const DEFAULT_MOTION_BUCKET_ID = 128; // مقدار پیش‌فرض برای اسلایدر حرکت
426
 
427
  let lastAttemptedVideoPayload = null;
428
+ let currentJob = null; // برای لغو احتمالی در آینده
429
+
430
  function updateSliderStyle(slider) {
431
  const percentage = ((slider.value - slider.min) / (slider.max - slider.min)) * 100;
432
  const colorStops = `var(--accent-primary), var(--accent-secondary)`;
 
438
  showCriticalError("اطلاعات کافی برای تلاش مجدد وجود ندارد. لطفاً از نو شروع کنید.");
439
  return;
440
  }
441
+ proceedWithVideoGeneration();
442
  }
443
 
444
  function showCriticalError(message) {
445
+ const lowerCaseMessage = (message || "").toLowerCase();
446
  if (lowerCaseMessage.includes("gpu") || lowerCaseMessage.includes("quota") || lowerCaseMessage.includes("exceeded") || lowerCaseMessage.includes("limit") || lowerCaseMessage.includes("capacity") || lowerCaseMessage.includes("queue full")) {
447
  const detailedGpuErrorHtml = `<div class="gpu-error-message-container"><h2><span class="icon">💡</span> راهنمای رفع محدودیت ویدیو</h2><div class="error-content"><strong class="error-message-title">ارور محدودیت استفاده از GPU</strong><p>مدل‌های هوش مصنوعی برای اجرا به کارت‌های گرافیک بسیار قدرتمند نیاز دارند که این امر گاهی محدودیت‌هایی جی پی یو را در استفاده از آن‌ها ایجاد می‌کند. با این حال، خبر خوب این است که این محدودیت‌ها با استفاده از یک ترفند ساده قابل رفع هستند.</p><p><strong>برای رفع محدودیت هر بار اینگونه عمل کنید:</strong></p><ul><li>اگر از اینترنت سیم‌کارت استفاده می‌کنید، فیلترشکن خود را حتما خاموش کنید.</li><li>به مدت ۱۰ تا ۱۵ ثانیه، گوشی خود را در حالت هواپیما قرار دهید و سپس آن را غیرفعال کنید. این محدودیت با همین روش برداشته میشود</li><li>اگر از وای فای استفاده می‌کنید، مودم خود را یک بار خاموش و روشن کنید (بهتر است برای استفاده نامحدود از برنامه از اینترنت سیم کارت استفاده کنید).</li></ul><p>خلاصه: هربار این صفحه اومد اینگونه عمل کنید از اینترنت سیم کارت استفاده کنید، فیلتر شکن خاموش باشه، یک یا دوبار حالت هواپیما رو روشن و خاموش کنید. تلاش مجدداً بزنید این ارور بر طرف میشه، با این روش میشه بصورت نامحدود با این برنامه ویدیو ساخت☘️</p></div><div class="gpu-error-actions"><button class="action-button back-button" id="gpuErrorGoBackBtn">بازگشت</button><button class="action-button retry-button" id="gpuErrorRetryBtn">تلاش مجدد</button></div></div>`;
448
  criticalErrorSection.innerHTML = detailedGpuErrorHtml;
449
+ document.getElementById('gpuErrorGoBackBtn').addEventListener('click', hideCriticalError);
450
  document.getElementById('gpuErrorRetryBtn').addEventListener('click', retryLastAttempt);
451
  } else {
452
  criticalErrorSection.innerHTML = `<div id="criticalErrorMessage" style="color: var(--danger-color); font-weight: 500; margin-bottom: 1.5rem; line-height: 1.6;"><p>${message}</p></div><div class="video-controls"><button id="simpleErrorGoBackBtn" class="video-button">بازگشت</button><button id="simpleErrorRetryBtn" class="video-button primary">تلاش مجدد</button></div>`;
453
+ document.getElementById('simpleErrorGoBackBtn').addEventListener('click', hideCriticalError);
454
  document.getElementById('simpleErrorRetryBtn').addEventListener('click', retryLastAttempt);
455
  }
456
  resultContainer.classList.add('active');
 
459
  outputSection.classList.remove('active');
460
  aiLoader.style.display = 'none';
461
  generateButton.disabled = false;
462
+ }
463
+
464
+ function hideCriticalError() {
465
+ criticalErrorSection.classList.remove('active');
466
+ criticalErrorSection.innerHTML = '';
467
  }
468
 
469
  function setAiLoaderState(text, percentage = null) {
470
  if (loaderTextOverlay) loaderTextOverlay.textContent = text;
471
+ if (loaderProgressBar && percentage !== null) {
472
+ loaderProgressBar.style.width = `${Math.max(0, Math.min(100, percentage))}%`;
 
 
 
 
 
 
 
 
473
  }
 
 
474
  }
475
 
476
  function addStatusMessage(message, type = 'info') {
 
486
 
487
  function clearStatusMessages() {
488
  statusMessagesDiv.innerHTML = '';
489
+ setAiLoaderState("...", 0);
490
  aiLoader.style.display = 'none';
491
  if (outputVideo.src && outputVideo.src.startsWith('blob:')) {
492
  URL.revokeObjectURL(outputVideo.src);
 
494
  outputVideo.src = '';
495
  finalSeedElement.textContent = '';
496
  btnDownloadVideo.style.display = 'none';
 
 
497
  }
498
 
499
  function initializeForm() {
500
+ hideCriticalError();
 
 
 
 
 
501
  resultContainer.classList.remove('active');
502
  statusSection.classList.remove('active');
503
  outputSection.classList.remove('active');
 
508
  imageDropZone.classList.remove('has-image');
509
  promptInput.value = DEFAULT_PROMPT_TEXT;
510
  generateButton.disabled = false;
511
+ durationSlider.value = DEFAULT_MOTION_BUCKET_ID;
512
+ durationValueDisplay.textContent = DEFAULT_MOTION_BUCKET_ID;
513
  updateSliderStyle(durationSlider);
514
  updateUIForSubscriptionStatus(userSubscriptionStatus);
515
  }
516
 
517
  async function translatePersianToEnglish(persianText) {
518
+ if (!persianText?.trim()) return persianText;
519
+ try {
520
+ setAiLoaderState("در حال ترجمه دستور شما...", 5);
521
+ const translator = await Client.connect(TRANSLATOR_SPACE_NAME);
522
+ const resp = await translator.predict(1, [persianText, "انگلیسی (آمریکا) - جنی (زن)", 0, 0, 0]);
523
+ const english = resp?.data?.[0];
524
+ if (english) {
525
+ addStatusMessage(`ترجمه: ${english}`, 'info');
526
+ return english;
527
+ }
528
+ } catch (e) {
529
+ addStatusMessage('ترجمه ناموفق بود، با متن فارسی ادامه می‌دهیم.', 'error');
530
+ }
531
+ return persianText;
532
  }
533
 
534
+ async function handleSuccessfulVideoGeneration(result) {
535
  aiLoader.style.display = 'none';
536
+ if (!result?.data?.[0]?.video?.url) {
537
  showCriticalError('پاسخ دریافتی از سرور ویدیو معتبر نیست.');
538
  return;
539
  }
540
 
541
  if (userSubscriptionStatus === 'free') {
542
+ // ... (بخش اعتبار بدون تغییر)
 
543
  }
544
 
545
  addStatusMessage('ویدیو آماده شد! 🎉', 'success');
546
+ const videoUrl = result.data[0].video.url;
547
+ const usedSeed = result.data[1];
548
  setTimeout(() => {
549
  statusSection.classList.remove('active');
550
  outputSection.classList.add('active');
 
552
  finalSeedElement.textContent = `Seed: ${usedSeed}`;
553
  prepareCustomDownloadLink(videoUrl);
554
  }, 300);
 
 
555
  }
556
 
557
  function prepareCustomDownloadLink(videoUrl) {
 
569
  });
570
 
571
  durationSlider.addEventListener('input', () => {
572
+ durationValueDisplay.textContent = durationSlider.value;
 
573
  updateSliderStyle(durationSlider);
574
  });
575
 
 
581
 
582
  imageFileInput.addEventListener('change', function(event) {
583
  const file = event.target.files[0];
584
+ hideCriticalError();
585
  if (file) {
586
  if (!file.type.startsWith('image/')) {
587
+ addStatusMessage('فرمت نامعتبر', 'error');
588
+ initializeForm();
589
  } else {
 
 
590
  const reader = new FileReader();
591
  reader.onload = (e) => { imagePreview.src = e.target.result; imageDropZone.classList.add('has-image'); };
592
  reader.readAsDataURL(file);
 
596
  imageDropZone.classList.remove('has-image');
597
  }
598
  });
599
+
600
+ const proceedWithVideoGeneration = async () => {
601
+ const persianPrompt = promptInput.value.trim();
602
+ const imageFile = imageFileInput.files[0];
603
+ const motionBucketId = parseInt(durationSlider.value, 10);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
604
 
605
  if (!persianPrompt) { addStatusMessage('لطفاً توضیحات را وارد کنید.', 'error'); return; }
606
  if (!imageFile) { addStatusMessage('لطفاً تصویر انتخاب کنید.', 'error'); return; }
607
 
608
  clearStatusMessages();
609
+ hideCriticalError();
 
610
  generateButton.disabled = true;
611
  resultContainer.classList.add('active');
612
  statusSection.classList.add('active');
 
614
  outputSection.classList.remove('active');
615
  addStatusMessage('در حال آماده سازی...', 'info');
616
 
617
+ lastAttemptedVideoPayload = { persianPrompt, imageSourceFile: imageFile, motionBucketId };
 
 
618
 
619
+ const promptForVideo = await translatePersianToEnglish(persianPrompt);
 
 
 
 
 
 
 
 
620
 
621
  try {
622
  const client = await Client.connect(VIDEO_SPACE_NAME);
623
+ currentJob = client.submit(0, [
624
+ imageFile, // 0: input_image
625
+ promptForVideo, // 1: prompt
626
+ motionBucketId, // 2: motion_bucket_id (NEW)
627
+ DEFAULT_NEGATIVE_PROMPT, // 3: negative_prompt
628
+ 3.2, // 4: cfg
629
+ 1, // 5: steps
630
+ 1, // 6: guidance_scale_2
631
+ 0, // 7: seed (0 = random)
632
+ true // 8: randomize_seed
633
  ]);
634
+
635
+ let finalResult = null;
636
+
637
+ for await (const status of currentJob) {
638
+ if (status.type === "status") {
639
+ const msgData = status.data;
640
+ switch (msgData.msg) {
641
+ case 'estimation':
642
+ const eta = Math.round(msgData.rank_eta);
643
+ addStatusMessage(`در صف قرار گرفتید. زمان تخمینی: ${eta} ثانیه`, 'info');
644
+ setAiLoaderState(`در صف انتظار... (${eta} ثانیه)`, 10);
645
+ break;
646
+ case 'process_starts':
647
+ addStatusMessage('پردازش در سرور شروع شد...', 'info');
648
+ break;
649
+ case 'progress':
650
+ if (msgData.progress_data && msgData.progress_data.length > 0) {
651
+ const p = msgData.progress_data[0];
652
+ const percentage = p.length > 0 ? (p.index / p.length) * 100 : 0;
653
+ let stepDescription = "در حال پردازش...";
654
+ if (p.desc && p.desc.toLowerCase().includes("zerogpu")) {
655
+ stepDescription = "آماده‌سازی GPU...";
656
+ }
657
+ setAiLoaderState(`${stepDescription} (${Math.round(percentage)}%)`, 20 + (percentage * 0.7)); // Map 0-100% progress to 20-90% of loader
658
+ }
659
+ break;
660
+ }
661
+ } else if (status.type === "data") {
662
+ finalResult = status;
663
+ } else if (status.type === "log") {
664
+ // می‌توان لاگ‌ها را در کنسول نمایش داد
665
+ // console.log("Server Log:", status.data.log);
666
+ }
667
+ }
668
 
669
+ if (finalResult) {
670
+ // Gradio client has a quirk where success can be false in the final packet
671
+ // We check for the presence of video data instead.
672
+ if (finalResult.data?.data?.[0]?.video?.url) {
673
+ await handleSuccessfulVideoGeneration(finalResult.data);
674
+ } else {
675
+ const errorMessage = finalResult.data?.data?.error || "خطای نامشخصی در پردازش رخ داد.";
676
+ showCriticalError(`خطا: ${errorMessage}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
677
  }
678
+ } else {
679
+ throw new Error("نتیجه‌ای از سرور دریافت نشد.");
680
+ }
681
 
682
  } catch (errorCaught) {
683
+ showCriticalError(`خطا: ${errorCaught.message}`);
684
+ } finally {
685
  generateButton.disabled = false;
686
+ currentJob = null;
687
  }
688
  };
689
 
 
693
  if (userSubscriptionStatus === 'paid') {
694
  await proceedWithVideoGeneration();
695
  } else {
696
+ // بخش اعتبار بدون تغییر
697
+ await proceedWithVideoGeneration();
698
  }
699
  });
700
  })();
701
  </script>
702
 
703
  <script>
704
+ // --- Header Animation Script (بدون تغییر) ---
705
  document.addEventListener('DOMContentLoaded', () => {
706
  const canvas = document.getElementById('neural-network-canvas');
707
  if (!canvas) return;