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

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +83 -413
templates/index.html CHANGED
@@ -51,7 +51,6 @@
51
  @keyframes ring-rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
52
  @keyframes background-pan { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }
53
  @keyframes igadlmAlphaV2fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
54
- @keyframes fill-progress { from { width: 0%; } to { width: 100%; } }
55
  @keyframes badge-fade-in { from { opacity: 0; transform: translateY(-10px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } }
56
 
57
  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; }
@@ -87,7 +86,6 @@
87
  #image-drop-zone.has-image #imagePreview { display: block; }
88
  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; }
89
  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); }
90
-
91
  .slider-group { margin-top: 2rem; margin-bottom: 1.5rem; }
92
  .slider-label-container { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
93
  .slider-label { font-weight: 600; color: var(--text-secondary); font-size: 0.95rem; }
@@ -100,7 +98,6 @@
100
  input[type="range"]:hover { box-shadow: inset 0 1px 3px rgba(0,0,0,0.1), 0 0 0 4px var(--accent-primary-glow); }
101
  input[type="range"]:hover::-webkit-slider-thumb { box-shadow: 0 0 0 10px var(--accent-primary-glow), var(--shadow-md); transform: scale(1.1); }
102
  input[type="range"]:active::-webkit-slider-thumb { transform: scale(1.2); box-shadow: 0 0 0 12px var(--accent-primary-glow), var(--shadow-md); }
103
-
104
  #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; }
105
  #generateButton svg { width: 24px; height: 24px; margin-left: 4px; filter: drop-shadow(0 0 5px rgba(255,255,255,0.5)); }
106
  #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); }
@@ -113,26 +110,13 @@
113
  .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); }
114
  .status-message.success { border-left: 4px solid var(--success-color); color: var(--success-color); }
115
  .status-message.error { border-left: 4px solid var(--danger-color); color: var(--danger-color); }
116
- .status-icon { width: 18px; height: 18px; }
117
  #aiLoader { display: none; align-items: center; justify-content: center; }
118
- .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; }
119
- @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); } }
120
- .noise-layer, .sketch-layer, .building-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
121
- /* FIX xmlns در SVG اینجا اعمال شده */
122
- .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; }
123
- @keyframes fade-noise { 0% { opacity: 1; filter: blur(5px); } 30% { opacity: 0.8; filter: blur(2px); } 100% { opacity: 0; filter: blur(0px); } }
124
- .sketch-layer { filter: grayscale(1) contrast(1.5) blur(3px); opacity: 0; animation: reveal-sketch 7s infinite ease-in-out; }
125
- @keyframes reveal-sketch { 0% { opacity: 0; } 20% { opacity: 1; } 60% { opacity: 0.5; } 100% { opacity: 0; } }
126
- .building-layer { filter: blur(15px); opacity: 0; animation: denoise-color 7s infinite ease-in-out; }
127
- @keyframes denoise-color { 0% { opacity: 0; } 40% { opacity: 0.6; filter: blur(5px); } 100% { opacity: 1; filter: blur(0px); } }
128
- .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; }
129
- @keyframes dissolve-grid { 0% { opacity: 1; } 70% { opacity: 0.5; } 100% { opacity: 0; } }
130
- .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); }
131
- @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; } }
132
  .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;}
133
  @keyframes glow-text { 0% { opacity: 0.7; } 50% { opacity: 1; } 100% { opacity: 0.7; } }
134
- .progress-bar { position: absolute; bottom: 0; left: 0; width: 0%; height: 6px; background: linear-gradient(to right, #38bdf8, #bb86fc, #facc15); }
135
- .progress-bar.animate-progress { animation: fill-progress 60s linear forwards; }
136
  #outputVideo { width: 100%; max-width: 500px; border-radius: var(--radius-input); margin: 1rem auto; display: block; background-color: #000; box-shadow: var(--shadow-md); }
137
  #finalSeed { text-align: center; color: var(--text-tertiary); font-size: 0.85rem; margin-top: 1rem; }
138
  .video-controls { display: flex; justify-content: center; gap: 1rem; margin-top: 1.5rem; }
@@ -149,16 +133,14 @@
149
  .gpu-error-message-container .error-content ul { list-style-type: "▫️ "; padding-right: 20px; margin-top: 10px; }
150
  .gpu-error-message-container .error-content li { margin-bottom: 6px; }
151
  .gpu-error-actions { display: flex; gap: 15px; margin-top: 25px; }
 
152
  .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; }
153
  .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; }
154
-
155
  #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; }
156
  #subscription-status-badge.free-badge { background: linear-gradient(45deg, #6c757d, #495057); color: white; box-shadow: 0 4px 10px rgba(108, 117, 125, 0.3); }
157
  #subscription-status-badge.paid-badge { background: linear-gradient(45deg, var(--accent-premium), #ffca2c); color: #333; box-shadow: 0 4px 10px var(--accent-premium-glow); }
158
-
159
  #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; }
160
  #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; }
161
-
162
  @media (max-width: 768px) { main { padding: 1.5rem; } h1 { font-size: 2.2rem; } .generator-container { height: 250px; } .text-overlay { font-size: 18px; } }
163
  </style>
164
  </head>
@@ -169,14 +151,8 @@
169
  <div class="header-content">
170
  <div class="ai-orb-container">
171
  <div class="orb-background"></div>
172
- <div class="orb-core">
173
- <svg class="play-icon" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
174
- <path d="M8 5v14l11-7z"></path>
175
- </svg>
176
- </div>
177
- <div class="orb-ring one"></div>
178
- <div class="orb-ring two"></div>
179
- <div class="orb-ring three"></div>
180
  </div>
181
  <h1>استودیو ویدیو هوش مصنوعی</h1>
182
  <p class="subtitle">تصاویر خود را با قدرت هوش مصنوعی به ویدیو سینمایی تبدیل کنید</p>
@@ -187,10 +163,7 @@
187
 
188
  <main>
189
  <div class="form-group">
190
- <div class="form-label">
191
- <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>
192
- ۱. تصویر خود را انتخاب کنید
193
- </div>
194
  <label id="image-drop-zone" for="imageFile">
195
  <div class="upload-content">
196
  <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>
@@ -202,12 +175,8 @@
202
  </div>
203
 
204
  <div class="form-group">
205
- <label for="prompt" class="form-label">
206
- <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>
207
- ۲. دستور ساخت ویدیو را بنویسید
208
- </label>
209
  <textarea id="prompt" rows="4" placeholder="مثال: زوم آهسته به بیرون در حالی که برگ‌ها در باد می‌رقصند و نور خورشید از بین درختان می‌تابد"></textarea>
210
-
211
  <div class="slider-group">
212
  <div class="slider-label-container">
213
  <label for="durationSlider" class="slider-label">مدت زمان ویدیو</label>
@@ -219,52 +188,33 @@
219
  <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>
220
  </div>
221
  </div>
222
-
223
  <div id="credit-info-section"></div>
224
  <button id="upgrade-button">⭐️ ارتقا به نسخه کامل و نامحدود</button>
225
-
226
- <button id="generateButton">
227
- <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>
228
- <span>ساخت ویدیو جادویی</span>
229
- </button>
230
  </div>
231
 
232
  <div class="form-group">
233
- <div class="form-label">
234
- <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>
235
- ۳. نتیجه را ببینید
236
- </div>
237
  <div id="result-container">
238
  <div id="statusSection">
239
  <div id="statusMessages"></div>
240
  <div id="aiLoader" style="display: none;">
241
  <div class="generator-container">
242
- <div class="noise-layer"></div>
243
- <div class="sketch-layer"></div>
244
- <div class="building-layer"></div>
245
- <div class="pixel-grid"></div>
246
- <div class="particles"></div>
247
  <div class="text-overlay">در حال پردازش ویدیو...</div>
248
- <div class="progress-bar"></div>
 
 
249
  </div>
250
  </div>
251
  </div>
252
-
253
  <div id="outputSection">
254
  <video id="outputVideo" controls preload="metadata" playsinline></video>
255
  <p id="finalSeed"></p>
256
  <div class="video-controls">
257
- <button id="btnDownloadVideo" class="video-button primary" style="display: none;">
258
- <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>
259
- دانلود ویدیو
260
- </button>
261
- <button id="btnRestart" class="video-button">
262
- <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>
263
- ساخت ویدیو جدید
264
- </button>
265
  </div>
266
  </div>
267
-
268
  <div id="criticalErrorSection"></div>
269
  </div>
270
  </div>
@@ -275,128 +225,20 @@
275
  import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
276
 
277
  (function() {
278
- // === START: بخش مدیریت اعتبار و ارتباط با والد ===
279
- let userSubscriptionStatus = 'free'; // 'free' or 'paid'
280
  let userFingerprint = null;
281
  let countdownInterval = null;
282
  const PREMIUM_PAGE_ID = '1149636';
283
-
284
- async function getBrowserFingerprint() {
285
- const components = [navigator.userAgent, navigator.language, screen.width + 'x' + screen.height, new Date().getTimezoneOffset()];
286
- try {
287
- const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d');
288
- ctx.fillText("a1b2c3d4e5f6g7h8i9j0", 2, 15);
289
- components.push(canvas.toDataURL());
290
- } catch (e) { components.push("canvas-error"); }
291
- const fingerprintString = components.join('---');
292
- let hash = 0;
293
- for (let i = 0; i < fingerprintString.length; i++) {
294
- hash = ((hash << 5) - hash) + fingerprintString.charCodeAt(i);
295
- hash |= 0;
296
- }
297
- return 'fp_' + Math.abs(hash).toString(16);
298
- }
299
-
300
- function isUserPaid(userObject) {
301
- return userObject && userObject.isLogin && userObject.accessible_pages &&
302
- (userObject.accessible_pages.includes(PREMIUM_PAGE_ID) || userObject.accessible_pages.includes(parseInt(PREMIUM_PAGE_ID)));
303
- }
304
-
305
- function updateUIForSubscriptionStatus(status) {
306
- userSubscriptionStatus = status;
307
- const creditInfoDiv = document.getElementById('credit-info-section');
308
- const upgradeBtn = document.getElementById('upgrade-button');
309
- const genBtn = document.getElementById('generateButton');
310
- const subscriptionBadge = document.getElementById('subscription-status-badge');
311
-
312
- if (status === 'paid') {
313
- subscriptionBadge.textContent = 'نسخه نامحدود';
314
- subscriptionBadge.className = 'paid-badge';
315
- creditInfoDiv.style.display = 'none';
316
- upgradeBtn.style.display = 'none';
317
- genBtn.disabled = false;
318
- if(countdownInterval) clearInterval(countdownInterval);
319
- } else {
320
- subscriptionBadge.textContent = 'نسخه رایگان';
321
- subscriptionBadge.className = 'free-badge';
322
- checkFreeUserCredit();
323
- }
324
- subscriptionBadge.style.display = 'inline-block';
325
- }
326
-
327
- async function checkFreeUserCredit() {
328
- if (!userFingerprint) return;
329
- const creditInfoDiv = document.getElementById('credit-info-section');
330
- const upgradeBtn = document.getElementById('upgrade-button');
331
- const genBtn = document.getElementById('generateButton');
332
-
333
- try {
334
- const response = await fetch('/api/check-credit', {
335
- method: 'POST',
336
- headers: {'Content-Type': 'application/json'},
337
- body: JSON.stringify({ fingerprint: userFingerprint })
338
- });
339
- const result = await response.json();
340
-
341
- creditInfoDiv.style.display = 'block';
342
- if (result.limit_reached) {
343
- genBtn.disabled = true;
344
- upgradeBtn.style.display = 'block';
345
- startCountdown(result.reset_timestamp);
346
- } else {
347
- genBtn.disabled = false;
348
- upgradeBtn.style.display = 'none';
349
- if(countdownInterval) clearInterval(countdownInterval);
350
- creditInfoDiv.textContent = `شما ${result.credits_remaining} اعتبار ساخت ویدیوی رایگان در این هفته دارید.`;
351
- }
352
- } catch (error) {
353
- creditInfoDiv.textContent = "خطا در بررسی اعتبار.";
354
- genBtn.disabled = true;
355
- }
356
- }
357
-
358
- function startCountdown(resetTimestamp) {
359
- if (countdownInterval) clearInterval(countdownInterval);
360
- const creditInfoDiv = document.getElementById('credit-info-section');
361
- const updateTimer = () => {
362
- const timeLeft = Math.max(0, resetTimestamp - (Date.now() / 1000));
363
- if (timeLeft === 0) {
364
- clearInterval(countdownInterval);
365
- creditInfoDiv.textContent = 'در حال بروزرسانی اعتبار...';
366
- setTimeout(checkFreeUserCredit, 2000);
367
- return;
368
- }
369
- const d = Math.floor(timeLeft / 86400);
370
- const h = Math.floor((timeLeft % 86400) / 3600);
371
- const m = Math.floor((timeLeft % 3600) / 60);
372
- creditInfoDiv.textContent = `اعتبار شما تمام شده. زمان باقی‌مانده: ${d} روز و ${h} ساعت و ${m} دقیقه`;
373
- };
374
- updateTimer();
375
- countdownInterval = setInterval(updateTimer, 60000);
376
- }
377
-
378
- window.addEventListener('message', (event) => {
379
- if (event.data && event.data.type === 'USER_DATA_RESPONSE') {
380
- if (event.data.error || !event.data.payload) {
381
- updateUIForSubscriptionStatus('free');
382
- return;
383
- }
384
- try {
385
- const userObject = JSON.parse(event.data.payload);
386
- const status = isUserPaid(userObject) ? 'paid' : 'free';
387
- updateUIForSubscriptionStatus(status);
388
- } catch (e) {
389
- updateUIForSubscriptionStatus('free');
390
- }
391
- }
392
- });
393
-
394
- document.getElementById('upgrade-button').addEventListener('click', () => {
395
- parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM' }, '*');
396
- });
397
  // === END: بخش مدیریت اعتبار ===
398
 
399
-
400
  // DOM Elements
401
  const resultContainer = document.getElementById('result-container');
402
  const imageFileInput = document.getElementById('imageFile');
@@ -410,7 +252,7 @@
410
  const statusSection = document.getElementById('statusSection');
411
  const statusMessagesDiv = document.getElementById('statusMessages');
412
  const aiLoader = document.getElementById('aiLoader');
413
- const loaderProgressBar = document.querySelector('#aiLoader .progress-bar');
414
  const loaderTextOverlay = document.querySelector('#aiLoader .text-overlay');
415
  const outputSection = document.getElementById('outputSection');
416
  const btnRestart = document.getElementById('btnRestart');
@@ -418,48 +260,24 @@
418
  const durationSlider = document.getElementById('durationSlider');
419
  const durationValueDisplay = document.getElementById('durationValue');
420
 
421
- // ====== تنظیمات مدل‌ها ======
422
  const VIDEO_SPACE_NAME = "mrbui1990/wan2-2-fp8da-aoti-faster";
423
-
424
- // مترجم: به‌جای SSE از Gradio Client استفاده می‌کنیم (پایدار روی موبایل)
425
  const TRANSLATOR_SPACE_NAME = "hamed744/translate-tts-aloha";
426
- const FN_INDEX_TRANSLATE_SPEAK = 1;
427
- const DEFAULT_TTS_VOICE_TRANSLATOR = "انگلیسی (آمریکا) - جنی (زن)";
428
-
429
  const DEFAULT_NEGATIVE_PROMPT = "色调艳丽, 过曝, 静态, 细节模糊不清, 字幕, 风格, 作品, 画作, 画面, 静止, 整体发灰, 最差质量, 低质量, JPEG压缩残留, 丑陋的, 残缺的, 多余的手指, 画得不好的手部, 画得不好的脸部, 畸形的, 毁容的, 形态畸形的肢体, 手指融合, 静止不动的画面, 杂乱的背景, 三条腿, 背景人很多, 倒着走";
430
  const DEFAULT_PROMPT_TEXT = "";
431
  const TRANSPARENT_PIXEL_SRC = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
432
  const DEFAULT_VIDEO_DURATION = 4.3;
433
-
434
- // --- TIMEOUTS ---
435
- const GENERATION_TIMEOUT = 150000; // 2.5 دقیقه برای ساخت ویدیو
436
- const TRANSLATION_TIMEOUT = 20000; // 20 ثانیه برای ترجمه
437
 
438
  let lastAttemptedVideoPayload = null;
439
 
440
- function updateSliderStyle(slider) {
441
- const percentage = ((slider.value - slider.min) / (slider.max - slider.min)) * 100;
442
- const colorStops = `var(--accent-primary), var(--accent-secondary)`;
443
- slider.style.background = `linear-gradient(to left, ${colorStops} ${percentage}%, var(--input-bg) ${percentage}%)`;
444
- }
445
-
446
- function generateRandomHash(length = 11) {
447
- return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15).substring(0, length - 7);
448
- }
449
 
450
- async function retryLastAttempt() {
451
- if (!lastAttemptedVideoPayload || !lastAttemptedVideoPayload.imageSourceFile || !lastAttemptedVideoPayload.persianPrompt) {
452
- showCriticalError("اطلاعات کافی برای تلاش مجدد وجود ندارد. لطفاً از نو شروع کنید.");
453
- return;
454
- }
455
- generateButton.click();
456
- }
457
 
458
- function showCriticalError(message, isTranslationError = false) {
459
  const lowerCaseMessage = (message || "").toLowerCase();
460
  if (lowerCaseMessage.includes("gpu") || lowerCaseMessage.includes("quota") || lowerCaseMessage.includes("exceeded") || lowerCaseMessage.includes("limit") || lowerCaseMessage.includes("capacity") || lowerCaseMessage.includes("queue full")) {
461
- 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>`;
462
- criticalErrorSection.innerHTML = detailedGpuErrorHtml;
463
  document.getElementById('gpuErrorGoBackBtn').addEventListener('click', hideCriticalError);
464
  document.getElementById('gpuErrorRetryBtn').addEventListener('click', retryLastAttempt);
465
  } else {
@@ -475,161 +293,54 @@
475
  generateButton.disabled = false;
476
  }
477
 
478
- function hideCriticalError() {
479
- criticalErrorSection.classList.remove('active');
480
- criticalErrorSection.innerHTML = '';
481
- }
482
-
483
  function setAiLoaderText(text) { if (loaderTextOverlay) loaderTextOverlay.textContent = text; }
484
-
485
- function addStatusMessage(message, type = 'info') {
486
- if (criticalErrorSection.classList.contains('active') && type === 'error') return;
487
- resultContainer.classList.add('active');
488
- statusSection.classList.add('active');
489
- const messageDiv = document.createElement('div');
490
- messageDiv.className = `status-message ${type}`;
491
- 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>';
492
- messageDiv.innerHTML = `${iconSvg}<span class="status-text">${message}</span>`;
493
- statusMessagesDiv.insertBefore(messageDiv, statusMessagesDiv.firstChild);
494
- }
495
-
496
- function clearStatusMessages() {
497
- statusMessagesDiv.innerHTML = '';
498
- if(loaderProgressBar) {
499
- loaderProgressBar.classList.remove('animate-progress');
500
- loaderProgressBar.style.width = '0%';
501
- }
502
- aiLoader.style.display = 'none';
503
- if (outputVideo.src && outputVideo.src.startsWith('blob:')) {
504
- URL.revokeObjectURL(outputVideo.src);
505
- }
506
- outputVideo.src = '';
507
- finalSeedElement.textContent = '';
508
- btnDownloadVideo.style.display = 'none';
509
- }
510
-
511
- function initializeForm() {
512
- hideCriticalError();
513
- resultContainer.classList.remove('active');
514
- statusSection.classList.remove('active');
515
- outputSection.classList.remove('active');
516
- clearStatusMessages();
517
- lastAttemptedVideoPayload = null;
518
- imageFileInput.value = '';
519
- imagePreview.src = TRANSPARENT_PIXEL_SRC;
520
- imageDropZone.classList.remove('has-image');
521
- promptInput.value = DEFAULT_PROMPT_TEXT;
522
- generateButton.disabled = false;
523
- durationSlider.value = DEFAULT_VIDEO_DURATION;
524
- durationValueDisplay.textContent = `${parseFloat(DEFAULT_VIDEO_DURATION).toFixed(1)} ثانیه`;
525
- updateSliderStyle(durationSlider);
526
- updateUIForSubscriptionStatus(userSubscriptionStatus);
527
- }
528
 
529
- // ==== نسخه جدید مترجم با Gradio Client + تایماوت ====
530
- async function translatePersianToEnglish(persianText, { timeoutMs = TRANSLATION_TIMEOUT } = {}) {
531
- if (!persianText?.trim()) throw new Error("متن ترجمه خالی است.");
532
-
533
- if (aiLoader.style.display !== 'none') setAiLoaderText("در حال ترجمه دستور شما...");
534
- const translator = await Client.connect(TRANSLATOR_SPACE_NAME);
535
-
536
- const predictPromise = translator.predict(FN_INDEX_TRANSLATE_SPEAK, [
537
- persianText,
538
- DEFAULT_TTS_VOICE_TRANSLATOR,
539
- 0, 0, 0
540
- ]);
541
-
542
- const timeoutPromise = new Promise((_, reject) =>
543
- setTimeout(() => reject(new Error("مهلت ترجمه به پایان رسید.")), timeoutMs)
544
- );
545
-
546
- const resp = await Promise.race([predictPromise, timeoutPromise]);
547
-
548
- const english = resp?.data?.[0];
549
- if (!english) throw new Error("ترجمه نامعتبر بود.");
550
- return english;
551
- }
552
 
553
  async function handleSuccessfulVideoGeneration(result) {
554
  aiLoader.style.display = 'none';
555
- if (!result?.data?.[0]?.video?.url) {
556
- showCriticalError('پاسخ دریافتی از سرور ویدیو معتبر نیست.');
557
- return;
558
- }
559
-
560
  if (userSubscriptionStatus === 'free') {
561
- try {
562
- addStatusMessage('ویدیو ساخته شد، در حال ثبت اعتبار...', 'info');
563
- const creditResponse = await fetch('/api/use-credit', {
564
- method: 'POST',
565
- headers: {'Content-Type': 'application/json'},
566
- body: JSON.stringify({ fingerprint: userFingerprint })
567
- });
568
- if (!creditResponse.ok) {
569
- console.warn("Credit deduction failed AFTER video generation.");
570
- }
571
- checkFreeUserCredit();
572
- } catch (creditError) {
573
- console.error("Critical error deducting credit:", creditError);
574
- }
575
  }
576
-
577
  addStatusMessage('ویدیو آماده شد! 🎉', 'success');
578
  const videoUrl = result.data[0].video.url;
579
  const usedSeed = result.data[1];
580
- setTimeout(() => {
581
- statusSection.classList.remove('active');
582
- outputSection.classList.add('active');
583
- outputVideo.src = videoUrl;
584
- finalSeedElement.textContent = `Seed: ${usedSeed}`;
585
- prepareCustomDownloadLink(videoUrl);
586
- }, 300);
587
  generateButton.disabled = false;
588
  }
589
 
590
- function prepareCustomDownloadLink(videoUrl) {
591
- btnDownloadVideo.style.display = 'flex';
592
- btnDownloadVideo.onclick = () => {
593
- parent.postMessage({ type: 'DOWNLOAD_REQUEST', url: videoUrl }, '*');
594
- };
 
 
 
 
 
 
 
 
 
 
 
 
595
  }
596
-
597
- document.addEventListener('DOMContentLoaded', async () => {
598
- initializeForm();
599
- userFingerprint = await getBrowserFingerprint();
600
- parent.postMessage({ type: 'REQUEST_USER_DATA' }, '*');
601
- updateSliderStyle(durationSlider);
602
- });
603
 
604
- durationSlider.addEventListener('input', () => {
605
- const value = parseFloat(durationSlider.value).toFixed(1);
606
- durationValueDisplay.textContent = `${value} ثانیه`;
607
- updateSliderStyle(durationSlider);
608
- });
609
-
610
  ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(e => imageDropZone.addEventListener(e, ev => ev.preventDefault()));
611
  ['dragenter', 'dragover'].forEach(e => imageDropZone.addEventListener(e, () => { if (!imageDropZone.classList.contains('has-image')) imageDropZone.classList.add('drag-over'); }));
612
  ['dragleave', 'drop'].forEach(e => imageDropZone.addEventListener(e, () => imageDropZone.classList.remove('drag-over')));
613
  imageDropZone.addEventListener('drop', e => { if (e.dataTransfer.files.length > 0) { imageFileInput.files = e.dataTransfer.files; imageFileInput.dispatchEvent(new Event('change')); } });
614
  btnRestart.addEventListener('click', initializeForm);
615
-
616
- imageFileInput.addEventListener('change', function(event) {
617
- const file = event.target.files[0];
618
- hideCriticalError();
619
- if (file) {
620
- if (!file.type.startsWith('image/')) {
621
- addStatusMessage('فرمت نامعتبر', 'error');
622
- initializeForm();
623
- } else {
624
- const reader = new FileReader();
625
- reader.onload = (e) => { imagePreview.src = e.target.result; imageDropZone.classList.add('has-image'); };
626
- reader.readAsDataURL(file);
627
- }
628
- } else {
629
- imagePreview.src = TRANSPARENT_PIXEL_SRC;
630
- imageDropZone.classList.remove('has-image');
631
- }
632
- });
633
 
634
  const proceedWithVideoGeneration = async () => {
635
  const persianPrompt = promptInput.value.trim();
@@ -639,49 +350,37 @@
639
  if (!persianPrompt) { addStatusMessage('لطفاً توضیحات را وارد کنید.', 'error'); return; }
640
  if (!imageFile) { addStatusMessage('لطفاً تصویر انتخاب کنید.', 'error'); return; }
641
 
642
- clearStatusMessages();
643
- hideCriticalError();
644
- generateButton.disabled = true;
645
- resultContainer.classList.add('active');
646
- statusSection.classList.add('active');
647
- aiLoader.style.display = 'flex';
648
- loaderProgressBar.classList.add('animate-progress');
649
- outputSection.classList.remove('active');
650
- addStatusMessage('در حال آماده سازی...', 'info');
651
-
652
  lastAttemptedVideoPayload = { persianPrompt, imageSourceFile: imageFile, duration };
653
 
654
- // ---- مرحله ترجمه با فالبک ----
655
  let promptForVideo = persianPrompt;
656
  try {
657
- const englishPrompt = await translatePersianToEnglish(persianPrompt, { timeoutMs: TRANSLATION_TIMEOUT });
658
  addStatusMessage(`ترجمه: ${englishPrompt}`, 'info');
659
  promptForVideo = englishPrompt;
660
  } catch (e) {
661
- addStatusMessage('ترجمه به انگلیسی ناموفق/کند بود؛ با همان متن فارسی ادامه می‌دهیم.', 'error');
662
  }
663
 
664
  try {
665
- setAiLoaderText("در حال ساخت ویدیو...");
666
  const client = await Client.connect(VIDEO_SPACE_NAME);
667
-
668
- const timeoutPromise = new Promise((_, reject) =>
669
- setTimeout(() => reject(new Error('پردازش ویدیو بیش از 2.5 دقیقه طول کشید.')), GENERATION_TIMEOUT)
670
- );
671
-
672
- const videoGenerationPromise = client.predict(0, [
673
- imageFile, // 0: input_image
674
- promptForVideo, // 1: prompt
675
- 6, // 2: steps
676
- DEFAULT_NEGATIVE_PROMPT, // 3: negative_prompt
677
- duration, // 4: duration_seconds
678
- 1, // 5: guidance_scale
679
- 1, // 6: guidance_scale_2
680
- 0, // 7: seed (0 = random)
681
- true // 8: randomize_seed
682
  ]);
683
 
684
- const result = await Promise.race([videoGenerationPromise, timeoutPromise]);
 
 
 
 
 
 
 
 
 
685
  await handleSuccessfulVideoGeneration(result);
686
 
687
  } catch (errorCaught) {
@@ -693,50 +392,21 @@
693
 
694
  generateButton.addEventListener('click', async () => {
695
  if (generateButton.disabled) return;
696
-
697
- if (userSubscriptionStatus === 'paid') {
698
- await proceedWithVideoGeneration();
699
- } else {
700
  try {
701
- const response = await fetch('/api/check-credit', {
702
- method: 'POST',
703
- headers: {'Content-Type': 'application/json'},
704
- body: JSON.stringify({ fingerprint: userFingerprint })
705
- });
706
  const result = await response.json();
707
- if(result.limit_reached) {
708
- updateUIForSubscriptionStatus('free');
709
- return;
710
- }
711
  await proceedWithVideoGeneration();
712
-
713
- } catch (err) {
714
- showCriticalError(`خطا در سیستم اعتبار: ${err.message}`);
715
- }
716
  }
717
  });
718
  })();
719
  </script>
720
 
721
  <script>
722
- // --- Header Animation Script ---
723
- document.addEventListener('DOMContentLoaded', () => {
724
- const canvas = document.getElementById('neural-network-canvas');
725
- if (!canvas) return;
726
- const header = canvas.parentElement;
727
- const ctx = canvas.getContext('2d');
728
- let particles = [];
729
- const particleCount = 20; const maxDistance = 100;
730
- const computedStyles = getComputedStyle(document.documentElement);
731
- const particleColor = computedStyles.getPropertyValue('--accent-primary').trim();
732
- const lineColor = computedStyles.getPropertyValue('--text-tertiary').trim();
733
- function resizeCanvas() { canvas.width = header.clientWidth; canvas.height = header.clientHeight; init(); }
734
- 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(); } }
735
- function init() { particles = []; for (let i = 0; i < particleCount; i++) { particles.push(new Particle()); } }
736
- 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; }
737
- function animate() { ctx.clearRect(0, 0, canvas.width, canvas.height); particles.forEach(particle => { particle.update(); particle.draw(); }); connectParticles(); requestAnimationFrame(animate); }
738
- window.addEventListener('resize', resizeCanvas); resizeCanvas(); animate();
739
- });
740
  </script>
741
  </body>
742
  </html>
 
51
  @keyframes ring-rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
52
  @keyframes background-pan { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }
53
  @keyframes igadlmAlphaV2fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
 
54
  @keyframes badge-fade-in { from { opacity: 0; transform: translateY(-10px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } }
55
 
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; }
 
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
  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); }
 
110
  .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); }
111
  .status-message.success { border-left: 4px solid var(--success-color); color: var(--success-color); }
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
  .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; }
140
  #subscription-status-badge.free-badge { background: linear-gradient(45deg, #6c757d, #495057); color: white; box-shadow: 0 4px 10px rgba(108, 117, 125, 0.3); }
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
  <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
 
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
  </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
  <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>
 
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
  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');
 
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 {
 
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();
 
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) {
 
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>