Ezmary commited on
Commit
2e0e75d
·
verified ·
1 Parent(s): 2ea3fc9

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +397 -152
templates/index.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>AI Video Studio - Frame Interpolation</title>
7
  <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;500;600;700;800&display=swap" rel="stylesheet">
8
  <style>
9
  :root {
@@ -33,85 +33,148 @@
33
  --radius-btn: 14px;
34
  --radius-input: 12px;
35
  --transition-smooth: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
 
 
 
 
 
36
  --igadlm-alpha-color-warning-bg: #FFFBEB;
37
  --igadlm-alpha-color-warning-border: #FBBF24;
38
  --igadlm-alpha-color-warning-text: #B45309;
39
- --igadlm-alpha-color-text-light: #4A5568;
 
40
  }
41
 
42
  @keyframes fadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
43
- @keyframes ring-rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
44
  @keyframes core-pulse { 0%, 100% { transform: scale(0.95); box-shadow: 0 0 25px var(--accent-primary-glow); } 50% { transform: scale(1.05); box-shadow: 0 0 50px rgba(74, 108, 250, 0.5); } }
45
- @keyframes badge-fade-in { from { opacity: 0; transform: translateY(-10px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } }
 
46
  @keyframes igadlmAlphaV2fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
 
47
 
48
  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; }
49
  .container { max-width: 820px; width: 100%; }
50
- header { text-align: center; margin-bottom: 2.5rem; animation: fadeIn 0.8s 0.1s ease-out backwards; }
 
 
51
  .ai-orb-container { width: 150px; height: 150px; margin: 0 auto 1rem; position: relative; display: flex; align-items: center; justify-content: center; }
 
52
  .orb-core { width: 60px; height: 60px; border-radius: 50%; background: radial-gradient(circle, #394A7A 0%, #1A202C 70%); box-shadow: 0 0 25px var(--accent-primary-glow); display: flex; align-items: center; justify-content: center; position: relative; z-index: 2; animation: core-pulse 4s ease-in-out infinite; border: 1px solid rgba(74, 108, 250, 0.3); }
53
- .play-icon { width: 24px; height: 24px; color: var(--accent-secondary); }
54
- .orb-ring { position: absolute; top: 50%; left: 50%; border-style: solid; border-color: transparent; border-radius: 50%; z-index: 1; }
55
- .orb-ring.one { width: 100px; height: 100px; margin-top: -50px; margin-left: -50px; border-width: 2px; border-top-color: var(--accent-primary); animation: ring-rotate 8s linear infinite; }
 
56
  .orb-ring.two { width: 120px; height: 120px; margin-top: -60px; margin-left: -60px; border-width: 1px; border-bottom-color: var(--accent-secondary); animation: ring-rotate 10s linear infinite reverse; }
 
57
  h1 { font-size: 2.8rem; font-weight: 800; margin: 0; background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; letter-spacing: -1px; }
58
  .subtitle { font-size: 1.1rem; color: var(--text-secondary); margin-top: 0.5rem; }
 
59
  main { padding: 3rem; background-color: var(--panel-bg); border-radius: var(--radius-card); box-shadow: var(--shadow-xl); border: 1px solid var(--panel-border); animation: fadeIn 0.8s 0.3s ease-out backwards; }
60
  .form-group { margin-bottom: 2.5rem; }
 
61
  .form-label { display: flex; align-items: center; gap: 0.75rem; font-weight: 700; color: var(--text-primary); font-size: 1.2em; margin-bottom: 1.2rem; }
62
  .form-label svg { width: 24px; height: 24px; color: var(--accent-primary); }
63
- .image-inputs-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; }
64
- .image-drop-zone { position: relative; border: 2px dashed var(--input-border); border-radius: var(--radius-input); padding: 1.5rem; text-align: center; cursor: pointer; transition: var(--transition-smooth); background-color: var(--input-bg); min-height: 200px; display: flex; flex-direction: column; justify-content: center; align-items: center; overflow: hidden; }
65
  .image-drop-zone.drag-over, .image-drop-zone:hover:not(.has-image) { border-color: var(--accent-primary); background-color: #fff; box-shadow: 0 0 15px var(--accent-primary-glow); }
66
  .image-drop-zone.has-image { border-style: solid; border-color: var(--success-color); padding: 0; cursor: default; }
67
- .upload-content p { margin: 0; color: var(--text-secondary); font-weight: 500; font-size: 0.9rem; }
68
- .upload-content-title { font-weight: 600; color: var(--text-primary); margin-bottom: 0.5rem; }
69
- .upload-icon svg { width: 36px; height: 36px; color: var(--accent-primary); opacity: 0.8; margin-bottom: 0.75rem; }
70
- .image-preview { display: none; width: 100%; height: 100%; object-fit: cover; position: absolute; top: 0; left: 0; }
71
  .image-drop-zone.has-image .upload-content { display: none; }
72
  .image-drop-zone.has-image .image-preview { display: block; }
73
- 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: 100px; resize: vertical; }
74
  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); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  #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; }
 
76
  #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); }
77
  #generateButton:disabled { background: var(--text-tertiary); cursor: not-allowed; box-shadow: none; opacity: 0.7; }
78
  #result-container { min-height: 350px; position: relative; padding: 1rem; background-color: var(--input-bg); border-radius: var(--radius-card); border: 2px dashed var(--input-border); box-shadow: var(--shadow-sm) inset; transition: var(--transition-smooth); display: flex; flex-direction: column; align-items: center; justify-content: center; }
79
  #result-container.active { border-style: solid; border-color: var(--panel-border); }
80
  #statusSection, #outputSection, #criticalErrorSection { display: none; width: 100%; }
81
  #statusSection.active, #outputSection.active, #criticalErrorSection.active { display: block; animation: fadeIn 0.5s; }
 
82
  .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); }
 
83
  .status-message.error { border-left: 4px solid var(--danger-color); color: var(--danger-color); }
84
  .status-icon { width: 18px; height: 18px; }
85
- #aiLoader { display: none; align-items: center; justify-content: center; margin-top: 1rem; }
86
- .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); background-color: #161b22; }
87
- .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); font-family: var(--app-font); width: 90%; text-align: center; color: #f0f6fc; }
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  .progress-bar { position: absolute; bottom: 0; left: 0; width: 0%; height: 6px; background: linear-gradient(to right, #38bdf8, #bb86fc, #facc15); transition: width 0.5s ease-out; }
89
- #outputVideo { width: 100%; max-width: 500px; border-radius: var(--radius-input); margin: 1rem auto; display: block; background-color: #000; }
 
90
  .video-controls { display: flex; justify-content: center; gap: 1rem; margin-top: 1.5rem; }
91
  .video-button { padding: 0.7rem 1.5rem; border-radius: var(--radius-btn); border: none; cursor: pointer; font-family: var(--app-font); font-weight: 600; font-size: 1rem; transition: var(--transition-smooth); display: flex; align-items: center; gap: 0.5rem; }
92
  .video-button.primary { background-color: var(--accent-primary); color: white; }
93
- .video-button.primary:hover { background-color: var(--accent-primary-hover); }
94
- .gpu-error-message-container { text-align: right; background-color: var(--igadlm-alpha-color-warning-bg); padding: 25px; border-radius: 12px; box-shadow: var(--shadow-lg); border: 2px solid var(--igadlm-alpha-color-warning-border); animation: igadlmAlphaV2fadeInUp 0.5s both; }
95
- .gpu-error-message-container h2 { font-size: 1.2em; color: var(--igadlm-alpha-color-warning-text); margin-bottom: 15px; font-weight: 700; }
96
- .gpu-error-message-container .error-content { font-size: 0.9rem; color: var(--igadlm-alpha-color-text-light); line-height: 1.7; }
97
- #subscription-status-badge { display: none; padding: 6px 16px; border-radius: 20px; font-size: 0.9em; font-weight: 700; margin-top: 1.25rem; animation: badge-fade-in 0.6s 0.5s ease-out backwards; }
98
- #subscription-status-badge.free-badge { background: linear-gradient(45deg, #6c757d, #495057); color: white; }
99
- #subscription-status-badge.paid-badge { background: linear-gradient(45deg, var(--accent-premium), #ffca2c); color: #333; }
100
- #credit-info-section { margin-top: 1.5rem; padding: 1rem; background-color: var(--input-bg); border-radius: var(--radius-input); text-align: center; font-size: 0.95rem; color: var(--text-secondary); border: 1px solid var(--input-border); display: none; }
 
 
 
 
 
 
 
 
101
  #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; }
102
- @media (max-width: 768px) { .image-inputs-grid { grid-template-columns: 1fr; } main { padding: 1.5rem; } h1 { font-size: 2.2rem; } }
 
103
  </style>
104
  </head>
105
  <body>
106
  <div class="container">
107
  <header>
 
108
  <div class="header-content">
109
  <div class="ai-orb-container">
110
- <div class="orb-core"><svg class="play-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"></path></svg></div>
111
- <div class="orb-ring one"></div><div class="orb-ring two"></div>
 
 
 
 
 
 
 
112
  </div>
113
  <h1>استودیو ویدیو هوش مصنوعی</h1>
114
- <p class="subtitle">ساخت ویدیو با هوش مصنوعی بین دو تصویر</p>
 
115
  <div id="subscription-status-badge" style="display:inline-block;"></div>
116
  </div>
117
  </header>
@@ -119,64 +182,86 @@
119
  <main>
120
  <div class="form-group">
121
  <div class="form-label">
122
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"><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>
123
- ۱. تصاوی�� شروع و پایان را انتخاب کنید
124
  </div>
125
- <div class="image-inputs-grid">
126
- <label id="startImageDropZone" class="image-drop-zone" for="startImageFile">
127
- <div class="upload-content">
128
- <div class="upload-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"><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>
129
- <p class="upload-content-title">تصویر شروع</p>
130
- <p>فایل را اینجا بکشید یا کلیک کنید</p>
131
- </div>
132
- <img id="startImagePreview" class="image-preview" alt="Preview Start">
133
- </label>
134
- <input type="file" id="startImageFile" accept="image/jpeg, image/png, image/webp" hidden>
135
-
136
- <label id="endImageDropZone" class="image-drop-zone" for="endImageFile">
137
- <div class="upload-content">
138
- <div class="upload-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"><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>
139
- <p class="upload-content-title">تصویر پایان</p>
140
- <p>فایل را اینجا بکشید یا کلیک کنید</p>
141
- </div>
142
- <img id="endImagePreview" class="image-preview" alt="Preview End">
143
- </label>
144
- <input type="file" id="endImageFile" accept="image/jpeg, image/png, image/webp" hidden>
145
  </div>
 
 
 
 
 
 
 
 
146
  </div>
147
 
148
  <div class="form-group">
149
  <label for="prompt" class="form-label">
150
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"><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>
151
- ۲. توضیح دهید بین دو تصویر چه اتفاقی بیفتد
152
  </label>
153
- <textarea id="prompt" rows="3" placeholder="مثال: مرد می چرخد و به سمت دوربین نگاه می کند"></textarea>
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
  <div id="credit-info-section"></div>
156
  <button id="upgrade-button">⭐️ ارتقا به نسخه کامل و نامحدود</button>
 
157
  <button id="generateButton">
158
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M10 3L12 8L17 10L12 12L10 17L8 12L3 10L8 8L10 3z"/></svg>
159
  <span>ساخت ویدیو جادویی</span>
160
  </button>
161
  </div>
162
 
163
  <div class="form-group">
164
  <div class="form-label">
165
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"><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>
166
- ۳. نتیجه را ببینید
167
  </div>
168
  <div id="result-container">
169
  <div id="statusSection">
170
  <div id="statusMessages"></div>
171
- <div id="aiLoader">
172
  <div class="generator-container">
 
 
 
 
 
173
  <div class="text-overlay">در حال پردازش ویدیو...</div>
174
  <div class="progress-bar"></div>
175
  </div>
176
  </div>
177
  </div>
 
178
  <div id="outputSection">
179
  <video id="outputVideo" controls preload="metadata" playsinline></video>
 
180
  <div class="video-controls">
181
  <button id="btnDownloadVideo" class="video-button primary" style="display: none;">
182
  <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>
@@ -188,6 +273,7 @@
188
  </button>
189
  </div>
190
  </div>
 
191
  <div id="criticalErrorSection"></div>
192
  </div>
193
  </div>
@@ -205,7 +291,6 @@
205
  const PREMIUM_PAGE_ID = '1149636';
206
 
207
  async function getBrowserFingerprint() {
208
- // ... (کد این بخش بدون تغییر باقی می‌ماند)
209
  const components = [navigator.userAgent, navigator.language, screen.width + 'x' + screen.height, new Date().getTimezoneOffset()];
210
  try {
211
  const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d');
@@ -227,7 +312,6 @@
227
  }
228
 
229
  function updateUIForSubscriptionStatus(status) {
230
- // ... (کد این بخش بدون تغییر باقی می‌ماند)
231
  userSubscriptionStatus = status;
232
  const creditInfoDiv = document.getElementById('credit-info-section');
233
  const upgradeBtn = document.getElementById('upgrade-button');
@@ -250,7 +334,6 @@
250
  }
251
 
252
  async function checkFreeUserCredit() {
253
- // ... (کد این بخش بدون تغییر باقی می‌ماند)
254
  if (!userFingerprint) return;
255
  const creditInfoDiv = document.getElementById('credit-info-section');
256
  const upgradeBtn = document.getElementById('upgrade-button');
@@ -280,8 +363,8 @@
280
  genBtn.disabled = true;
281
  }
282
  }
283
- function startCountdown(resetTimestamp) {
284
- // ... (کد این بخش بدون تغییر باقی می‌ماند)
285
  if (countdownInterval) clearInterval(countdownInterval);
286
  const creditInfoDiv = document.getElementById('credit-info-section');
287
  const updateTimer = () => {
@@ -300,30 +383,41 @@
300
  updateTimer();
301
  countdownInterval = setInterval(updateTimer, 60000);
302
  }
 
303
  window.addEventListener('message', (event) => {
304
- // ... (کد این بخش بدون تغییر باقی می‌ماند)
305
  if (event.data && event.data.type === 'USER_DATA_RESPONSE') {
306
- if (event.data.error || !event.data.payload) { updateUIForSubscriptionStatus('free'); return; }
 
 
 
307
  try {
308
  const userObject = JSON.parse(event.data.payload);
309
  const status = isUserPaid(userObject) ? 'paid' : 'free';
310
  updateUIForSubscriptionStatus(status);
311
- } catch (e) { updateUIForSubscriptionStatus('free'); }
 
 
312
  }
313
  });
314
- document.getElementById('upgrade-button').addEventListener('click', () => { parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM' }, '*'); });
 
 
 
315
  // === END: بخش مدیریت اعتبار ===
316
 
 
317
  // DOM Elements
318
- const startImageInput = document.getElementById('startImageFile');
319
- const endImageInput = document.getElementById('endImageFile');
320
- const startImagePreview = document.getElementById('startImagePreview');
321
- const endImagePreview = document.getElementById('endImagePreview');
322
- const startImageDropZone = document.getElementById('startImageDropZone');
323
- const endImageDropZone = document.getElementById('endImageDropZone');
 
324
  const promptInput = document.getElementById('prompt');
325
  const generateButton = document.getElementById('generateButton');
326
  const outputVideo = document.getElementById('outputVideo');
 
327
  const criticalErrorSection = document.getElementById('criticalErrorSection');
328
  const statusSection = document.getElementById('statusSection');
329
  const statusMessagesDiv = document.getElementById('statusMessages');
@@ -333,12 +427,22 @@
333
  const outputSection = document.getElementById('outputSection');
334
  const btnRestart = document.getElementById('btnRestart');
335
  const btnDownloadVideo = document.getElementById('btnDownloadVideo');
336
- const resultContainer = document.getElementById('result-container');
 
337
 
338
- // Model Settings
339
  const VIDEO_SPACE_URL = "https://multimodalart-wan-2-2-first-last-frame.hf.space/";
340
- const DEFAULT_NEGATIVE_PROMPT = "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指";
 
 
 
 
 
 
341
  const TRANSPARENT_PIXEL_SRC = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
 
 
 
342
 
343
  let lastAttemptedVideoPayload = null;
344
  let eventSource = null;
@@ -346,8 +450,14 @@
346
 
347
  const generateSessionHash = () => Math.random().toString(36).substring(2, 13);
348
 
 
 
 
 
 
 
349
  async function retryLastAttempt() {
350
- if (!lastAttemptedVideoPayload || !lastAttemptedVideoPayload.startImageFile || !lastAttemptedVideoPayload.endImageFile || !lastAttemptedVideoPayload.prompt) {
351
  showCriticalError("اطلاعات کافی برای تلاش مجدد وجود ندارد. لطفاً از نو شروع کنید.");
352
  return;
353
  }
@@ -355,16 +465,18 @@
355
  }
356
 
357
  function showCriticalError(message, isGpuQuotaError = false) {
358
- if (isGpuQuotaError) {
359
- const detailedGpuErrorHtml = `<div class="gpu-error-message-container"><h2>💡 راهنمای رفع محدودیت ویدیو</h2><div class="error-content"><p><strong>برای رفع محدودیت، مراحل زیر را دقیقاً انجام دهید:</strong></p><ul><li>اگر از اینترنت سیم‌کارت استفاده می‌کنید، فیلترشکن خود را حتما خاموش کنید.</li><li><strong>به مدت ۱۰ تا ۱۵ ثانیه، گوشی خود را در حالت هواپیما قرار دهید و سپس آن را غیرفعال کنید.</strong> این کار IP شما را تغییر داده و محدودیت را برطرف می‌کند.</li><li>اگر از وای فای استفاده می‌کنید، مودم خود را یک بار خاموش و روشن کنید.</li></ul><p>خلاصه: هر بار با این خطا مواجه شدید، از اینترنت سیم‌کارت بدون فیلترشکن استفاده کرده و یک یا دو بار حالت هواپیما را روشن و خاموش کنید. سپس دکمه "تلاش مجدد" را بزنید.</p></div><div class="video-controls"><button id="gpuErrorGoBackBtn" class="video-button">بازگشت</button><button id="gpuErrorRetryBtn" class="video-button primary">تلاش مجدد</button></div></div>`;
 
360
  criticalErrorSection.innerHTML = detailedGpuErrorHtml;
361
  document.getElementById('gpuErrorGoBackBtn').addEventListener('click', hideCriticalError);
362
  document.getElementById('gpuErrorRetryBtn').addEventListener('click', retryLastAttempt);
363
  } else {
364
- criticalErrorSection.innerHTML = `<div style="text-align: center; color: var(--danger-color);"><p>${message}</p></div><div class="video-controls"><button id="simpleErrorGoBackBtn" class="video-button">بازگشت</button><button id="simpleErrorRetryBtn" class="video-button primary">تلاش مجدد</button></div>`;
365
  document.getElementById('simpleErrorGoBackBtn').addEventListener('click', hideCriticalError);
366
  document.getElementById('simpleErrorRetryBtn').addEventListener('click', retryLastAttempt);
367
  }
 
368
  criticalErrorSection.classList.add('active');
369
  statusSection.classList.remove('active');
370
  outputSection.classList.remove('active');
@@ -378,7 +490,13 @@
378
  }
379
 
380
  function setAiLoaderText(text) { if (loaderTextOverlay) loaderTextOverlay.textContent = text; }
381
- function setProgressBar(percentage) { if(loaderProgressBar) loaderProgressBar.style.width = `${Math.min(100, percentage)}%`; }
 
 
 
 
 
 
382
 
383
  function addStatusMessage(message, type = 'info') {
384
  if (criticalErrorSection.classList.contains('active') && type === 'error') return;
@@ -386,42 +504,77 @@
386
  statusSection.classList.add('active');
387
  const messageDiv = document.createElement('div');
388
  messageDiv.className = `status-message ${type}`;
389
- const icon = type === 'error' ? '❗️' : '';
390
- messageDiv.innerHTML = `<span class="status-icon">${icon}</span><span class="status-text">${message}</span>`;
391
  statusMessagesDiv.insertBefore(messageDiv, statusMessagesDiv.firstChild);
392
  }
393
-
 
 
 
 
 
 
 
 
 
 
 
 
394
  function initializeForm() {
395
- if (eventSource) { eventSource.close(); eventSource = null; }
 
 
 
396
  isProcessCompleted = false;
397
  hideCriticalError();
398
  resultContainer.classList.remove('active');
399
  statusSection.classList.remove('active');
400
  outputSection.classList.remove('active');
401
- statusMessagesDiv.innerHTML = '';
402
- setProgressBar(0);
403
  lastAttemptedVideoPayload = null;
404
 
405
- startImageInput.value = '';
406
- startImagePreview.src = TRANSPARENT_PIXEL_SRC;
407
- startImageDropZone.classList.remove('has-image');
408
-
409
- endImageInput.value = '';
410
- endImagePreview.src = TRANSPARENT_PIXEL_SRC;
411
- endImageDropZone.classList.remove('has-image');
412
 
413
- promptInput.value = "";
 
 
 
 
414
  generateButton.disabled = false;
 
 
 
415
  updateUIForSubscriptionStatus(userSubscriptionStatus);
416
  }
417
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
  async function handleSuccessfulVideoGeneration(output) {
419
  aiLoader.style.display = 'none';
420
  if (!output?.data?.[0]?.video?.path) {
421
  showCriticalError('پاسخ دریافتی از سرور ویدیو معتبر نیست.');
422
  return;
423
  }
424
- if (userSubscriptionStatus === 'free') {
 
425
  try {
426
  addStatusMessage('ویدیو ساخته شد، در حال ثبت اعتبار...', 'info');
427
  await fetch('/api/use-credit', {
@@ -430,55 +583,88 @@
430
  body: JSON.stringify({ fingerprint: userFingerprint })
431
  });
432
  checkFreeUserCredit();
433
- } catch (creditError) { console.error("Critical error deducting credit:", creditError); }
 
 
434
  }
 
435
  addStatusMessage('ویدیو آماده شد! 🎉', 'success');
436
- const videoUrl = `${VIDEO_SPACE_URL}gradio_api/file=${output.data[0].video.path}`;
437
- statusSection.classList.remove('active');
438
- outputSection.classList.add('active');
439
- outputVideo.src = videoUrl;
440
- btnDownloadVideo.style.display = 'flex';
441
- btnDownloadVideo.onclick = () => parent.postMessage({ type: 'DOWNLOAD_REQUEST', url: videoUrl }, '*');
 
 
 
 
 
442
  generateButton.disabled = false;
443
  }
444
 
 
 
 
 
 
 
 
 
445
  document.addEventListener('DOMContentLoaded', async () => {
446
  initializeForm();
447
  userFingerprint = await getBrowserFingerprint();
448
  parent.postMessage({ type: 'REQUEST_USER_DATA' }, '*');
 
 
 
 
 
 
 
449
  });
450
 
451
- const setupImageInput = (dropZone, fileInput, previewEl) => {
452
  ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(e => dropZone.addEventListener(e, ev => ev.preventDefault()));
453
  ['dragenter', 'dragover'].forEach(e => dropZone.addEventListener(e, () => { if (!dropZone.classList.contains('has-image')) dropZone.classList.add('drag-over'); }));
454
  ['dragleave', 'drop'].forEach(e => dropZone.addEventListener(e, () => dropZone.classList.remove('drag-over')));
455
- dropZone.addEventListener('drop', e => { if (e.dataTransfer.files.length) { fileInput.files = e.dataTransfer.files; fileInput.dispatchEvent(new Event('change')); } });
456
- fileInput.addEventListener('change', (e) => {
457
- const file = e.target.files[0];
458
- if (file && file.type.startsWith('image/')) {
459
- const reader = new FileReader();
460
- reader.onload = (e) => { previewEl.src = e.target.result; dropZone.classList.add('has-image'); };
461
- reader.readAsDataURL(file);
 
 
 
 
 
 
462
  } else {
463
- previewEl.src = TRANSPARENT_PIXEL_SRC;
464
  dropZone.classList.remove('has-image');
465
  }
466
  });
467
- };
468
-
469
- setupImageInput(startImageDropZone, startImageInput, startImagePreview);
470
- setupImageInput(endImageDropZone, endImageInput, endImagePreview);
 
471
  btnRestart.addEventListener('click', initializeForm);
 
472
 
473
  const proceedWithVideoGeneration = async () => {
474
- const prompt = promptInput.value.trim();
475
- const startImageFile = startImageInput.files[0];
476
- const endImageFile = endImageInput.files[0];
 
477
 
478
- if (!prompt) { addStatusMessage('لطفاً توضیحات را وارد کنید.', 'error'); return; }
479
- if (!startImageFile || !endImageFile) { addStatusMessage('لطفاً هر دو تصویر شروع و پایان را انتخاب کنید.', 'error'); return; }
 
480
 
481
- statusMessagesDiv.innerHTML = '';
482
  hideCriticalError();
483
  generateButton.disabled = true;
484
  resultContainer.classList.add('active');
@@ -488,41 +674,59 @@
488
  outputSection.classList.remove('active');
489
  addStatusMessage('در حال آماده سازی...', 'info');
490
 
491
- lastAttemptedVideoPayload = { prompt, startImageFile, endImageFile };
492
  isProcessCompleted = false;
493
-
 
 
 
 
 
 
 
 
 
 
 
 
 
494
  const sessionHash = generateSessionHash();
495
 
496
  try {
497
- setAiLoaderText("آپلود تصویر شروع...");
498
- const uploadFile = async (file) => {
499
  const formData = new FormData();
500
  formData.append('files', file);
501
- const resp = await fetch(`${VIDEO_SPACE_URL}gradio_api/upload`, { method: 'POST', body: formData });
502
- if (!resp.ok) throw new Error(`خطا در آپلود: ${resp.statusText}`);
503
- return (await resp.json())[0];
504
  };
505
 
506
- const startServerPath = await uploadFile(startImageFile);
507
- setProgressBar(15);
508
- setAiLoaderText("آپلود تصویر پایان...");
509
- const endServerPath = await uploadFile(endImageFile);
510
- setProgressBar(30);
 
 
 
511
 
512
- setAiLoaderText("ارسال درخواست به صف...");
513
  const joinPayload = {
514
  "data": [
515
  { "path": startServerPath, "url": `${VIDEO_SPACE_URL}gradio_api/file=${startServerPath}` },
516
  { "path": endServerPath, "url": `${VIDEO_SPACE_URL}gradio_api/file=${endServerPath}` },
517
- prompt, DEFAULT_NEGATIVE_PROMPT, 2.1, 8, 1, 1, 42, true
518
- ], "fn_index": 0, "session_hash": sessionHash
 
 
 
519
  };
520
  const joinResponse = await fetch(`${VIDEO_SPACE_URL}gradio_api/queue/join`, {
521
  method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(joinPayload)
522
  });
523
  if (!joinResponse.ok) throw new Error(`خطا در اتصال به صف: ${joinResponse.statusText}`);
524
 
525
- setAiLoaderText("منتظر دریافت نتیجه...");
526
  eventSource = new EventSource(`${VIDEO_SPACE_URL}gradio_api/queue/data?session_hash=${sessionHash}`);
527
 
528
  eventSource.onmessage = async (event) => {
@@ -536,40 +740,60 @@
536
  addStatusMessage('پردازش شروع شد...');
537
  break;
538
  case 'progress':
539
- if (data.progress_data && data.progress_data[0] && data.progress_data[0].unit === "steps") {
540
  const p = data.progress_data[0];
541
- const percentage = 30 + (p.index / p.length) * 70;
 
 
 
 
 
 
 
 
 
 
 
 
542
  setProgressBar(percentage);
543
- setAiLoaderText(`ساخت ویدیو: ${Math.round(percentage)}%`);
544
  }
545
  break;
546
  case 'process_completed':
547
- isProcessCompleted = true;
548
  if (data.success) {
549
  setProgressBar(100);
550
  setAiLoaderText("پردازش کامل شد!");
551
  await handleSuccessfulVideoGeneration(data.output);
552
  } else {
553
- const errorMsg = data.output?.error || "یک خطای نامشخص رخ داد.";
554
- showCriticalError(errorMsg, /gpu quota/i.test(errorMsg));
 
555
  }
556
  eventSource.close();
 
557
  break;
558
  }
559
  };
560
 
561
- eventSource.onerror = () => {
562
- if (!isProcessCompleted) {
563
- showCriticalError("خطا در ارتباط با سرور. اتصال اینترنت خود را بررسی کنید.");
564
  eventSource.close();
 
565
  }
 
 
 
 
566
  };
567
- } catch (error) {
568
- showCriticalError(`خطای اولیه: ${error.message}`);
 
569
  generateButton.disabled = false;
570
  }
571
  };
572
- generateButton.addEventListener('click', async () => {
 
573
  if (generateButton.disabled) return;
574
 
575
  if (userSubscriptionStatus === 'paid') {
@@ -594,5 +818,26 @@
594
  });
595
  })();
596
  </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
597
  </body>
598
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AI Video Studio - Two Frames</title>
7
  <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;500;600;700;800&display=swap" rel="stylesheet">
8
  <style>
9
  :root {
 
33
  --radius-btn: 14px;
34
  --radius-input: 12px;
35
  --transition-smooth: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
36
+ --igadlm-alpha-color-primary: #5A67D8;
37
+ --igadlm-alpha-color-secondary: #805AD5;
38
+ --igadlm-alpha-color-primary-darker: #4C51BF;
39
+ --igadlm-alpha-color-secondary-darker: #6B46C1;
40
+ --igadlm-alpha-color-text-light: #4A5568;
41
  --igadlm-alpha-color-warning-bg: #FFFBEB;
42
  --igadlm-alpha-color-warning-border: #FBBF24;
43
  --igadlm-alpha-color-warning-text: #B45309;
44
+ --igadlm-alpha-border-radius-md: 8px;
45
+ --igadlm-alpha-border-radius-lg: 12px;
46
  }
47
 
48
  @keyframes fadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
49
+ @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
50
  @keyframes core-pulse { 0%, 100% { transform: scale(0.95); box-shadow: 0 0 25px var(--accent-primary-glow); } 50% { transform: scale(1.05); box-shadow: 0 0 50px rgba(74, 108, 250, 0.5); } }
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; }
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%; }
63
  .orb-core { width: 60px; height: 60px; border-radius: 50%; background: radial-gradient(circle, #394A7A 0%, #1A202C 70%); box-shadow: 0 0 25px var(--accent-primary-glow); display: flex; align-items: center; justify-content: center; position: relative; z-index: 2; animation: core-pulse 4s ease-in-out infinite; border: 1px solid rgba(74, 108, 250, 0.3); }
64
+ .play-icon { width: 24px; height: 24px; color: var(--accent-secondary); filter: drop-shadow(0 0 8px rgba(15, 212, 168, 0.7)); transition: var(--transition-smooth); }
65
+ .orb-core:hover .play-icon { transform: scale(1.1); color: #fff; filter: drop-shadow(0 0 12px rgba(15, 212, 168, 1)); }
66
+ .orb-ring { position: absolute; top: 50%; left: 50%; border-style: solid; border-color: transparent; border-radius: 50%; z-index: 1; mix-blend-mode: screen; }
67
+ .orb-ring.one { width: 100px; height: 100px; margin-top: -50px; margin-left: -50px; border-width: 2px; border-top-color: var(--accent-primary); border-right-color: var(--accent-primary); animation: ring-rotate 8s linear infinite; filter: blur(1px); }
68
  .orb-ring.two { width: 120px; height: 120px; margin-top: -60px; margin-left: -60px; border-width: 1px; border-bottom-color: var(--accent-secondary); animation: ring-rotate 10s linear infinite reverse; }
69
+ .orb-ring.three { width: 140px; height: 140px; margin-top: -70px; margin-left: -70px; border-width: 2px; border-left-color: rgba(255, 255, 255, 0.5); border-right-color: rgba(255, 255, 255, 0.5); animation: ring-rotate 12s linear infinite; filter: blur(1px); opacity: 0.5; }
70
  h1 { font-size: 2.8rem; font-weight: 800; margin: 0; background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; letter-spacing: -1px; }
71
  .subtitle { font-size: 1.1rem; color: var(--text-secondary); margin-top: 0.5rem; }
72
+ .instruction { font-size: 0.95rem; color: var(--text-tertiary); margin-top: 0.75rem; font-weight: 500; }
73
  main { padding: 3rem; background-color: var(--panel-bg); border-radius: var(--radius-card); box-shadow: var(--shadow-xl); border: 1px solid var(--panel-border); animation: fadeIn 0.8s 0.3s ease-out backwards; }
74
  .form-group { margin-bottom: 2.5rem; }
75
+ .form-group:last-child { margin-bottom: 0; }
76
  .form-label { display: flex; align-items: center; gap: 0.75rem; font-weight: 700; color: var(--text-primary); font-size: 1.2em; margin-bottom: 1.2rem; }
77
  .form-label svg { width: 24px; height: 24px; color: var(--accent-primary); }
78
+ .image-drop-zone { position: relative; border: 2px dashed var(--input-border); border-radius: var(--radius-input); padding: 2.5rem; text-align: center; cursor: pointer; transition: var(--transition-smooth); background-color: var(--input-bg); min-height: 200px; display: flex; flex-direction: column; justify-content: center; align-items: center; overflow: hidden; }
 
79
  .image-drop-zone.drag-over, .image-drop-zone:hover:not(.has-image) { border-color: var(--accent-primary); background-color: #fff; box-shadow: 0 0 15px var(--accent-primary-glow); }
80
  .image-drop-zone.has-image { border-style: solid; border-color: var(--success-color); padding: 0; cursor: default; }
81
+ .upload-content { display: flex; flex-direction: column; align-items: center; gap: 1rem; }
82
+ .upload-icon svg { width: 48px; height: 48px; color: var(--accent-primary); stroke-width: 1.5; opacity: 0.8; }
83
+ .image-drop-zone p { margin: 0; color: var(--text-secondary); font-weight: 500; }
84
+ .image-preview { display: none; width: 100%; height: 100%; object-fit: contain; position: absolute; top: 0; left: 0; }
85
  .image-drop-zone.has-image .upload-content { display: none; }
86
  .image-drop-zone.has-image .image-preview { 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; }
93
+ .slider-value { font-weight: 700; font-size: 1rem; color: var(--accent-primary); background-color: var(--input-bg); padding: 0.25rem 0.75rem; border-radius: 8px; border: 1px solid var(--input-border); min-width: 80px; text-align: center; }
94
+ .slider-wrapper { display: flex; align-items: center; gap: 1rem; }
95
+ .slider-icon { width: 22px; height: 22px; color: var(--text-tertiary); flex-shrink: 0; opacity: 0.8; }
96
+ input[type="range"] { -webkit-appearance: none; appearance: none; flex-grow: 1; width: 100%; height: 12px; background: var(--input-bg); border-radius: var(--radius-input); outline: none; cursor: pointer; direction: rtl; box-shadow: inset 0 1px 3px rgba(0,0,0,0.1); transition: var(--transition-smooth); }
97
+ input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 24px; height: 24px; background: linear-gradient(145deg, var(--accent-primary), var(--accent-secondary)); border-radius: 50%; border: 3px solid white; box-shadow: var(--shadow-md); transition: all 0.2s ease-in-out; margin-top: -6px; }
98
+ input[type="range"]::-moz-range-thumb { width: 24px; height: 24px; background: linear-gradient(145deg, var(--accent-primary), var(--accent-secondary)); border-radius: 50%; border: 3px solid white; box-shadow: var(--shadow-md); transition: all 0.2s ease-in-out; }
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); }
106
  #generateButton:disabled { background: var(--text-tertiary); cursor: not-allowed; box-shadow: none; opacity: 0.7; }
107
  #result-container { min-height: 350px; position: relative; padding: 1rem; background-color: var(--input-bg); border-radius: var(--radius-card); border: 2px dashed var(--input-border); box-shadow: var(--shadow-sm) inset; transition: var(--transition-smooth); display: flex; flex-direction: column; align-items: center; justify-content: center; }
108
  #result-container.active { border-style: solid; border-color: var(--panel-border); }
109
  #statusSection, #outputSection, #criticalErrorSection { display: none; width: 100%; }
110
  #statusSection.active, #outputSection.active, #criticalErrorSection.active { display: block; animation: fadeIn 0.5s; }
111
+ #statusMessages { max-height: 150px; overflow-y: auto; width: 100%; padding: 0.5rem; margin-bottom: 1rem; }
112
  .status-message { display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem 1rem; margin-bottom: 0.5rem; border-radius: var(--radius-input); font-size: 0.9rem; background: var(--panel-bg); border: 1px solid var(--panel-border); color: var(--text-secondary); }
113
+ .status-message.success { border-left: 4px solid var(--success-color); color: var(--success-color); }
114
  .status-message.error { border-left: 4px solid var(--danger-color); color: var(--danger-color); }
115
  .status-icon { width: 18px; height: 18px; }
116
+ #aiLoader { display: none; align-items: center; justify-content: center; }
117
+ .generator-container { position: relative; width: 400px; max-width: 100%; height: 300px; border: 2px solid #38bdf8; border-radius: 20px; overflow: hidden; box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); animation: pulse-loader 5s infinite cubic-bezier(0.4, 0, 0.6, 1); background-color: #161b22; color: #f0f6fc; }
118
+ @keyframes pulse-loader { 0% { box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); } 50% { box-shadow: 0 0 60px rgba(56, 189, 248, 0.7); } 100% { box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); } }
119
+ .noise-layer, .sketch-layer, .building-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
120
+ .noise-layer { background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><rect width="100" height="100" fill="none"/><filter id="noise"><feTurbulence type="fractalNoise" baseFrequency="0.5" numOctaves="4" stitchTiles="stitch"/></filter><rect width="100%" height="100%" filter="url(%23noise)" opacity="0.6"/></svg>') repeat; opacity: 1; animation: fade-noise 7s infinite ease-in-out; }
121
+ @keyframes fade-noise { 0% { opacity: 1; filter: blur(5px); } 30% { opacity: 0.8; filter: blur(2px); } 100% { opacity: 0; filter: blur(0px); } }
122
+ .sketch-layer { filter: grayscale(1) contrast(1.5) blur(3px); opacity: 0; animation: reveal-sketch 7s infinite ease-in-out; }
123
+ @keyframes reveal-sketch { 0% { opacity: 0; } 20% { opacity: 1; } 60% { opacity: 0.5; } 100% { opacity: 0; } }
124
+ .building-layer { filter: blur(15px); opacity: 0; animation: denoise-color 7s infinite ease-in-out; }
125
+ @keyframes denoise-color { 0% { opacity: 0; } 40% { opacity: 0.6; filter: blur(5px); } 100% { opacity: 1; filter: blur(0px); } }
126
+ .pixel-grid { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: repeating-linear-gradient(0deg, transparent 0 1px, rgba(255,255,255,0.1) 1px 2px), repeating-linear-gradient(90deg, transparent 0 1px, rgba(255,255,255,0.1) 1px 2px); opacity: 1; animation: dissolve-grid 7s infinite ease-in-out; }
127
+ @keyframes dissolve-grid { 0% { opacity: 1; } 70% { opacity: 0.5; } 100% { opacity: 0; } }
128
+ .particles { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(circle, rgba(56, 189, 248, 0.2) 0%, transparent 50%); animation: flow-particles 7s infinite cubic-bezier(0.4, 0, 0.6, 1); }
129
+ @keyframes flow-particles { 0% { transform: translate(0, 0) scale(1); opacity: 0.5; } 50% { transform: translate(10px, -15px) scale(1.05); opacity: 0.8; } 100% { transform: translate(0, 0) scale(1); opacity: 0.5; } }
130
+ .text-overlay { position: absolute; top: 45%; left: 50%; transform: translate(-50%, -50%); font-size: 24px; font-weight: 700; text-shadow: 0 0 20px rgba(56, 189, 248, 0.8); animation: glow-text 7s infinite ease-in-out; font-family: var(--app-font); width: 90%; text-align: center;}
131
+ @keyframes glow-text { 0% { opacity: 0.7; } 50% { opacity: 1; } 100% { opacity: 0.7; } }
132
  .progress-bar { position: absolute; bottom: 0; left: 0; width: 0%; height: 6px; background: linear-gradient(to right, #38bdf8, #bb86fc, #facc15); transition: width 0.5s ease-out; }
133
+ #outputVideo { width: 100%; max-width: 500px; border-radius: var(--radius-input); margin: 1rem auto; display: block; background-color: #000; box-shadow: var(--shadow-md); }
134
+ #finalSeed { text-align: center; color: var(--text-tertiary); font-size: 0.85rem; margin-top: 1rem; }
135
  .video-controls { display: flex; justify-content: center; gap: 1rem; margin-top: 1.5rem; }
136
  .video-button { padding: 0.7rem 1.5rem; border-radius: var(--radius-btn); border: none; cursor: pointer; font-family: var(--app-font); font-weight: 600; font-size: 1rem; transition: var(--transition-smooth); display: flex; align-items: center; gap: 0.5rem; }
137
  .video-button.primary { background-color: var(--accent-primary); color: white; }
138
+ .video-button.primary:hover { background-color: var(--accent-primary-hover); transform: translateY(-2px); }
139
+ .video-button:not(.primary) { background-color: var(--input-bg); color: var(--text-secondary); border: 1px solid var(--input-border); }
140
+ .video-button:not(.primary):hover { background-color: var(--panel-border); color: var(--text-primary); }
141
+ .gpu-error-message-container { text-align: right; background-color: var(--igadlm-alpha-color-warning-bg); padding: 25px; border-radius: var(--igadlm-alpha-border-radius-lg); box-shadow: var(--shadow-lg); border: 2px solid var(--igadlm-alpha-color-warning-border); animation: igadlmAlphaV2fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1) both; }
142
+ .gpu-error-message-container h2 { font-size: 1.2em; color: var(--igadlm-alpha-color-warning-text); margin-bottom: 15px; font-weight: 700; display: flex; align-items: center; border-bottom: 1px solid var(--igadlm-alpha-color-warning-border); padding-bottom: 12px; }
143
+ .gpu-error-message-container h2 .icon { margin-left: 10px; font-size: 1.3em; }
144
+ .gpu-error-message-container .error-content { font-size: 0.9rem; color: var(--igadlm-alpha-color-text-light); line-height: 1.7; word-break: break-word; white-space: pre-line; }
145
+ .gpu-error-message-container .error-content strong.error-message-title { font-weight: 600; color: var(--igadlm-alpha-color-warning-text); display: block; margin-bottom: 8px;}
146
+ .gpu-error-message-container .error-content ul { list-style-type: "▫️ "; padding-right: 20px; margin-top: 10px; }
147
+ .gpu-error-message-container .error-content li { margin-bottom: 6px; }
148
+
149
+ #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; }
150
+ #subscription-status-badge.free-badge { background: linear-gradient(45deg, #6c757d, #495057); color: white; box-shadow: 0 4px 10px rgba(108, 117, 125, 0.3); }
151
+ #subscription-status-badge.paid-badge { background: linear-gradient(45deg, var(--accent-premium), #ffca2c); color: #333; box-shadow: 0 4px 10px var(--accent-premium-glow); }
152
+
153
+ #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; }
154
  #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; }
155
+
156
+ @media (max-width: 768px) { main { padding: 1.5rem; } h1 { font-size: 2.2rem; } .generator-container { height: 250px; } .text-overlay { font-size: 18px; } }
157
  </style>
158
  </head>
159
  <body>
160
  <div class="container">
161
  <header>
162
+ <canvas id="neural-network-canvas"></canvas>
163
  <div class="header-content">
164
  <div class="ai-orb-container">
165
+ <div class="orb-background"></div>
166
+ <div class="orb-core">
167
+ <svg class="play-icon" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
168
+ <path d="M8 5v14l11-7z"></path>
169
+ </svg>
170
+ </div>
171
+ <div class="orb-ring one"></div>
172
+ <div class="orb-ring two"></div>
173
+ <div class="orb-ring three"></div>
174
  </div>
175
  <h1>استودیو ویدیو هوش مصنوعی</h1>
176
+ <p class="subtitle">تصاویر خود را با قدرت هوش مصنوعی به ویدیو سینمایی تبدیل کنید</p>
177
+ <p class="instruction">دو تصویر برای شروع و پایان ویدیو انتخاب کرده و دستور ساخت را بنویسید.</p>
178
  <div id="subscription-status-badge" style="display:inline-block;"></div>
179
  </div>
180
  </header>
 
182
  <main>
183
  <div class="form-group">
184
  <div class="form-label">
185
+ <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>
186
+ ۱. تصویر شروع را انتخاب کنید
187
  </div>
188
+ <label id="image-drop-zone-start" class="image-drop-zone" for="imageFileStart">
189
+ <div class="upload-content">
190
+ <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>
191
+ <p>فایل تصویر شروع را اینجا بکشید یا برای انتخاب کلیک کنید</p>
192
+ </div>
193
+ <img id="imagePreviewStart" class="image-preview" src="" alt="Preview Start">
194
+ </label>
195
+ <input type="file" id="imageFileStart" accept="image/jpeg, image/png, image/webp" hidden>
196
+ </div>
197
+
198
+ <div class="form-group">
199
+ <div class="form-label">
200
+ <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>
201
+ ۲. تصویر پایان را انتخاب کنید
 
 
 
 
 
 
202
  </div>
203
+ <label id="image-drop-zone-end" class="image-drop-zone" for="imageFileEnd">
204
+ <div class="upload-content">
205
+ <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>
206
+ <p>فایل تصویر پایان را اینجا بکشید یا برای انتخاب کلیک کنید</p>
207
+ </div>
208
+ <img id="imagePreviewEnd" class="image-preview" src="" alt="Preview End">
209
+ </label>
210
+ <input type="file" id="imageFileEnd" accept="image/jpeg, image/png, image/webp" hidden>
211
  </div>
212
 
213
  <div class="form-group">
214
  <label for="prompt" class="form-label">
215
+ <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>
216
+ ۳. دستور ساخت ویدیو را بنویسید
217
  </label>
218
+ <textarea id="prompt" rows="4" placeholder="مثال: مرد رو به دوربین می‌چرخد و لبخند می‌زند"></textarea>
219
+
220
+ <div class="slider-group">
221
+ <div class="slider-label-container">
222
+ <label for="durationSlider" class="slider-label">مدت زمان ویدیو</label>
223
+ <span id="durationValue" class="slider-value">2.1 ثانیه</span>
224
+ </div>
225
+ <div class="slider-wrapper">
226
+ <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>
227
+ <input type="range" id="durationSlider" min="0.5" max="5.0" value="2.1" step="0.1">
228
+ <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>
229
+ </div>
230
+ </div>
231
 
232
  <div id="credit-info-section"></div>
233
  <button id="upgrade-button">⭐️ ارتقا به نسخه کامل و نامحدود</button>
234
+
235
  <button id="generateButton">
236
+ <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>
237
  <span>ساخت ویدیو جادویی</span>
238
  </button>
239
  </div>
240
 
241
  <div class="form-group">
242
  <div class="form-label">
243
+ <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>
244
+ ۴. نتیجه را ببینید
245
  </div>
246
  <div id="result-container">
247
  <div id="statusSection">
248
  <div id="statusMessages"></div>
249
+ <div id="aiLoader" style="display: none;">
250
  <div class="generator-container">
251
+ <div class="noise-layer"></div>
252
+ <div class="sketch-layer"></div>
253
+ <div class="building-layer"></div>
254
+ <div class="pixel-grid"></div>
255
+ <div class="particles"></div>
256
  <div class="text-overlay">در حال پردازش ویدیو...</div>
257
  <div class="progress-bar"></div>
258
  </div>
259
  </div>
260
  </div>
261
+
262
  <div id="outputSection">
263
  <video id="outputVideo" controls preload="metadata" playsinline></video>
264
+ <p id="finalSeed"></p>
265
  <div class="video-controls">
266
  <button id="btnDownloadVideo" class="video-button primary" style="display: none;">
267
  <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>
 
273
  </button>
274
  </div>
275
  </div>
276
+
277
  <div id="criticalErrorSection"></div>
278
  </div>
279
  </div>
 
291
  const PREMIUM_PAGE_ID = '1149636';
292
 
293
  async function getBrowserFingerprint() {
 
294
  const components = [navigator.userAgent, navigator.language, screen.width + 'x' + screen.height, new Date().getTimezoneOffset()];
295
  try {
296
  const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d');
 
312
  }
313
 
314
  function updateUIForSubscriptionStatus(status) {
 
315
  userSubscriptionStatus = status;
316
  const creditInfoDiv = document.getElementById('credit-info-section');
317
  const upgradeBtn = document.getElementById('upgrade-button');
 
334
  }
335
 
336
  async function checkFreeUserCredit() {
 
337
  if (!userFingerprint) return;
338
  const creditInfoDiv = document.getElementById('credit-info-section');
339
  const upgradeBtn = document.getElementById('upgrade-button');
 
363
  genBtn.disabled = true;
364
  }
365
  }
366
+
367
+ function startCountdown(resetTimestamp) {
368
  if (countdownInterval) clearInterval(countdownInterval);
369
  const creditInfoDiv = document.getElementById('credit-info-section');
370
  const updateTimer = () => {
 
383
  updateTimer();
384
  countdownInterval = setInterval(updateTimer, 60000);
385
  }
386
+
387
  window.addEventListener('message', (event) => {
 
388
  if (event.data && event.data.type === 'USER_DATA_RESPONSE') {
389
+ if (event.data.error || !event.data.payload) {
390
+ updateUIForSubscriptionStatus('free');
391
+ return;
392
+ }
393
  try {
394
  const userObject = JSON.parse(event.data.payload);
395
  const status = isUserPaid(userObject) ? 'paid' : 'free';
396
  updateUIForSubscriptionStatus(status);
397
+ } catch (e) {
398
+ updateUIForSubscriptionStatus('free');
399
+ }
400
  }
401
  });
402
+
403
+ document.getElementById('upgrade-button').addEventListener('click', () => {
404
+ parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM' }, '*');
405
+ });
406
  // === END: بخش مدیریت اعتبار ===
407
 
408
+
409
  // DOM Elements
410
+ const resultContainer = document.getElementById('result-container');
411
+ const imageFileInputStart = document.getElementById('imageFileStart');
412
+ const imagePreviewStart = document.getElementById('imagePreviewStart');
413
+ const imageDropZoneStart = document.getElementById('image-drop-zone-start');
414
+ const imageFileInputEnd = document.getElementById('imageFileEnd');
415
+ const imagePreviewEnd = document.getElementById('imagePreviewEnd');
416
+ const imageDropZoneEnd = document.getElementById('image-drop-zone-end');
417
  const promptInput = document.getElementById('prompt');
418
  const generateButton = document.getElementById('generateButton');
419
  const outputVideo = document.getElementById('outputVideo');
420
+ const finalSeedElement = document.getElementById('finalSeed');
421
  const criticalErrorSection = document.getElementById('criticalErrorSection');
422
  const statusSection = document.getElementById('statusSection');
423
  const statusMessagesDiv = document.getElementById('statusMessages');
 
427
  const outputSection = document.getElementById('outputSection');
428
  const btnRestart = document.getElementById('btnRestart');
429
  const btnDownloadVideo = document.getElementById('btnDownloadVideo');
430
+ const durationSlider = document.getElementById('durationSlider');
431
+ const durationValueDisplay = document.getElementById('durationValue');
432
 
433
+ // ====== تنظیمات مدل‌ها ======
434
  const VIDEO_SPACE_URL = "https://multimodalart-wan-2-2-first-last-frame.hf.space/";
435
+ const VIDEO_FN_INDEX = 0; // The /generate_video endpoint
436
+ const TRANSLATOR_SPACE_NAME = "hamed744/translate-tts-aloha";
437
+ const FN_INDEX_TRANSLATE_SPEAK = 1;
438
+ const DEFAULT_TTS_VOICE_TRANSLATOR = "انگلیسی (آمریکا) - جنی (زن)";
439
+
440
+ const DEFAULT_NEGATIVE_PROMPT = "色调艳丽, 过曝, 静态, 细节模糊不清, 字幕, 风格, 作品, 画作, 画面, 静止, 整体发灰, 最差质量, 低质量, JPEG压缩残留, 丑陋的, 残缺的, 多余的手指, 画得不好的手部, 画得不好的脸部, 畸形的, 毁容的, 形态畸形的肢体, 手指融合, 静止不动的画面, 杂乱的背景, 三条腿, 背景人很多, 倒着走,过曝,";
441
+ const DEFAULT_PROMPT_TEXT = "";
442
  const TRANSPARENT_PIXEL_SRC = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
443
+ const DEFAULT_VIDEO_DURATION = 2.1;
444
+
445
+ const TRANSLATION_TIMEOUT = 20000;
446
 
447
  let lastAttemptedVideoPayload = null;
448
  let eventSource = null;
 
450
 
451
  const generateSessionHash = () => Math.random().toString(36).substring(2, 13);
452
 
453
+ function updateSliderStyle(slider) {
454
+ const percentage = ((slider.value - slider.min) / (slider.max - slider.min)) * 100;
455
+ const colorStops = `var(--accent-primary), var(--accent-secondary)`;
456
+ slider.style.background = `linear-gradient(to left, ${colorStops} ${percentage}%, var(--input-bg) ${percentage}%)`;
457
+ }
458
+
459
  async function retryLastAttempt() {
460
+ if (!lastAttemptedVideoPayload || !lastAttemptedVideoPayload.startImageFile || !lastAttemptedVideoPayload.endImageFile || !lastAttemptedVideoPayload.persianPrompt) {
461
  showCriticalError("اطلاعات کافی برای تلاش مجدد وجود ندارد. لطفاً از نو شروع کنید.");
462
  return;
463
  }
 
465
  }
466
 
467
  function showCriticalError(message, isGpuQuotaError = false) {
468
+ const lowerCaseMessage = (message || "").toLowerCase();
469
+ if (isGpuQuotaError || lowerCaseMessage.includes("gpu") || lowerCaseMessage.includes("quota") || lowerCaseMessage.includes("capacity")) {
470
+ 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><strong>به مدت ۱۰ تا ۱۵ ثانیه، گوشی خود را در حالت هواپیما قرار دهید و سپس آن را غیرفعال کنید.</strong> این کار IP شما را تغییر داده و محدودیت را برطرف می‌کند.</li><li>اگر از وای فای استفاده می‌کنید، مودم خود را یک بار خاموش و روشن کنید. (توصیه می‌شود برای استفاده نامحدود از اینترنت سیم کارت استفاده کنید).</li></ul><p>خلاصه: هر بار با این خطا مواجه شدید، از اینترنت سیم‌کارت بدون فیلترشکن استفاده کرده و یک یا دو بار حالت هواپیما را روشن و خاموش کنید. سپس دکمه "تلاش مجدد" را بزنید. با این روش می‌توانید به صورت نامحدود ویدیو بسازید. ☘️</p></div><div class="video-controls"><button id="gpuErrorGoBackBtn" class="video-button">بازگشت</button><button id="gpuErrorRetryBtn" class="video-button primary">تلاش مجدد</button></div></div>`;
471
  criticalErrorSection.innerHTML = detailedGpuErrorHtml;
472
  document.getElementById('gpuErrorGoBackBtn').addEventListener('click', hideCriticalError);
473
  document.getElementById('gpuErrorRetryBtn').addEventListener('click', retryLastAttempt);
474
  } else {
475
+ criticalErrorSection.innerHTML = `<div style="text-align: center; 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>`;
476
  document.getElementById('simpleErrorGoBackBtn').addEventListener('click', hideCriticalError);
477
  document.getElementById('simpleErrorRetryBtn').addEventListener('click', retryLastAttempt);
478
  }
479
+ resultContainer.classList.add('active');
480
  criticalErrorSection.classList.add('active');
481
  statusSection.classList.remove('active');
482
  outputSection.classList.remove('active');
 
490
  }
491
 
492
  function setAiLoaderText(text) { if (loaderTextOverlay) loaderTextOverlay.textContent = text; }
493
+
494
+ function setProgressBar(percentage) {
495
+ if(loaderProgressBar) {
496
+ const p = Math.max(0, Math.min(100, percentage));
497
+ loaderProgressBar.style.width = `${p}%`;
498
+ }
499
+ }
500
 
501
  function addStatusMessage(message, type = 'info') {
502
  if (criticalErrorSection.classList.contains('active') && type === 'error') return;
 
504
  statusSection.classList.add('active');
505
  const messageDiv = document.createElement('div');
506
  messageDiv.className = `status-message ${type}`;
507
+ 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>';
508
+ messageDiv.innerHTML = `${iconSvg}<span class="status-text">${message}</span>`;
509
  statusMessagesDiv.insertBefore(messageDiv, statusMessagesDiv.firstChild);
510
  }
511
+
512
+ function clearStatusMessages() {
513
+ statusMessagesDiv.innerHTML = '';
514
+ setProgressBar(0);
515
+ aiLoader.style.display = 'none';
516
+ if (outputVideo.src && outputVideo.src.startsWith('blob:')) {
517
+ URL.revokeObjectURL(outputVideo.src);
518
+ }
519
+ outputVideo.src = '';
520
+ finalSeedElement.textContent = '';
521
+ btnDownloadVideo.style.display = 'none';
522
+ }
523
+
524
  function initializeForm() {
525
+ if (eventSource) {
526
+ eventSource.close();
527
+ eventSource = null;
528
+ }
529
  isProcessCompleted = false;
530
  hideCriticalError();
531
  resultContainer.classList.remove('active');
532
  statusSection.classList.remove('active');
533
  outputSection.classList.remove('active');
534
+ clearStatusMessages();
 
535
  lastAttemptedVideoPayload = null;
536
 
537
+ imageFileInputStart.value = '';
538
+ imagePreviewStart.src = TRANSPARENT_PIXEL_SRC;
539
+ imageDropZoneStart.classList.remove('has-image');
 
 
 
 
540
 
541
+ imageFileInputEnd.value = '';
542
+ imagePreviewEnd.src = TRANSPARENT_PIXEL_SRC;
543
+ imageDropZoneEnd.classList.remove('has-image');
544
+
545
+ promptInput.value = DEFAULT_PROMPT_TEXT;
546
  generateButton.disabled = false;
547
+ durationSlider.value = DEFAULT_VIDEO_DURATION;
548
+ durationValueDisplay.textContent = `${parseFloat(DEFAULT_VIDEO_DURATION).toFixed(1)} ثانیه`;
549
+ updateSliderStyle(durationSlider);
550
  updateUIForSubscriptionStatus(userSubscriptionStatus);
551
  }
552
 
553
+ async function translatePersianToEnglish(persianText, { timeoutMs = TRANSLATION_TIMEOUT } = {}) {
554
+ if (!persianText?.trim()) {
555
+ return Promise.resolve("");
556
+ }
557
+ const translator = await Client.connect(TRANSLATOR_SPACE_NAME);
558
+ const predictPromise = translator.predict(FN_INDEX_TRANSLATE_SPEAK, [
559
+ persianText, DEFAULT_TTS_VOICE_TRANSLATOR, 0, 0, 0
560
+ ]);
561
+ const timeoutPromise = new Promise((_, reject) =>
562
+ setTimeout(() => reject(new Error("Translation request timed out.")), timeoutMs)
563
+ );
564
+ const resp = await Promise.race([predictPromise, timeoutPromise]);
565
+ const english = resp?.data?.[0];
566
+ if (typeof english !== 'string') throw new Error("Invalid translation response.");
567
+ return english;
568
+ }
569
+
570
  async function handleSuccessfulVideoGeneration(output) {
571
  aiLoader.style.display = 'none';
572
  if (!output?.data?.[0]?.video?.path) {
573
  showCriticalError('پاسخ دریافتی از سرور ویدیو معتبر نیست.');
574
  return;
575
  }
576
+
577
+ if (userSubscriptionStatus === 'free') {
578
  try {
579
  addStatusMessage('ویدیو ساخته شد، در حال ثبت اعتبار...', 'info');
580
  await fetch('/api/use-credit', {
 
583
  body: JSON.stringify({ fingerprint: userFingerprint })
584
  });
585
  checkFreeUserCredit();
586
+ } catch (creditError) {
587
+ console.error("Critical error deducting credit:", creditError);
588
+ }
589
  }
590
+
591
  addStatusMessage('ویدیو آماده شد! 🎉', 'success');
592
+ const videoPath = output.data[0].video.path;
593
+ const videoUrl = `${VIDEO_SPACE_URL}gradio_api/file=${videoPath}`;
594
+ const usedSeed = output.data[1];
595
+
596
+ setTimeout(() => {
597
+ statusSection.classList.remove('active');
598
+ outputSection.classList.add('active');
599
+ outputVideo.src = videoUrl;
600
+ finalSeedElement.textContent = `Seed: ${usedSeed}`;
601
+ prepareCustomDownloadLink(videoUrl);
602
+ }, 300);
603
  generateButton.disabled = false;
604
  }
605
 
606
+ function prepareCustomDownloadLink(videoUrl) {
607
+ btnDownloadVideo.style.display = 'flex';
608
+ btnDownloadVideo.onclick = () => {
609
+ parent.postMessage({ type: 'DOWNLOAD_REQUEST', url: videoUrl }, '*');
610
+ };
611
+ }
612
+
613
+ // --- UI Event Handlers ---
614
  document.addEventListener('DOMContentLoaded', async () => {
615
  initializeForm();
616
  userFingerprint = await getBrowserFingerprint();
617
  parent.postMessage({ type: 'REQUEST_USER_DATA' }, '*');
618
+ updateSliderStyle(durationSlider);
619
+ });
620
+
621
+ durationSlider.addEventListener('input', () => {
622
+ const value = parseFloat(durationSlider.value).toFixed(1);
623
+ durationValueDisplay.textContent = `${value} ثانیه`;
624
+ updateSliderStyle(durationSlider);
625
  });
626
 
627
+ function setupImageInput(fileInput, dropZone, preview) {
628
  ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(e => dropZone.addEventListener(e, ev => ev.preventDefault()));
629
  ['dragenter', 'dragover'].forEach(e => dropZone.addEventListener(e, () => { if (!dropZone.classList.contains('has-image')) dropZone.classList.add('drag-over'); }));
630
  ['dragleave', 'drop'].forEach(e => dropZone.addEventListener(e, () => dropZone.classList.remove('drag-over')));
631
+ dropZone.addEventListener('drop', e => { if (e.dataTransfer.files.length > 0) { fileInput.files = e.dataTransfer.files; fileInput.dispatchEvent(new Event('change')); } });
632
+
633
+ fileInput.addEventListener('change', function(event) {
634
+ const file = event.target.files[0];
635
+ hideCriticalError();
636
+ if (file) {
637
+ if (!file.type.startsWith('image/')) {
638
+ addStatusMessage('فرمت نامعتبر', 'error');
639
+ } else {
640
+ const reader = new FileReader();
641
+ reader.onload = (e) => { preview.src = e.target.result; dropZone.classList.add('has-image'); };
642
+ reader.readAsDataURL(file);
643
+ }
644
  } else {
645
+ preview.src = TRANSPARENT_PIXEL_SRC;
646
  dropZone.classList.remove('has-image');
647
  }
648
  });
649
+ }
650
+
651
+ setupImageInput(imageFileInputStart, imageDropZoneStart, imagePreviewStart);
652
+ setupImageInput(imageFileInputEnd, imageDropZoneEnd, imagePreviewEnd);
653
+
654
  btnRestart.addEventListener('click', initializeForm);
655
+
656
 
657
  const proceedWithVideoGeneration = async () => {
658
+ const persianPrompt = promptInput.value.trim();
659
+ const startImageFile = imageFileInputStart.files[0];
660
+ const endImageFile = imageFileInputEnd.files[0];
661
+ const duration = parseFloat(durationSlider.value);
662
 
663
+ if (!persianPrompt) { addStatusMessage('لط��اً توضیحات را وارد کنید.', 'error'); return; }
664
+ if (!startImageFile) { addStatusMessage('لطفاً تصویر شروع را انتخاب کنید.', 'error'); return; }
665
+ if (!endImageFile) { addStatusMessage('لطفاً تصویر پایان را انتخاب کنید.', 'error'); return; }
666
 
667
+ clearStatusMessages();
668
  hideCriticalError();
669
  generateButton.disabled = true;
670
  resultContainer.classList.add('active');
 
674
  outputSection.classList.remove('active');
675
  addStatusMessage('در حال آماده سازی...', 'info');
676
 
677
+ lastAttemptedVideoPayload = { persianPrompt, startImageFile, endImageFile, duration };
678
  isProcessCompleted = false;
679
+
680
+ let promptForVideo = persianPrompt;
681
+ try {
682
+ setAiLoaderText("در حال ترجمه دستور شما...");
683
+ const englishPrompt = await translatePersianToEnglish(persianPrompt);
684
+ if(englishPrompt){
685
+ addStatusMessage(`ترجمه: ${englishPrompt}`, 'info');
686
+ promptForVideo = englishPrompt;
687
+ }
688
+ } catch (e) {
689
+ console.warn("Translation failed:", e.message);
690
+ addStatusMessage('ترجمه ناموفق بود. با متن فارسی ادامه می‌دهیم.', 'error');
691
+ }
692
+
693
  const sessionHash = generateSessionHash();
694
 
695
  try {
696
+ const uploadFile = async (file, name) => {
 
697
  const formData = new FormData();
698
  formData.append('files', file);
699
+ const response = await fetch(`${VIDEO_SPACE_URL}gradio_api/upload`, { method: 'POST', body: formData });
700
+ if (!response.ok) throw new Error(`خطا در آپلود ${name}: ${response.statusText}`);
701
+ return (await response.json())[0];
702
  };
703
 
704
+ setAiLoaderText("۱/۴: آپلود تصویر شروع...");
705
+ const startServerPath = await uploadFile(startImageFile, "تصویر شروع");
706
+ setProgressBar(5);
707
+
708
+ setAiLoaderText("۲/۴: آپلود تصویر پایان...");
709
+ const endServerPath = await uploadFile(endImageFile, "تصویر پایان");
710
+ setProgressBar(10);
711
+
712
 
713
+ setAiLoaderText("۳/۴: ارسال درخواست به صف...");
714
  const joinPayload = {
715
  "data": [
716
  { "path": startServerPath, "url": `${VIDEO_SPACE_URL}gradio_api/file=${startServerPath}` },
717
  { "path": endServerPath, "url": `${VIDEO_SPACE_URL}gradio_api/file=${endServerPath}` },
718
+ promptForVideo,
719
+ DEFAULT_NEGATIVE_PROMPT,
720
+ duration, 8, 1, 1, 42, true
721
+ ],
722
+ "fn_index": VIDEO_FN_INDEX, "session_hash": sessionHash
723
  };
724
  const joinResponse = await fetch(`${VIDEO_SPACE_URL}gradio_api/queue/join`, {
725
  method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(joinPayload)
726
  });
727
  if (!joinResponse.ok) throw new Error(`خطا در اتصال به صف: ${joinResponse.statusText}`);
728
 
729
+ setAiLoaderText("۴/۴: منتظر دریافت نتیجه...");
730
  eventSource = new EventSource(`${VIDEO_SPACE_URL}gradio_api/queue/data?session_hash=${sessionHash}`);
731
 
732
  eventSource.onmessage = async (event) => {
 
740
  addStatusMessage('پردازش شروع شد...');
741
  break;
742
  case 'progress':
743
+ if (data.progress_data && data.progress_data.length > 0) {
744
  const p = data.progress_data[0];
745
+ let percentage = 0;
746
+ let progressText = "در حال پردازش...";
747
+
748
+ if(p.desc && p.desc.includes("Generating")) {
749
+ percentage = 15 + (p.progress || 0) * 10; // 15% to 25%
750
+ progressText = "ساخت فریم‌ها...";
751
+ } else if (p.unit === "steps" && p.index !== null) {
752
+ percentage = 25 + ((p.index + 1) / p.length) * 65; // 25% to 90%
753
+ progressText = `ساخت ویدیو: ${Math.round(percentage)}%`;
754
+ } else if(p.desc && p.desc.includes("Encoding")) {
755
+ percentage = 90 + (p.progress || 0) * 10; // 90% to 100%
756
+ progressText = "ذخیره سازی نهایی...";
757
+ }
758
  setProgressBar(percentage);
759
+ setAiLoaderText(progressText);
760
  }
761
  break;
762
  case 'process_completed':
763
+ isProcessCompleted = true;
764
  if (data.success) {
765
  setProgressBar(100);
766
  setAiLoaderText("پردازش کامل شد!");
767
  await handleSuccessfulVideoGeneration(data.output);
768
  } else {
769
+ const errorMessage = data.output?.error || "یک خطای نامشخص رخ داد.";
770
+ const isGpuQuotaError = /exceeded your gpu quota/i.test(errorMessage);
771
+ showCriticalError(errorMessage, isGpuQuotaError);
772
  }
773
  eventSource.close();
774
+ eventSource = null;
775
  break;
776
  }
777
  };
778
 
779
+ eventSource.onerror = (error) => {
780
+ if (isProcessCompleted) {
 
781
  eventSource.close();
782
+ return;
783
  }
784
+ console.error('EventSource failed:', error);
785
+ showCriticalError("خطا در ارتباط با سرور. لطفاً اتصال اینترنت خود را بررسی کرده و مجدداً تلاش کنید.");
786
+ eventSource.close();
787
+ eventSource = null;
788
  };
789
+
790
+ } catch (errorCaught) {
791
+ showCriticalError(`خطای اولیه: ${errorCaught.message}`);
792
  generateButton.disabled = false;
793
  }
794
  };
795
+
796
+ generateButton.addEventListener('click', async () => {
797
  if (generateButton.disabled) return;
798
 
799
  if (userSubscriptionStatus === 'paid') {
 
818
  });
819
  })();
820
  </script>
821
+
822
+ <script>
823
+ // --- Header Animation Script ---
824
+ document.addEventListener('DOMContentLoaded', () => {
825
+ const canvas = document.getElementById('neural-network-canvas');
826
+ if (!canvas) return;
827
+ const header = canvas.parentElement;
828
+ const ctx = canvas.getContext('2d');
829
+ let particles = [];
830
+ const particleCount = 20; const maxDistance = 100;
831
+ const computedStyles = getComputedStyle(document.documentElement);
832
+ const particleColor = computedStyles.getPropertyValue('--accent-primary').trim();
833
+ const lineColor = computedStyles.getPropertyValue('--text-tertiary').trim();
834
+ function resizeCanvas() { canvas.width = header.clientWidth; canvas.height = header.clientHeight; init(); }
835
+ 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(); } }
836
+ function init() { particles = []; for (let i = 0; i < particleCount; i++) { particles.push(new Particle()); } }
837
+ 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; }
838
+ function animate() { ctx.clearRect(0, 0, canvas.width, canvas.height); particles.forEach(particle => { particle.update(); particle.draw(); }); connectParticles(); requestAnimationFrame(animate); }
839
+ window.addEventListener('resize', resizeCanvas); resizeCanvas(); animate();
840
+ });
841
+ </script>
842
  </body>
843
  </html>