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

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +421 -99
templates/index.html CHANGED
@@ -56,7 +56,7 @@
56
  body { font-family: var(--app-font); background-color: var(--app-bg); color: var(--text-primary); margin: 0; padding: 2.5rem 1rem; display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; }
57
  .container { max-width: 820px; width: 100%; }
58
  header { position: relative; text-align: center; margin-bottom: 2.5rem; padding: 2rem 0; animation: fadeIn 0.8s 0.1s ease-out backwards; overflow: hidden; }
59
- #neural-network-canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
60
  .header-content { position: relative; z-index: 2; }
61
  .ai-orb-container { width: 150px; height: 150px; margin: 0 auto 1rem; position: relative; display: flex; align-items: center; justify-content: center; }
62
  .orb-background { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(ellipse at center, rgba(74, 108, 250, 0.15) 0%, transparent 70%), linear-gradient(45deg, rgba(15, 212, 168, 0.05), rgba(74, 108, 250, 0.05)); border-radius: 50%; animation: background-pan 12s ease-in-out infinite; background-size: 200% 200%; }
@@ -86,6 +86,7 @@
86
  #image-drop-zone.has-image #imagePreview { display: block; }
87
  textarea { width: 100%; padding: 1rem 1.2rem; border-radius: var(--radius-input); border: 1px solid var(--input-border); background-color: var(--input-bg); color: var(--text-primary); box-shadow: var(--shadow-sm) inset; font-family: var(--app-font); font-size: 1rem; box-sizing: border-box; transition: var(--transition-smooth); min-height: 120px; resize: vertical; }
88
  textarea:focus { outline: none; border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-primary-glow), var(--shadow-sm) inset; background-color: var(--panel-bg); }
 
89
  .slider-group { margin-top: 2rem; margin-bottom: 1.5rem; }
90
  .slider-label-container { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
91
  .slider-label { font-weight: 600; color: var(--text-secondary); font-size: 0.95rem; }
@@ -98,6 +99,7 @@
98
  input[type="range"]:hover { box-shadow: inset 0 1px 3px rgba(0,0,0,0.1), 0 0 0 4px var(--accent-primary-glow); }
99
  input[type="range"]:hover::-webkit-slider-thumb { box-shadow: 0 0 0 10px var(--accent-primary-glow), var(--shadow-md); transform: scale(1.1); }
100
  input[type="range"]:active::-webkit-slider-thumb { transform: scale(1.2); box-shadow: 0 0 0 12px var(--accent-primary-glow), var(--shadow-md); }
 
101
  #generateButton { display: flex; align-items: center; justify-content: center; gap: 0.75rem; width: 100%; padding: 1.1rem; font-size: 1.2rem; font-weight: 700; background: linear-gradient(95deg, var(--accent-secondary) 0%, var(--accent-primary) 100%); color: #fff; border: none; border-radius: var(--radius-btn); cursor: pointer; transition: all 0.3s ease; box-shadow: 0 6px 12px -3px var(--accent-primary-glow), 0 6px 12px -3px rgba(15, 212, 168, 0.25); margin-top: 1rem; }
102
  #generateButton svg { width: 24px; height: 24px; margin-left: 4px; filter: drop-shadow(0 0 5px rgba(255,255,255,0.5)); }
103
  #generateButton:hover:not(:disabled) { transform: translateY(-5px) scale(1.02); box-shadow: 0 8px 20px -4px var(--accent-primary-glow), 0 8px 20px -4px rgba(15, 212, 168, 0.3); }
@@ -112,11 +114,11 @@
112
  .status-message.error { border-left: 4px solid var(--danger-color); color: var(--danger-color); }
113
  .status-icon { width: 18px; height: 18px; flex-shrink: 0; }
114
  #aiLoader { display: none; align-items: center; justify-content: center; }
115
- .generator-container { position: relative; width: 400px; max-width: 100%; height: 300px; border-radius: 20px; overflow: hidden; background-color: #161b22; color: #f0f6fc; }
116
- .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;}
 
117
  @keyframes glow-text { 0% { opacity: 0.7; } 50% { opacity: 1; } 100% { opacity: 0.7; } }
118
- .progress-bar-container { position: absolute; bottom: 20px; left: 10%; width: 80%; height: 8px; background-color: rgba(255, 255, 255, 0.2); border-radius: 4px; }
119
- .progress-bar-fill { height: 100%; width: 0%; background: linear-gradient(to right, #38bdf8, #bb86fc, #facc15); border-radius: 4px; transition: width 0.3s ease-out; }
120
  #outputVideo { width: 100%; max-width: 500px; border-radius: var(--radius-input); margin: 1rem auto; display: block; background-color: #000; box-shadow: var(--shadow-md); }
121
  #finalSeed { text-align: center; color: var(--text-tertiary); font-size: 0.85rem; margin-top: 1rem; }
122
  .video-controls { display: flex; justify-content: center; gap: 1rem; margin-top: 1.5rem; }
@@ -133,7 +135,6 @@
133
  .gpu-error-message-container .error-content ul { list-style-type: "▫️ "; padding-right: 20px; margin-top: 10px; }
134
  .gpu-error-message-container .error-content li { margin-bottom: 6px; }
135
  .gpu-error-actions { display: flex; gap: 15px; margin-top: 25px; }
136
- .gpu-error-actions .action-button { padding: 12px 20px; font-size: 1em; font-weight: 600; border-radius: var(--igadlm-alpha-border-radius-md); border: none; cursor: pointer; transition: all 0.2s; }
137
  .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; }
138
  .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; }
139
  #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,6 +142,13 @@
141
  #subscription-status-badge.paid-badge { background: linear-gradient(45deg, var(--accent-premium), #ffca2c); color: #333; box-shadow: 0 4px 10px var(--accent-premium-glow); }
142
  #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; }
143
  #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; }
 
 
 
 
 
 
 
144
  @media (max-width: 768px) { main { padding: 1.5rem; } h1 { font-size: 2.2rem; } .generator-container { height: 250px; } .text-overlay { font-size: 18px; } }
145
  </style>
146
  </head>
@@ -151,8 +159,14 @@
151
  <div class="header-content">
152
  <div class="ai-orb-container">
153
  <div class="orb-background"></div>
154
- <div class="orb-core"><svg class="play-icon" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M8 5v14l11-7z"></path></svg></div>
155
- <div class="orb-ring one"></div><div class="orb-ring two"></div><div class="orb-ring three"></div>
 
 
 
 
 
 
156
  </div>
157
  <h1>استودیو ویدیو هوش مصنوعی</h1>
158
  <p class="subtitle">تصاویر خود را با قدرت هوش مصنوعی به ویدیو سینمایی تبدیل کنید</p>
@@ -163,7 +177,10 @@
163
 
164
  <main>
165
  <div class="form-group">
166
- <div class="form-label"><svg 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="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4Z"/></svg>۱. تصویر خود را انتخاب کنید</div>
 
 
 
167
  <label id="image-drop-zone" for="imageFile">
168
  <div class="upload-content">
169
  <div class="upload-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg></div>
@@ -175,8 +192,12 @@
175
  </div>
176
 
177
  <div class="form-group">
178
- <label for="prompt" class="form-label"><svg 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="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>۲. دستور ساخت ویدیو را بنویسید</label>
 
 
 
179
  <textarea id="prompt" rows="4" placeholder="مثال: زوم آهسته به بیرون در حالی که برگ‌ها در باد می‌رقصند و نور خورشید از بین درختان می‌تابد"></textarea>
 
180
  <div class="slider-group">
181
  <div class="slider-label-container">
182
  <label for="durationSlider" class="slider-label">مدت زمان ویدیو</label>
@@ -188,34 +209,55 @@
188
  <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>
189
  </div>
190
  </div>
 
191
  <div id="credit-info-section"></div>
192
  <button id="upgrade-button">⭐️ ارتقا به نسخه کامل و نامحدود</button>
193
- <button id="generateButton"><svg 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 3L12 8L17 10L12 12L10 17L8 12L3 10L8 8L10 3z"/></svg><span>ساخت ویدیو جادویی</span></button>
 
 
 
 
194
  </div>
195
 
196
  <div class="form-group">
197
- <div class="form-label"><svg 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 3L12 8L17 10L12 12L10 17L8 12L3 10L8 8L10 3z"/><path d="M21 14l-1.5 3-3-1.5 3-3 1.5 3z"/><path d="M19.5 2.5l-3 1.5 1.5 3 3-1.5-1.5-3z"/></svg>۳. نتیجه را ببینید</div>
 
 
 
198
  <div id="result-container">
199
  <div id="statusSection">
200
  <div id="statusMessages"></div>
201
  <div id="aiLoader" style="display: none;">
202
  <div class="generator-container">
203
  <div class="text-overlay">در حال پردازش ویدیو...</div>
204
- <div class="progress-bar-container">
205
- <div class="progress-bar-fill"></div>
206
- </div>
207
  </div>
208
  </div>
209
  </div>
 
210
  <div id="outputSection">
211
  <video id="outputVideo" controls preload="metadata" playsinline></video>
212
  <p id="finalSeed"></p>
213
  <div class="video-controls">
214
- <button id="btnDownloadVideo" class="video-button primary" style="display: none;"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>دانلود ویدیو</button>
215
- <button id="btnRestart" class="video-button"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>ساخت ویدیو جدید</button>
 
 
 
 
 
 
216
  </div>
217
  </div>
 
218
  <div id="criticalErrorSection"></div>
 
 
 
 
 
 
 
219
  </div>
220
  </div>
221
  </main>
@@ -225,20 +267,128 @@
225
  import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
226
 
227
  (function() {
228
- // === START: بخش مدیریت اعتبار و ارتباط با والد (بدون تغییر) ===
229
- let userSubscriptionStatus = 'free';
230
  let userFingerprint = null;
231
  let countdownInterval = null;
232
  const PREMIUM_PAGE_ID = '1149636';
233
- async function getBrowserFingerprint() { const components = [navigator.userAgent, navigator.language, screen.width + 'x' + screen.height, new Date().getTimezoneOffset()]; try { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); ctx.fillText("a1b2c3d4e5f6g7h8i9j0", 2, 15); components.push(canvas.toDataURL()); } catch (e) { components.push("canvas-error"); } const fingerprintString = components.join('---'); let hash = 0; for (let i = 0; i < fingerprintString.length; i++) { hash = ((hash << 5) - hash) + fingerprintString.charCodeAt(i); hash |= 0; } return 'fp_' + Math.abs(hash).toString(16); }
234
- function isUserPaid(userObject) { return userObject && userObject.isLogin && userObject.accessible_pages && (userObject.accessible_pages.includes(PREMIUM_PAGE_ID) || userObject.accessible_pages.includes(parseInt(PREMIUM_PAGE_ID))); }
235
- function updateUIForSubscriptionStatus(status) { userSubscriptionStatus = status; const creditInfoDiv = document.getElementById('credit-info-section'); const upgradeBtn = document.getElementById('upgrade-button'); const genBtn = document.getElementById('generateButton'); const subscriptionBadge = document.getElementById('subscription-status-badge'); if (status === 'paid') { subscriptionBadge.textContent = 'نسخه نامحدود'; subscriptionBadge.className = 'paid-badge'; creditInfoDiv.style.display = 'none'; upgradeBtn.style.display = 'none'; genBtn.disabled = false; if(countdownInterval) clearInterval(countdownInterval); } else { subscriptionBadge.textContent = 'نسخه رایگان'; subscriptionBadge.className = 'free-badge'; checkFreeUserCredit(); } subscriptionBadge.style.display = 'inline-block'; }
236
- async function checkFreeUserCredit() { if (!userFingerprint) return; const creditInfoDiv = document.getElementById('credit-info-section'); const upgradeBtn = document.getElementById('upgrade-button'); const genBtn = document.getElementById('generateButton'); try { const response = await fetch('/api/check-credit', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ fingerprint: userFingerprint }) }); const result = await response.json(); creditInfoDiv.style.display = 'block'; if (result.limit_reached) { genBtn.disabled = true; upgradeBtn.style.display = 'block'; startCountdown(result.reset_timestamp); } else { genBtn.disabled = false; upgradeBtn.style.display = 'none'; if(countdownInterval) clearInterval(countdownInterval); creditInfoDiv.textContent = `شما ${result.credits_remaining} اعتبار ساخت ویدیوی رایگان در این هفته دارید.`; } } catch (error) { creditInfoDiv.textContent = "خطا در بررسی اعتبار."; genBtn.disabled = true; } }
237
- function startCountdown(resetTimestamp) { if (countdownInterval) clearInterval(countdownInterval); const creditInfoDiv = document.getElementById('credit-info-section'); const updateTimer = () => { const timeLeft = Math.max(0, resetTimestamp - (Date.now() / 1000)); if (timeLeft === 0) { clearInterval(countdownInterval); creditInfoDiv.textContent = 'در حال بروزرسانی اعتبار...'; setTimeout(checkFreeUserCredit, 2000); return; } const d = Math.floor(timeLeft / 86400); const h = Math.floor((timeLeft % 86400) / 3600); const m = Math.floor((timeLeft % 3600) / 60); creditInfoDiv.textContent = `اعتبار شما تمام شده. زمان باقی‌مانده: ${d} روز و ${h} ساعت و ${m} دقیقه`; }; updateTimer(); countdownInterval = setInterval(updateTimer, 60000); }
238
- window.addEventListener('message', (event) => { if (event.data && event.data.type === 'USER_DATA_RESPONSE') { if (event.data.error || !event.data.payload) { updateUIForSubscriptionStatus('free'); return; } try { const userObject = JSON.parse(event.data.payload); const status = isUserPaid(userObject) ? 'paid' : 'free'; updateUIForSubscriptionStatus(status); } catch (e) { updateUIForSubscriptionStatus('free'); } } });
239
- document.getElementById('upgrade-button').addEventListener('click', () => { parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM' }, '*'); });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  // === END: بخش مدیریت اعتبار ===
241
 
 
242
  // DOM Elements
243
  const resultContainer = document.getElementById('result-container');
244
  const imageFileInput = document.getElementById('imageFile');
@@ -252,37 +402,55 @@
252
  const statusSection = document.getElementById('statusSection');
253
  const statusMessagesDiv = document.getElementById('statusMessages');
254
  const aiLoader = document.getElementById('aiLoader');
255
- const loaderProgressBarFill = document.querySelector('.progress-bar-fill');
256
  const loaderTextOverlay = document.querySelector('#aiLoader .text-overlay');
257
  const outputSection = document.getElementById('outputSection');
258
  const btnRestart = document.getElementById('btnRestart');
259
  const btnDownloadVideo = document.getElementById('btnDownloadVideo');
260
  const durationSlider = document.getElementById('durationSlider');
261
  const durationValueDisplay = document.getElementById('durationValue');
 
 
 
262
 
 
263
  const VIDEO_SPACE_NAME = "mrbui1990/wan2-2-fp8da-aoti-faster";
264
  const TRANSLATOR_SPACE_NAME = "hamed744/translate-tts-aloha";
 
 
 
265
  const DEFAULT_NEGATIVE_PROMPT = "色调艳丽, 过曝, 静态, 细节模糊不清, 字幕, 风格, 作品, 画作, 画面, 静止, 整体发灰, 最差质量, 低质量, JPEG压缩残留, 丑陋的, 残缺的, 多余的手指, 画得不好的手部, 画得不好的脸部, 畸形的, 毁容的, 形态畸形的肢体, 手指融合, 静止不动的画面, 杂乱的背景, 三条腿, 背景人很多, 倒着走";
266
  const DEFAULT_PROMPT_TEXT = "";
267
  const TRANSPARENT_PIXEL_SRC = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
268
  const DEFAULT_VIDEO_DURATION = 4.3;
269
- const TRANSLATION_TIMEOUT = 20000;
270
 
271
  let lastAttemptedVideoPayload = null;
272
-
273
- function updateSliderStyle(slider) { const percentage = ((slider.value - slider.min) / (slider.max - slider.min)) * 100; const colorStops = `var(--accent-primary), var(--accent-secondary)`; slider.style.background = `linear-gradient(to left, ${colorStops} ${percentage}%, var(--input-bg) ${percentage}%)`; }
 
 
 
 
 
274
 
275
- async function retryLastAttempt() { if (!lastAttemptedVideoPayload || !lastAttemptedVideoPayload.imageSourceFile || !lastAttemptedVideoPayload.persianPrompt) { showCriticalError("اطلاعات کافی برای تلاش مجدد وجود ندارد. لطفاً از نو شروع کنید."); return; } generateButton.click(); }
 
 
 
 
 
 
276
 
277
  function showCriticalError(message) {
278
- const lowerCaseMessage = (message || "").toLowerCase();
279
  if (lowerCaseMessage.includes("gpu") || lowerCaseMessage.includes("quota") || lowerCaseMessage.includes("exceeded") || lowerCaseMessage.includes("limit") || lowerCaseMessage.includes("capacity") || lowerCaseMessage.includes("queue full")) {
280
- criticalErrorSection.innerHTML = `<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>`;
281
- document.getElementById('gpuErrorGoBackBtn').addEventListener('click', hideCriticalError);
 
282
  document.getElementById('gpuErrorRetryBtn').addEventListener('click', retryLastAttempt);
283
  } else {
284
  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>`;
285
- document.getElementById('simpleErrorGoBackBtn').addEventListener('click', hideCriticalError);
286
  document.getElementById('simpleErrorRetryBtn').addEventListener('click', retryLastAttempt);
287
  }
288
  resultContainer.classList.add('active');
@@ -291,122 +459,276 @@
291
  outputSection.classList.remove('active');
292
  aiLoader.style.display = 'none';
293
  generateButton.disabled = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  }
295
 
296
- function hideCriticalError() { criticalErrorSection.classList.remove('active'); criticalErrorSection.innerHTML = ''; }
297
- function setAiLoaderText(text) { if (loaderTextOverlay) loaderTextOverlay.textContent = text; }
298
- function addStatusMessage(message, type = 'info') { if (criticalErrorSection.classList.contains('active') && type === 'error') return; resultContainer.classList.add('active'); statusSection.classList.add('active'); const messageDiv = document.createElement('div'); messageDiv.className = `status-message ${type}`; const iconSvg = type === 'success' ? '<svg class="status-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/></svg>' : type === 'error' ? '<svg class="status-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>' : '<svg class="status-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>'; messageDiv.innerHTML = `${iconSvg}<span class="status-text">${message}</span>`; statusMessagesDiv.insertBefore(messageDiv, statusMessagesDiv.firstChild); }
299
- function clearStatusMessages() { statusMessagesDiv.innerHTML = ''; if(loaderProgressBarFill) loaderProgressBarFill.style.width = '0%'; aiLoader.style.display = 'none'; if (outputVideo.src && outputVideo.src.startsWith('blob:')) { URL.revokeObjectURL(outputVideo.src); } outputVideo.src = ''; finalSeedElement.textContent = ''; btnDownloadVideo.style.display = 'none'; }
300
- function initializeForm() { hideCriticalError(); resultContainer.classList.remove('active'); statusSection.classList.remove('active'); outputSection.classList.remove('active'); clearStatusMessages(); lastAttemptedVideoPayload = null; imageFileInput.value = ''; imagePreview.src = TRANSPARENT_PIXEL_SRC; imageDropZone.classList.remove('has-image'); promptInput.value = DEFAULT_PROMPT_TEXT; generateButton.disabled = false; durationSlider.value = DEFAULT_VIDEO_DURATION; durationValueDisplay.textContent = `${parseFloat(DEFAULT_VIDEO_DURATION).toFixed(1)} ثانیه`; updateSliderStyle(durationSlider); updateUIForSubscriptionStatus(userSubscriptionStatus); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
 
302
- async function translatePersianToEnglish(persianText) { if (!persianText?.trim()) throw new Error("متن ترجمه خالی است."); setAiLoaderText("در حال ترجمه دستور شما..."); const translator = await Client.connect(TRANSLATOR_SPACE_NAME); const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("مهلت ترجمه به پایان رسید.")), TRANSLATION_TIMEOUT)); const predictPromise = translator.predict(1, [persianText, "انگلیسی (آمریکا) - جنی (زن)", 0, 0, 0]); const resp = await Promise.race([predictPromise, timeoutPromise]); const english = resp?.data?.[0]; if (!english) throw new Error("ترجمه نامعتبر بود."); return english; }
 
 
 
 
 
 
 
 
303
 
304
- async function handleSuccessfulVideoGeneration(result) {
305
  aiLoader.style.display = 'none';
306
- if (!result?.data?.[0]?.video?.url) { showCriticalError('پاسخ دریافتی از سرور ویدیو معتبر نیست.'); return; }
 
 
 
 
307
  if (userSubscriptionStatus === 'free') {
308
- try { addStatusMessage('ویدیو ساخته شد، در حال ثبت اعتبار...', 'info'); const creditResponse = await fetch('/api/use-credit', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ fingerprint: userFingerprint }) }); if (!creditResponse.ok) console.warn("Credit deduction failed AFTER video generation."); checkFreeUserCredit(); } catch (creditError) { console.error("Critical error deducting credit:", creditError); }
 
309
  }
 
310
  addStatusMessage('ویدیو آماده شد! 🎉', 'success');
311
- const videoUrl = result.data[0].video.url;
312
- const usedSeed = result.data[1];
313
- setTimeout(() => { statusSection.classList.remove('active'); outputSection.classList.add('active'); outputVideo.src = videoUrl; finalSeedElement.textContent = `Seed: ${usedSeed}`; prepareCustomDownloadLink(videoUrl); }, 300);
 
 
 
 
 
 
314
  generateButton.disabled = false;
 
315
  }
316
 
317
- function prepareCustomDownloadLink(videoUrl) { btnDownloadVideo.style.display = 'flex'; btnDownloadVideo.onclick = () => { parent.postMessage({ type: 'DOWNLOAD_REQUEST', url: videoUrl }, '*'); }; }
318
-
319
- // --- تابع جدید برای مدیریت پیشرفت ---
320
- function handleProgressUpdate(progressData) {
321
- if (!progressData || progressData.length === 0) return;
322
- const p = progressData[0];
323
- const percentage = p.length > 0 ? (p.index / p.length) * 100 : 0;
324
-
325
- if(loaderProgressBarFill) loaderProgressBarFill.style.width = `${percentage}%`;
326
-
327
- let statusText = "در حال پردازش ویدیو...";
328
- if (p.desc === "ZeroGPU init") {
329
- statusText = `مرحله ۱/۲: آماده‌سازی GPU... (${Math.round(percentage)}%)`;
330
- } else if (p.desc === null) { // This is the main processing step
331
- statusText = `مرحله ۲/۲: ساخت فریم‌ها... (${Math.round(percentage)}%)`;
332
- }
333
- setAiLoaderText(statusText);
334
  }
 
 
 
 
 
 
 
335
 
336
- document.addEventListener('DOMContentLoaded', async () => { initializeForm(); userFingerprint = await getBrowserFingerprint(); parent.postMessage({ type: 'REQUEST_USER_DATA' }, '*'); updateSliderStyle(durationSlider); });
337
- durationSlider.addEventListener('input', () => { const value = parseFloat(durationSlider.value).toFixed(1); durationValueDisplay.textContent = `${value} ثانیه`; updateSliderStyle(durationSlider); });
 
 
 
 
338
  ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(e => imageDropZone.addEventListener(e, ev => ev.preventDefault()));
339
  ['dragenter', 'dragover'].forEach(e => imageDropZone.addEventListener(e, () => { if (!imageDropZone.classList.contains('has-image')) imageDropZone.classList.add('drag-over'); }));
340
  ['dragleave', 'drop'].forEach(e => imageDropZone.addEventListener(e, () => imageDropZone.classList.remove('drag-over')));
341
  imageDropZone.addEventListener('drop', e => { if (e.dataTransfer.files.length > 0) { imageFileInput.files = e.dataTransfer.files; imageFileInput.dispatchEvent(new Event('change')); } });
342
  btnRestart.addEventListener('click', initializeForm);
343
- imageFileInput.addEventListener('change', function(event) { const file = event.target.files[0]; hideCriticalError(); if (file) { if (!file.type.startsWith('image/')) { addStatusMessage('فرمت نامعتبر', 'error'); initializeForm(); } else { const reader = new FileReader(); reader.onload = (e) => { imagePreview.src = e.target.result; imageDropZone.classList.add('has-image'); }; reader.readAsDataURL(file); } } else { imagePreview.src = TRANSPARENT_PIXEL_SRC; imageDropZone.classList.remove('has-image'); } });
344
-
345
- const proceedWithVideoGeneration = async () => {
346
- const persianPrompt = promptInput.value.trim();
347
- const imageFile = imageFileInput.files[0];
348
- const duration = parseFloat(durationSlider.value);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
 
350
  if (!persianPrompt) { addStatusMessage('لطفاً توضیحات را وارد کنید.', 'error'); return; }
351
  if (!imageFile) { addStatusMessage('لطفاً تصویر انتخاب کنید.', 'error'); return; }
352
 
353
- clearStatusMessages(); hideCriticalError(); generateButton.disabled = true; resultContainer.classList.add('active'); statusSection.classList.add('active'); aiLoader.style.display = 'flex'; outputSection.classList.remove('active'); addStatusMessage('در حال آماده سازی...', 'info');
354
- lastAttemptedVideoPayload = { persianPrompt, imageSourceFile: imageFile, duration };
 
 
 
 
 
 
 
 
 
 
 
355
 
356
  let promptForVideo = persianPrompt;
357
  try {
 
358
  const englishPrompt = await translatePersianToEnglish(persianPrompt);
359
  addStatusMessage(`ترجمه: ${englishPrompt}`, 'info');
360
  promptForVideo = englishPrompt;
361
  } catch (e) {
362
- addStatusMessage('ترجمه به انگلیسی ناموفق بود؛ با همان متن فارسی ادامه می‌دهیم.', 'error');
363
  }
364
 
365
  try {
366
- setAiLoaderText("در حال اتصال به سرور ویدیو...");
367
  const client = await Client.connect(VIDEO_SPACE_NAME);
368
- addStatusMessage('اتصال به سرور ویدیو برقرار شد.', 'info');
369
-
370
- const job = client.predict(0, [
371
  imageFile, promptForVideo, 6, DEFAULT_NEGATIVE_PROMPT, duration, 1, 1, 0, true
372
  ]);
373
 
374
- job.on("status", (status) => {
375
- if (status.stage === "pending") {
376
- addStatusMessage(`در صف قرار گرفتید. نفر ${status.queue_position} هستید. زمان تخمینی: ${Math.round(status.queue_eta)} ثانیه`, 'info');
377
- setAiLoaderText(`در صف... (نفر ${status.queue_position})`);
378
- } else if (status.stage === "generating") {
379
- handleProgressUpdate(status.progress_data);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
  }
381
  });
382
 
383
- const result = await job;
384
- await handleSuccessfulVideoGeneration(result);
385
-
386
  } catch (errorCaught) {
387
- showCriticalError(`خطا: ${errorCaught.message}`);
388
- } finally {
389
  generateButton.disabled = false;
390
  }
391
  };
392
 
393
  generateButton.addEventListener('click', async () => {
394
  if (generateButton.disabled) return;
395
- if (userSubscriptionStatus === 'paid') { await proceedWithVideoGeneration(); } else {
396
- try {
397
- const response = await fetch('/api/check-credit', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ fingerprint: userFingerprint }) });
398
- const result = await response.json();
399
- if(result.limit_reached) { updateUIForSubscriptionStatus('free'); return; }
400
- await proceedWithVideoGeneration();
401
- } catch (err) { showCriticalError(`خطا در سیستم اعتبار: ${err.message}`); }
402
  }
403
  });
404
  })();
405
  </script>
406
 
407
  <script>
408
- // --- Header Animation Script (بدون تغییر) ---
409
- document.addEventListener('DOMContentLoaded', () => { const canvas = document.getElementById('neural-network-canvas'); if (!canvas) return; const header = canvas.parentElement; const ctx = canvas.getContext('2d'); let particles = []; const particleCount = 20; const maxDistance = 100; const computedStyles = getComputedStyle(document.documentElement); const particleColor = computedStyles.getPropertyValue('--accent-primary').trim(); const lineColor = computedStyles.getPropertyValue('--text-tertiary').trim(); function resizeCanvas() { canvas.width = header.clientWidth; canvas.height = header.clientHeight; init(); } class Particle { constructor() { this.x = Math.random() * canvas.width; this.y = Math.random() * canvas.height; this.vx = (Math.random() - 0.5) * 0.3; this.vy = (Math.random() - 0.5) * 0.3; this.radius = 1.2; } update() { this.x += this.vx; this.y += this.vy; if (this.x < 0 || this.x > canvas.width) this.vx *= -1; if (this.y < 0 || this.y > canvas.height) this.vy *= -1; } draw() { ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); ctx.fillStyle = particleColor; ctx.fill(); } } function init() { particles = []; for (let i = 0; i < particleCount; i++) { particles.push(new Particle()); } } function connectParticles() { for (let i = 0; i < particles.length; i++) { for (let j = i + 1; j < particles.length; j++) { const dx = particles[i].x - particles[j].x; const dy = particles[i].y - particles[j].y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < maxDistance) { ctx.beginPath(); ctx.moveTo(particles[i].x, particles[i].y); ctx.lineTo(particles[j].x, particles[j].y); ctx.strokeStyle = lineColor; ctx.lineWidth = 0.2; ctx.globalAlpha = 1 - distance / maxDistance; ctx.stroke(); } } } ctx.globalAlpha = 1; } function animate() { ctx.clearRect(0, 0, canvas.width, canvas.height); particles.forEach(particle => { particle.update(); particle.draw(); }); connectParticles(); requestAnimationFrame(animate); } window.addEventListener('resize', resizeCanvas); resizeCanvas(); animate(); });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
  </script>
411
  </body>
412
  </html>
 
56
  body { font-family: var(--app-font); background-color: var(--app-bg); color: var(--text-primary); margin: 0; padding: 2.5rem 1rem; display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; }
57
  .container { max-width: 820px; width: 100%; }
58
  header { position: relative; text-align: center; margin-bottom: 2.5rem; padding: 2rem 0; animation: fadeIn 0.8s 0.1s ease-out backwards; overflow: hidden; }
59
+ #neural-network-canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; opacity: 0.7; }
60
  .header-content { position: relative; z-index: 2; }
61
  .ai-orb-container { width: 150px; height: 150px; margin: 0 auto 1rem; position: relative; display: flex; align-items: center; justify-content: center; }
62
  .orb-background { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(ellipse at center, rgba(74, 108, 250, 0.15) 0%, transparent 70%), linear-gradient(45deg, rgba(15, 212, 168, 0.05), rgba(74, 108, 250, 0.05)); border-radius: 50%; animation: background-pan 12s ease-in-out infinite; background-size: 200% 200%; }
 
86
  #image-drop-zone.has-image #imagePreview { display: block; }
87
  textarea { width: 100%; padding: 1rem 1.2rem; border-radius: var(--radius-input); border: 1px solid var(--input-border); background-color: var(--input-bg); color: var(--text-primary); box-shadow: var(--shadow-sm) inset; font-family: var(--app-font); font-size: 1rem; box-sizing: border-box; transition: var(--transition-smooth); min-height: 120px; resize: vertical; }
88
  textarea:focus { outline: none; border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-primary-glow), var(--shadow-sm) inset; background-color: var(--panel-bg); }
89
+
90
  .slider-group { margin-top: 2rem; margin-bottom: 1.5rem; }
91
  .slider-label-container { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
92
  .slider-label { font-weight: 600; color: var(--text-secondary); font-size: 0.95rem; }
 
99
  input[type="range"]:hover { box-shadow: inset 0 1px 3px rgba(0,0,0,0.1), 0 0 0 4px var(--accent-primary-glow); }
100
  input[type="range"]:hover::-webkit-slider-thumb { box-shadow: 0 0 0 10px var(--accent-primary-glow), var(--shadow-md); transform: scale(1.1); }
101
  input[type="range"]:active::-webkit-slider-thumb { transform: scale(1.2); box-shadow: 0 0 0 12px var(--accent-primary-glow), var(--shadow-md); }
102
+
103
  #generateButton { display: flex; align-items: center; justify-content: center; gap: 0.75rem; width: 100%; padding: 1.1rem; font-size: 1.2rem; font-weight: 700; background: linear-gradient(95deg, var(--accent-secondary) 0%, var(--accent-primary) 100%); color: #fff; border: none; border-radius: var(--radius-btn); cursor: pointer; transition: all 0.3s ease; box-shadow: 0 6px 12px -3px var(--accent-primary-glow), 0 6px 12px -3px rgba(15, 212, 168, 0.25); margin-top: 1rem; }
104
  #generateButton svg { width: 24px; height: 24px; margin-left: 4px; filter: drop-shadow(0 0 5px rgba(255,255,255,0.5)); }
105
  #generateButton:hover:not(:disabled) { transform: translateY(-5px) scale(1.02); box-shadow: 0 8px 20px -4px var(--accent-primary-glow), 0 8px 20px -4px rgba(15, 212, 168, 0.3); }
 
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; }
 
135
  .gpu-error-message-container .error-content ul { list-style-type: "▫️ "; padding-right: 20px; margin-top: 10px; }
136
  .gpu-error-message-container .error-content li { margin-bottom: 6px; }
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; }
 
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>
 
159
  <div class="header-content">
160
  <div class="ai-orb-container">
161
  <div class="orb-background"></div>
162
+ <div class="orb-core">
163
+ <svg class="play-icon" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
164
+ <path d="M8 5v14l11-7z"></path>
165
+ </svg>
166
+ </div>
167
+ <div class="orb-ring one"></div>
168
+ <div class="orb-ring two"></div>
169
+ <div class="orb-ring three"></div>
170
  </div>
171
  <h1>استودیو ویدیو هوش مصنوعی</h1>
172
  <p class="subtitle">تصاویر خود را با قدرت هوش مصنوعی به ویدیو سینمایی تبدیل کنید</p>
 
177
 
178
  <main>
179
  <div class="form-group">
180
+ <div class="form-label">
181
+ <svg 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="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4Z"/></svg>
182
+ ۱. تصویر خود را انتخاب کنید
183
+ </div>
184
  <label id="image-drop-zone" for="imageFile">
185
  <div class="upload-content">
186
  <div class="upload-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg></div>
 
192
  </div>
193
 
194
  <div class="form-group">
195
+ <label for="prompt" class="form-label">
196
+ <svg 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="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
197
+ ۲. دستور ساخت ویدیو را بنویسید
198
+ </label>
199
  <textarea id="prompt" rows="4" placeholder="مثال: زوم آهسته به بیرون در حالی که برگ‌ها در باد می‌رقصند و نور خورشید از بین درختان می‌تابد"></textarea>
200
+
201
  <div class="slider-group">
202
  <div class="slider-label-container">
203
  <label for="durationSlider" class="slider-label">مدت زمان ویدیو</label>
 
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>
212
+
213
  <div id="credit-info-section"></div>
214
  <button id="upgrade-button">⭐️ ارتقا به نسخه کامل و نامحدود</button>
215
+
216
+ <button id="generateButton">
217
+ <svg 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 3L12 8L17 10L12 12L10 17L8 12L3 10L8 8L10 3z"/></svg>
218
+ <span>ساخت ویدیو جادویی</span>
219
+ </button>
220
  </div>
221
 
222
  <div class="form-group">
223
+ <div class="form-label">
224
+ <svg 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 3L12 8L17 10L12 12L10 17L8 12L3 10L8 8L10 3z"/><path d="M21 14l-1.5 3-3-1.5 3-3 1.5 3z"/><path d="M19.5 2.5l-3 1.5 1.5 3 3-1.5-1.5-3z"/></svg>
225
+ ۳. نتیجه را ببینید
226
+ </div>
227
  <div id="result-container">
228
  <div id="statusSection">
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>
237
+
238
  <div id="outputSection">
239
  <video id="outputVideo" controls preload="metadata" playsinline></video>
240
  <p id="finalSeed"></p>
241
  <div class="video-controls">
242
+ <button id="btnDownloadVideo" class="video-button primary" style="display: none;">
243
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>
244
+ دانلود ویدیو
245
+ </button>
246
+ <button id="btnRestart" class="video-button">
247
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
248
+ ساخت ویدیو جدید
249
+ </button>
250
  </div>
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
  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';
275
+
276
+ async function getBrowserFingerprint() {
277
+ const components = [navigator.userAgent, navigator.language, screen.width + 'x' + screen.height, new Date().getTimezoneOffset()];
278
+ try {
279
+ const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d');
280
+ ctx.fillText("a1b2c3d4e5f6g7h8i9j0", 2, 15);
281
+ components.push(canvas.toDataURL());
282
+ } catch (e) { components.push("canvas-error"); }
283
+ const fingerprintString = components.join('---');
284
+ let hash = 0;
285
+ for (let i = 0; i < fingerprintString.length; i++) {
286
+ hash = ((hash << 5) - hash) + fingerprintString.charCodeAt(i);
287
+ hash |= 0;
288
+ }
289
+ return 'fp_' + Math.abs(hash).toString(16);
290
+ }
291
+
292
+ function isUserPaid(userObject) {
293
+ return userObject && userObject.isLogin && userObject.accessible_pages &&
294
+ (userObject.accessible_pages.includes(PREMIUM_PAGE_ID) || userObject.accessible_pages.includes(parseInt(PREMIUM_PAGE_ID)));
295
+ }
296
+
297
+ function updateUIForSubscriptionStatus(status) {
298
+ userSubscriptionStatus = status;
299
+ const creditInfoDiv = document.getElementById('credit-info-section');
300
+ const upgradeBtn = document.getElementById('upgrade-button');
301
+ const genBtn = document.getElementById('generateButton');
302
+ const subscriptionBadge = document.getElementById('subscription-status-badge');
303
+
304
+ if (status === 'paid') {
305
+ subscriptionBadge.textContent = 'نسخه نامحدود';
306
+ subscriptionBadge.className = 'paid-badge';
307
+ creditInfoDiv.style.display = 'none';
308
+ upgradeBtn.style.display = 'none';
309
+ genBtn.disabled = false;
310
+ if(countdownInterval) clearInterval(countdownInterval);
311
+ } else {
312
+ subscriptionBadge.textContent = 'نسخه رایگان';
313
+ subscriptionBadge.className = 'free-badge';
314
+ checkFreeUserCredit();
315
+ }
316
+ subscriptionBadge.style.display = 'inline-block';
317
+ }
318
+
319
+ async function checkFreeUserCredit() {
320
+ if (!userFingerprint) return;
321
+ const creditInfoDiv = document.getElementById('credit-info-section');
322
+ const upgradeBtn = document.getElementById('upgrade-button');
323
+ const genBtn = document.getElementById('generateButton');
324
+
325
+ try {
326
+ const response = await fetch('/api/check-credit', {
327
+ method: 'POST',
328
+ headers: {'Content-Type': 'application/json'},
329
+ body: JSON.stringify({ fingerprint: userFingerprint })
330
+ });
331
+ const result = await response.json();
332
+
333
+ creditInfoDiv.style.display = 'block';
334
+ if (result.limit_reached) {
335
+ genBtn.disabled = true;
336
+ upgradeBtn.style.display = 'block';
337
+ startCountdown(result.reset_timestamp);
338
+ } else {
339
+ genBtn.disabled = false;
340
+ upgradeBtn.style.display = 'none';
341
+ if(countdownInterval) clearInterval(countdownInterval);
342
+ creditInfoDiv.textContent = `شما ${result.credits_remaining} اعتبار ساخت ویدیوی رایگان در این هفته دارید.`;
343
+ }
344
+ } catch (error) {
345
+ creditInfoDiv.textContent = "توجه: سیستم اعتبار در این نسخه آزمایشی فعال است و نیازی به پرداخت نیست.";
346
+ genBtn.disabled = false;
347
+ }
348
+ }
349
+
350
+ function startCountdown(resetTimestamp) {
351
+ if (countdownInterval) clearInterval(countdownInterval);
352
+ const creditInfoDiv = document.getElementById('credit-info-section');
353
+ const updateTimer = () => {
354
+ const timeLeft = Math.max(0, resetTimestamp - (Date.now() / 1000));
355
+ if (timeLeft === 0) {
356
+ clearInterval(countdownInterval);
357
+ creditInfoDiv.textContent = 'در حال بروزرسانی اعتبار...';
358
+ setTimeout(checkFreeUserCredit, 2000);
359
+ return;
360
+ }
361
+ const d = Math.floor(timeLeft / 86400);
362
+ const h = Math.floor((timeLeft % 86400) / 3600);
363
+ const m = Math.floor((timeLeft % 3600) / 60);
364
+ creditInfoDiv.textContent = `اعتبار شما تمام شده. زمان باقی‌مانده: ${d} روز و ${h} ساعت و ${m} دقیقه`;
365
+ };
366
+ updateTimer();
367
+ countdownInterval = setInterval(updateTimer, 60000);
368
+ }
369
+
370
+ window.addEventListener('message', (event) => {
371
+ if (event.data && event.data.type === 'USER_DATA_RESPONSE') {
372
+ if (event.data.error || !event.data.payload) {
373
+ updateUIForSubscriptionStatus('free');
374
+ return;
375
+ }
376
+ try {
377
+ const userObject = JSON.parse(event.data.payload);
378
+ const status = isUserPaid(userObject) ? 'paid' : 'free';
379
+ updateUIForSubscriptionStatus(status);
380
+ } catch (e) {
381
+ updateUIForSubscriptionStatus('free');
382
+ }
383
+ }
384
+ });
385
+
386
+ document.getElementById('upgrade-button').addEventListener('click', () => {
387
+ parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM' }, '*');
388
+ });
389
  // === END: بخش مدیریت اعتبار ===
390
 
391
+
392
  // DOM Elements
393
  const resultContainer = document.getElementById('result-container');
394
  const imageFileInput = document.getElementById('imageFile');
 
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)`;
433
+ slider.style.background = `linear-gradient(to left, ${colorStops} ${percentage}%, var(--input-bg) ${percentage}%)`;
434
+ }
435
 
436
+ async function retryLastAttempt() {
437
+ if (!lastAttemptedVideoPayload || !lastAttemptedVideoPayload.imageSourceFile || !lastAttemptedVideoPayload.persianPrompt) {
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
  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') {
483
+ if (criticalErrorSection.classList.contains('active') && type === 'error') return;
484
+ resultContainer.classList.add('active');
485
+ statusSection.classList.add('active');
486
+ const messageDiv = document.createElement('div');
487
+ messageDiv.className = `status-message ${type}`;
488
+ const iconSvg = type === 'success' ? '<svg class="status-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/></svg>' : type === 'error' ? '<svg class="status-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>' : '<svg class="status-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>';
489
+ messageDiv.innerHTML = `${iconSvg}<span class="status-text">${message}</span>`;
490
+ statusMessagesDiv.insertBefore(messageDiv, statusMessagesDiv.firstChild);
491
+ }
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);
499
+ }
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');
517
+ clearStatusMessages();
518
+ lastAttemptedVideoPayload = null;
519
+ imageFileInput.value = '';
520
+ imagePreview.src = TRANSPARENT_PIXEL_SRC;
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');
558
+ outputVideo.src = videoUrl;
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) {
567
+ btnDownloadVideo.style.display = 'flex';
568
+ btnDownloadVideo.onclick = () => {
569
+ parent.postMessage({ type: 'DOWNLOAD_REQUEST', url: videoUrl }, '*');
570
+ };
 
 
 
 
 
 
 
 
 
 
 
 
571
  }
572
+
573
+ document.addEventListener('DOMContentLoaded', async () => {
574
+ initializeForm();
575
+ userFingerprint = await getBrowserFingerprint();
576
+ parent.postMessage({ type: 'REQUEST_USER_DATA' }, '*');
577
+ updateSliderStyle(durationSlider);
578
+ });
579
 
580
+ durationSlider.addEventListener('input', () => {
581
+ const value = parseFloat(durationSlider.value).toFixed(1);
582
+ durationValueDisplay.textContent = `${value} ثانیه`;
583
+ updateSliderStyle(durationSlider);
584
+ });
585
+
586
  ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(e => imageDropZone.addEventListener(e, ev => ev.preventDefault()));
587
  ['dragenter', 'dragover'].forEach(e => imageDropZone.addEventListener(e, () => { if (!imageDropZone.classList.contains('has-image')) imageDropZone.classList.add('drag-over'); }));
588
  ['dragleave', 'drop'].forEach(e => imageDropZone.addEventListener(e, () => imageDropZone.classList.remove('drag-over')));
589
  imageDropZone.addEventListener('drop', e => { if (e.dataTransfer.files.length > 0) { imageFileInput.files = e.dataTransfer.files; imageFileInput.dispatchEvent(new Event('change')); } });
590
  btnRestart.addEventListener('click', initializeForm);
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);
604
+ }
605
+ } else {
606
+ imagePreview.src = TRANSPARENT_PIXEL_SRC;
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');
641
+ aiLoader.style.display = 'flex';
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
 
700
  generateButton.addEventListener('click', async () => {
701
  if (generateButton.disabled) return;
702
+
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;
718
+ const header = canvas.parentElement;
719
+ const ctx = canvas.getContext('2d');
720
+ let particles = [];
721
+ const particleCount = 20; const maxDistance = 100;
722
+ const computedStyles = getComputedStyle(document.documentElement);
723
+ const particleColor = computedStyles.getPropertyValue('--accent-primary').trim();
724
+ const lineColor = computedStyles.getPropertyValue('--text-tertiary').trim();
725
+ function resizeCanvas() { canvas.width = header.clientWidth; canvas.height = header.clientHeight; init(); }
726
+ class Particle { constructor() { this.x = Math.random() * canvas.width; this.y = Math.random() * canvas.height; this.vx = (Math.random() - 0.5) * 0.3; this.vy = (Math.random() - 0.5) * 0.3; this.radius = 1.2; } update() { this.x += this.vx; this.y += this.vy; if (this.x < 0 || this.x > canvas.width) this.vx *= -1; if (this.y < 0 || this.y > canvas.height) this.vy *= -1; } draw() { ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); ctx.fillStyle = particleColor; ctx.fill(); } }
727
+ function init() { particles = []; for (let i = 0; i < particleCount; i++) { particles.push(new Particle()); } }
728
+ function connectParticles() { for (let i = 0; i < particles.length; i++) { for (let j = i + 1; j < particles.length; j++) { const dx = particles[i].x - particles[j].x; const dy = particles[i].y - particles[j].y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < maxDistance) { ctx.beginPath(); ctx.moveTo(particles[i].x, particles[i].y); ctx.lineTo(particles[j].x, particles[j].y); ctx.strokeStyle = lineColor; ctx.lineWidth = 0.2; ctx.globalAlpha = 1 - distance / maxDistance; ctx.stroke(); } } } ctx.globalAlpha = 1; }
729
+ function animate() { ctx.clearRect(0, 0, canvas.width, canvas.height); particles.forEach(particle => { particle.update(); particle.draw(); }); connectParticles(); requestAnimationFrame(animate); }
730
+ window.addEventListener('resize', resizeCanvas); resizeCanvas(); animate();
731
+ });
732
  </script>
733
  </body>
734
  </html>