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

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +320 -182
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 - Two Image</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 {
@@ -20,8 +20,11 @@
20
  --accent-primary-hover: #3553D6;
21
  --accent-primary-glow: rgba(74, 108, 250, 0.25);
22
  --accent-secondary: #0FD4A8;
 
 
23
  --success-color: #38A169;
24
  --danger-color: #e53e3e;
 
25
  --shadow-sm: 0 1px 2px 0 rgba(26, 32, 44, 0.03);
26
  --shadow-md: 0 4px 6px -1px rgba(26, 32, 44, 0.05), 0 2px 4px -2px rgba(26, 32, 44, 0.04);
27
  --shadow-lg: 0 10px 15px -3px rgba(26, 32, 44, 0.06), 0 4px 6px -4px rgba(26, 32, 44, 0.05);
@@ -30,22 +33,26 @@
30
  --radius-btn: 14px;
31
  --radius-input: 12px;
32
  --transition-smooth: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
 
 
 
 
33
  }
34
 
35
  @keyframes fadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
36
- @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); } }
37
  @keyframes ring-rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
38
- @keyframes background-pan { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }
 
 
39
 
40
  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; }
41
  .container { max-width: 820px; width: 100%; }
42
  header { text-align: center; margin-bottom: 2.5rem; animation: fadeIn 0.8s 0.1s ease-out backwards; }
43
  .ai-orb-container { width: 150px; height: 150px; margin: 0 auto 1rem; position: relative; display: flex; align-items: center; justify-content: center; }
44
- .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%; }
45
  .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); }
46
- .play-icon { width: 24px; height: 24px; color: var(--accent-secondary); filter: drop-shadow(0 0 8px rgba(15, 212, 168, 0.7)); }
47
- .orb-ring { position: absolute; top: 50%; left: 50%; border-style: solid; border-color: transparent; border-radius: 50%; z-index: 1; mix-blend-mode: screen; }
48
- .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; }
49
  .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; }
50
  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; }
51
  .subtitle { font-size: 1.1rem; color: var(--text-secondary); margin-top: 0.5rem; }
@@ -54,12 +61,12 @@
54
  .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; }
55
  .form-label svg { width: 24px; height: 24px; color: var(--accent-primary); }
56
  .image-inputs-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; }
57
- .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; }
58
  .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); }
59
  .image-drop-zone.has-image { border-style: solid; border-color: var(--success-color); padding: 0; cursor: default; }
60
- .upload-content { display: flex; flex-direction: column; align-items: center; gap: 1rem; }
61
- .upload-icon svg { width: 48px; height: 48px; color: var(--accent-primary); stroke-width: 1.5; opacity: 0.8; }
62
- .image-drop-zone p { margin: 0; color: var(--text-secondary); font-weight: 500; font-size: 0.9rem; }
63
  .image-preview { display: none; width: 100%; height: 100%; object-fit: cover; position: absolute; top: 0; left: 0; }
64
  .image-drop-zone.has-image .upload-content { display: none; }
65
  .image-drop-zone.has-image .image-preview { display: block; }
@@ -72,26 +79,27 @@
72
  #result-container.active { border-style: solid; border-color: var(--panel-border); }
73
  #statusSection, #outputSection, #criticalErrorSection { display: none; width: 100%; }
74
  #statusSection.active, #outputSection.active, #criticalErrorSection.active { display: block; animation: fadeIn 0.5s; }
75
- #statusMessages { max-height: 150px; overflow-y: auto; width: 100%; padding: 0.5rem; margin-bottom: 1rem; }
76
  .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); }
77
- .status-message.success { border-left: 4px solid var(--success-color); color: var(--success-color); }
78
  .status-message.error { border-left: 4px solid var(--danger-color); color: var(--danger-color); }
79
  .status-icon { width: 18px; height: 18px; }
80
- #aiLoader { display: none; align-items: center; justify-content: center; }
81
- .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; color: #f0f6fc; }
82
- .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;}
83
  .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; }
84
- #outputVideo { width: 100%; max-width: 500px; border-radius: var(--radius-input); margin: 1rem auto; display: block; background-color: #000; box-shadow: var(--shadow-md); }
85
- #finalSeed { text-align: center; color: var(--text-tertiary); font-size: 0.85rem; margin-top: 1rem; }
86
  .video-controls { display: flex; justify-content: center; gap: 1rem; margin-top: 1.5rem; }
87
  .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; }
88
  .video-button.primary { background-color: var(--accent-primary); color: white; }
89
- .video-button.primary:hover { background-color: var(--accent-primary-hover); transform: translateY(-2px); }
90
- .gpu-error-message-container { text-align: right; background-color: #FFFBEB; padding: 25px; border-radius: 12px; box-shadow: var(--shadow-lg); border: 2px solid #FBBF24; }
91
- .gpu-error-message-container h2 { font-size: 1.2em; color: #B45309; margin-bottom: 15px; font-weight: 700; display: flex; align-items: center; border-bottom: 1px solid #FBBF24; padding-bottom: 12px; }
92
- .gpu-error-message-container .error-content { font-size: 0.9rem; color: #4A5568; line-height: 1.7; }
93
- .gpu-error-message-container .error-content ul { padding-right: 20px; }
94
- @media (max-width: 768px) { main { padding: 1.5rem; } h1 { font-size: 2.2rem; } .image-inputs-grid { grid-template-columns: 1fr; } .generator-container { height: 250px; } .text-overlay { font-size: 18px; } }
 
 
 
 
95
  </style>
96
  </head>
97
  <body>
@@ -99,106 +107,223 @@
99
  <header>
100
  <div class="header-content">
101
  <div class="ai-orb-container">
102
- <div class="orb-background"></div>
103
  <div class="orb-core"><svg class="play-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"></path></svg></div>
104
- <div class="orb-ring one"></div>
105
- <div class="orb-ring two"></div>
106
  </div>
107
- <h1>استودیو ویدیو: دو تصویر به ویدیو</h1>
108
- <p class="subtitle">فریم شروع و پایان را انتخاب کنید تا هوش مصنوعی بین آن‌ها را متحرک کند</p>
 
109
  </div>
110
  </header>
111
 
112
  <main>
113
  <div class="form-group">
114
  <div class="form-label">
115
- <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>
116
- ۱. تصاویر خود را انتخاب کنید
117
  </div>
118
  <div class="image-inputs-grid">
119
- <label id="image-drop-zone-start" class="image-drop-zone" for="imageFileStart">
120
  <div class="upload-content">
121
- <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>
122
- <p>تصویر شروع (Start Frame)</p>
 
123
  </div>
124
- <img id="imagePreviewStart" class="image-preview" src="" alt="Preview Start">
125
  </label>
126
- <input type="file" id="imageFileStart" accept="image/*" hidden>
127
-
128
- <label id="image-drop-zone-end" class="image-drop-zone" for="imageFileEnd">
129
  <div class="upload-content">
130
- <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>
131
- <p>تصویر پایان (End Frame)</p>
 
132
  </div>
133
- <img id="imagePreviewEnd" class="image-preview" src="" alt="Preview End">
134
  </label>
135
- <input type="file" id="imageFileEnd" accept="image/*" hidden>
136
  </div>
137
  </div>
138
 
139
  <div class="form-group">
140
  <label for="prompt" class="form-label">
141
- <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>
142
- ۲. دستور ساخت ویدیو ه انگلیسی)
143
  </label>
144
- <textarea id="prompt" rows="3" placeholder="مثال: the man turns around (مرد میچرخد)"></textarea>
 
 
 
145
  <button id="generateButton">
146
- <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>
147
  <span>ساخت ویدیو جادویی</span>
148
  </button>
149
  </div>
150
 
151
  <div class="form-group">
152
  <div class="form-label">
153
- <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>
154
  ۳. نتیجه را ببینید
155
  </div>
156
  <div id="result-container">
157
  <div id="statusSection">
158
  <div id="statusMessages"></div>
159
- <div id="aiLoader" style="display: none;">
160
  <div class="generator-container">
161
  <div class="text-overlay">در حال پردازش ویدیو...</div>
162
  <div class="progress-bar"></div>
163
  </div>
164
  </div>
165
  </div>
166
-
167
  <div id="outputSection">
168
  <video id="outputVideo" controls preload="metadata" playsinline></video>
169
- <p id="finalSeed"></p>
170
  <div class="video-controls">
171
- <a id="btnDownloadVideo" class="video-button primary" style="display: none;" download="generated_video.mp4">
172
  <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>
173
  دانلود ویدیو
174
- </a>
175
  <button id="btnRestart" class="video-button">
176
  <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>
177
  ساخت ویدیو جدید
178
  </button>
179
  </div>
180
  </div>
181
-
182
  <div id="criticalErrorSection"></div>
183
  </div>
184
  </div>
185
  </main>
186
  </div>
187
 
188
- <script>
 
 
189
  (function() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  // DOM Elements
191
- const resultContainer = document.getElementById('result-container');
192
- const imageFileInputStart = document.getElementById('imageFileStart');
193
- const imagePreviewStart = document.getElementById('imagePreviewStart');
194
- const imageDropZoneStart = document.getElementById('image-drop-zone-start');
195
- const imageFileInputEnd = document.getElementById('imageFileEnd');
196
- const imagePreviewEnd = document.getElementById('imagePreviewEnd');
197
- const imageDropZoneEnd = document.getElementById('image-drop-zone-end');
198
  const promptInput = document.getElementById('prompt');
199
  const generateButton = document.getElementById('generateButton');
200
  const outputVideo = document.getElementById('outputVideo');
201
- const finalSeedElement = document.getElementById('finalSeed');
202
  const criticalErrorSection = document.getElementById('criticalErrorSection');
203
  const statusSection = document.getElementById('statusSection');
204
  const statusMessagesDiv = document.getElementById('statusMessages');
@@ -208,27 +333,38 @@
208
  const outputSection = document.getElementById('outputSection');
209
  const btnRestart = document.getElementById('btnRestart');
210
  const btnDownloadVideo = document.getElementById('btnDownloadVideo');
 
211
 
 
212
  const VIDEO_SPACE_URL = "https://multimodalart-wan-2-2-first-last-frame.hf.space/";
213
- const DEFAULT_NEGATIVE_PROMPT = "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走,过曝,";
214
  const TRANSPARENT_PIXEL_SRC = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
215
 
 
216
  let eventSource = null;
217
  let isProcessCompleted = false;
218
- let lastAttemptedPayload = null;
219
 
220
  const generateSessionHash = () => Math.random().toString(36).substring(2, 13);
221
 
222
- function showCriticalError(message) {
223
- const lowerCaseMessage = (message || "").toLowerCase();
224
- if (lowerCaseMessage.includes("gpu quota")) {
225
- criticalErrorSection.innerHTML = `<div class="gpu-error-message-container"><h2><span class="icon">💡</span> راهنمای رفع محدودیت ویدیو</h2><div class="error-content"><p><strong>ارور محدودیت استفاده از GPU (کارت گرافیک) رخ داده است.</strong></p><p>این خطا به دلیل محدودیت‌های موقتی در سرویس‌دهنده ایجاد می‌شود. برای رفع آن:</p><ul><li>اگر از اینترنت سیم‌کارت استفاده می‌کنید، فیلترشکن خود را خاموش کرده و <strong>گوشی را برای ۱۰ ثانیه در حالت هواپیما قرار دهید</strong> و سپس غیرفعال کنید.</li><li>اگر از وای فای استفاده می‌کنید، مودم خود را یک بار خاموش و روشن کنید.</li></ul><p>سپس دکمه "تلاش مجدد" را بزنید. این کار IP شما را تغییر داده و معمولاً محدودیت را برطرف می‌کند.</p></div><div class="video-controls"><button id="gpuErrorRetryBtn" class="video-button primary">تلاش مجدد</button></div></div>`;
 
 
 
 
 
 
 
 
 
226
  document.getElementById('gpuErrorRetryBtn').addEventListener('click', retryLastAttempt);
227
  } else {
228
- 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="simpleErrorRetryBtn" class="video-button primary">تلاش مجدد</button></div>`;
 
229
  document.getElementById('simpleErrorRetryBtn').addEventListener('click', retryLastAttempt);
230
  }
231
- resultContainer.classList.add('active');
232
  criticalErrorSection.classList.add('active');
233
  statusSection.classList.remove('active');
234
  outputSection.classList.remove('active');
@@ -236,120 +372,113 @@
236
  generateButton.disabled = false;
237
  }
238
 
239
- function retryLastAttempt() {
240
- if (!lastAttemptedPayload) {
241
- alert("اطلاعات کافی برای تلاش مجدد وجود ندارد. لطفاً از نو شروع کنید.");
242
- return;
243
- }
244
- proceedWithVideoGeneration(lastAttemptedPayload.startFile, lastAttemptedPayload.endFile, lastAttemptedPayload.prompt);
245
- }
246
-
247
  function hideCriticalError() {
248
  criticalErrorSection.classList.remove('active');
249
  criticalErrorSection.innerHTML = '';
250
  }
251
 
252
  function setAiLoaderText(text) { if (loaderTextOverlay) loaderTextOverlay.textContent = text; }
253
- function setProgressBar(percentage) { if(loaderProgressBar) loaderProgressBar.style.width = `${Math.max(0, Math.min(100, percentage))}%`; }
254
 
255
  function addStatusMessage(message, type = 'info') {
256
- if (criticalErrorSection.classList.contains('active') && type === 'error') return;
257
  resultContainer.classList.add('active');
258
  statusSection.classList.add('active');
259
  const messageDiv = document.createElement('div');
260
  messageDiv.className = `status-message ${type}`;
261
- 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>';
262
- messageDiv.innerHTML = `${iconSvg}<span class="status-text">${message}</span>`;
263
  statusMessagesDiv.insertBefore(messageDiv, statusMessagesDiv.firstChild);
264
  }
265
-
266
- function clearStatusMessages() {
267
- statusMessagesDiv.innerHTML = '';
268
- setProgressBar(0);
269
- aiLoader.style.display = 'none';
270
- if (outputVideo.src && !outputVideo.src.startsWith('data:')) { URL.revokeObjectURL(outputVideo.src); }
271
- outputVideo.src = '';
272
- finalSeedElement.textContent = '';
273
- btnDownloadVideo.style.display = 'none';
274
- btnDownloadVideo.href = '#';
275
- }
276
-
277
  function initializeForm() {
278
- if (eventSource) eventSource.close();
279
  isProcessCompleted = false;
280
- lastAttemptedPayload = null;
281
  hideCriticalError();
282
  resultContainer.classList.remove('active');
283
  statusSection.classList.remove('active');
284
  outputSection.classList.remove('active');
285
- clearStatusMessages();
 
 
286
 
287
- imageFileInputStart.value = '';
288
- imagePreviewStart.src = TRANSPARENT_PIXEL_SRC;
289
- imageDropZoneStart.classList.remove('has-image');
290
 
291
- imageFileInputEnd.value = '';
292
- imagePreviewEnd.src = TRANSPARENT_PIXEL_SRC;
293
- imageDropZoneEnd.classList.remove('has-image');
294
 
295
  promptInput.value = "";
296
  generateButton.disabled = false;
 
297
  }
298
 
299
- const uploadFile = async (file) => {
300
- const formData = new FormData();
301
- formData.append('files', file);
302
- const response = await fetch(`${VIDEO_SPACE_URL}gradio_api/upload`, { method: 'POST', body: formData });
303
- if (!response.ok) throw new Error(`خطا در آپلود فایل: ${file.name}`);
304
- const uploadedFiles = await response.json();
305
- return uploadedFiles[0];
306
- };
307
-
308
- function handleSuccessfulVideoGeneration(output) {
309
  aiLoader.style.display = 'none';
310
  if (!output?.data?.[0]?.video?.path) {
311
  showCriticalError('پاسخ دریافتی از سرور ویدیو معتبر نیست.');
312
  return;
 
 
 
 
 
 
 
 
 
 
 
313
  }
314
  addStatusMessage('ویدیو آماده شد! 🎉', 'success');
315
- const videoPath = output.data[0].video.path;
316
- const videoUrl = `${VIDEO_SPACE_URL}gradio_api/file=${videoPath}`;
317
- const usedSeed = output.data[1];
318
-
319
- setTimeout(() => {
320
- statusSection.classList.remove('active');
321
- outputSection.classList.add('active');
322
- outputVideo.src = videoUrl;
323
- finalSeedElement.textContent = `Seed: ${usedSeed}`;
324
- btnDownloadVideo.href = videoUrl;
325
- btnDownloadVideo.style.display = 'flex';
326
- }, 300);
327
  generateButton.disabled = false;
328
  }
329
 
330
- document.addEventListener('DOMContentLoaded', initializeForm);
331
- btnRestart.addEventListener('click', initializeForm);
 
 
 
332
 
333
- function setupImageDropZone(dropZone, fileInput, previewEl) {
334
  ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(e => dropZone.addEventListener(e, ev => ev.preventDefault()));
335
  ['dragenter', 'dragover'].forEach(e => dropZone.addEventListener(e, () => { if (!dropZone.classList.contains('has-image')) dropZone.classList.add('drag-over'); }));
336
  ['dragleave', 'drop'].forEach(e => dropZone.addEventListener(e, () => dropZone.classList.remove('drag-over')));
337
- dropZone.addEventListener('drop', e => { if (e.dataTransfer.files.length > 0) { fileInput.files = e.dataTransfer.files; fileInput.dispatchEvent(new Event('change')); } });
338
- fileInput.addEventListener('change', (event) => {
339
- const file = event.target.files[0];
340
- if (file) {
341
  const reader = new FileReader();
342
  reader.onload = (e) => { previewEl.src = e.target.result; dropZone.classList.add('has-image'); };
343
  reader.readAsDataURL(file);
 
 
 
344
  }
345
  });
346
- }
347
- setupImageDropZone(imageDropZoneStart, imageFileInputStart, imagePreviewStart);
348
- setupImageDropZone(imageDropZoneEnd, imageFileInputEnd, imagePreviewEnd);
 
 
349
 
 
 
 
 
350
 
351
- async function proceedWithVideoGeneration(startImageFile, endImageFile, prompt) {
352
- clearStatusMessages();
 
 
353
  hideCriticalError();
354
  generateButton.disabled = true;
355
  resultContainer.classList.add('active');
@@ -357,25 +486,35 @@
357
  aiLoader.style.display = 'flex';
358
  setProgressBar(0);
359
  outputSection.classList.remove('active');
 
 
 
360
  isProcessCompleted = false;
 
361
  const sessionHash = generateSessionHash();
362
 
363
  try {
364
- addStatusMessage('۱/۴: در حال آپلود تصویر شروع...', 'info');
365
  setAiLoaderText("آپلود تصویر شروع...");
366
- const startServerPath = await uploadFile(startImageFile);
 
 
 
 
 
 
367
 
368
- addStatusMessage('۲/۴: در حال آپلود تصویر پایان...', 'info');
 
369
  setAiLoaderText("آپلود تصویر پایان...");
370
  const endServerPath = await uploadFile(endImageFile);
 
371
 
372
- addStatusMessage('۳/۴: ارسال درخواست به صف...', 'info');
373
- setAiLoaderText("ارسال درخواست...");
374
  const joinPayload = {
375
  "data": [
376
  { "path": startServerPath, "url": `${VIDEO_SPACE_URL}gradio_api/file=${startServerPath}` },
377
  { "path": endServerPath, "url": `${VIDEO_SPACE_URL}gradio_api/file=${endServerPath}` },
378
- prompt, DEFAULT_NEGATIVE_PROMPT, 2.1, 8, 1, 1, 0, true
379
  ], "fn_index": 0, "session_hash": sessionHash
380
  };
381
  const joinResponse = await fetch(`${VIDEO_SPACE_URL}gradio_api/queue/join`, {
@@ -383,35 +522,25 @@
383
  });
384
  if (!joinResponse.ok) throw new Error(`خطا در اتصال به صف: ${joinResponse.statusText}`);
385
 
386
- addStatusMessage('۴/۴: منتظر دریافت نتیجه...', 'info');
387
  setAiLoaderText("منتظر دریافت نتیجه...");
388
  eventSource = new EventSource(`${VIDEO_SPACE_URL}gradio_api/queue/data?session_hash=${sessionHash}`);
389
 
390
- eventSource.onmessage = (event) => {
391
  const data = JSON.parse(event.data);
392
- console.log("Received data:", data);
393
-
394
  switch (data.msg) {
395
  case 'estimation':
396
  addStatusMessage(`در صف انتظار... زمان تخمینی: ${Math.round(data.rank_eta)} ثانیه`);
397
  break;
398
  case 'process_starts':
399
- addStatusMessage('پردازش در سرور شروع شد...');
400
  break;
401
  case 'progress':
402
- if (data.progress_data && data.progress_data.length > 0) {
403
  const p = data.progress_data[0];
404
- let percentage = 0;
405
- let text = "در حال ساخت ویدیو...";
406
-
407
- if (p.progress !== null) {
408
- percentage = p.progress * 100;
409
- } else if (p.index !== null && p.length !== null) {
410
- percentage = ((p.index + 1) / p.length) * 100;
411
- text = `مرحله ${p.index + 1} از ${p.length}`;
412
- }
413
  setProgressBar(percentage);
414
- setAiLoaderText(`${text} ${Math.round(percentage)}%`);
415
  }
416
  break;
417
  case 'process_completed':
@@ -419,41 +548,50 @@
419
  if (data.success) {
420
  setProgressBar(100);
421
  setAiLoaderText("پردازش کامل شد!");
422
- handleSuccessfulVideoGeneration(data.output);
423
  } else {
424
- const errorMessage = data.output?.error || "یک خطای نامشخص رخ داد.";
425
- showCriticalError(errorMessage);
426
  }
427
- if(eventSource) eventSource.close();
428
  break;
429
  }
430
  };
431
 
432
- eventSource.onerror = (error) => {
433
- if (isProcessCompleted) return;
434
- showCriticalError("خطا در ارتباط با سرور. اتصال اینترنت خود را بررسی کنید.");
435
- if(eventSource) eventSource.close();
 
436
  };
437
-
438
  } catch (error) {
439
- showCriticalError(error.message);
440
  generateButton.disabled = false;
441
  }
442
- }
443
-
444
- generateButton.addEventListener('click', () => {
445
- const startFile = imageFileInputStart.files[0];
446
- const endFile = imageFileInputEnd.files[0];
447
- const prompt = promptInput.value.trim();
448
 
449
- if (!startFile) { alert('لطفاً تصویر شروع را انتخاب کنید.'); return; }
450
- if (!endFile) { alert('لطفاً تصویر پایان را انتخاب کنید.'); return; }
451
- if (!prompt) { alert('لطفاً دستور ساخت ویدیو را بنویسید.'); return; }
452
-
453
- lastAttemptedPayload = { startFile, endFile, prompt };
454
- proceedWithVideoGeneration(startFile, endFile, prompt);
 
 
 
 
 
 
 
 
 
 
 
 
 
455
  });
456
-
457
  })();
458
  </script>
459
  </body>
 
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 {
 
20
  --accent-primary-hover: #3553D6;
21
  --accent-primary-glow: rgba(74, 108, 250, 0.25);
22
  --accent-secondary: #0FD4A8;
23
+ --accent-premium: #FFC107;
24
+ --accent-premium-glow: rgba(255, 193, 7, 0.3);
25
  --success-color: #38A169;
26
  --danger-color: #e53e3e;
27
+ --danger-color-hover: #c53030;
28
  --shadow-sm: 0 1px 2px 0 rgba(26, 32, 44, 0.03);
29
  --shadow-md: 0 4px 6px -1px rgba(26, 32, 44, 0.05), 0 2px 4px -2px rgba(26, 32, 44, 0.04);
30
  --shadow-lg: 0 10px 15px -3px rgba(26, 32, 44, 0.06), 0 4px 6px -4px rgba(26, 32, 44, 0.05);
 
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; }
 
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; }
 
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>
 
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>
118
 
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>
183
  دانلود ویدیو
184
+ </button>
185
  <button id="btnRestart" class="video-button">
186
  <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>
187
  ساخت ویدیو جدید
188
  </button>
189
  </div>
190
  </div>
 
191
  <div id="criticalErrorSection"></div>
192
  </div>
193
  </div>
194
  </main>
195
  </div>
196
 
197
+ <script type="module">
198
+ import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
199
+
200
  (function() {
201
+ // === START: بخش مدیریت اعتبار و ارتباط با والد ===
202
+ let userSubscriptionStatus = 'free'; // 'free' or 'paid'
203
+ let userFingerprint = null;
204
+ let countdownInterval = null;
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');
212
+ ctx.fillText("a1b2c3d4e5f6g7h8i9j0", 2, 15);
213
+ components.push(canvas.toDataURL());
214
+ } catch (e) { components.push("canvas-error"); }
215
+ const fingerprintString = components.join('---');
216
+ let hash = 0;
217
+ for (let i = 0; i < fingerprintString.length; i++) {
218
+ hash = ((hash << 5) - hash) + fingerprintString.charCodeAt(i);
219
+ hash |= 0;
220
+ }
221
+ return 'fp_' + Math.abs(hash).toString(16);
222
+ }
223
+
224
+ function isUserPaid(userObject) {
225
+ return userObject && userObject.isLogin && userObject.accessible_pages &&
226
+ (userObject.accessible_pages.includes(PREMIUM_PAGE_ID) || userObject.accessible_pages.includes(parseInt(PREMIUM_PAGE_ID)));
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');
234
+ const genBtn = document.getElementById('generateButton');
235
+ const subscriptionBadge = document.getElementById('subscription-status-badge');
236
+
237
+ if (status === 'paid') {
238
+ subscriptionBadge.textContent = 'نسخه نامحدود';
239
+ subscriptionBadge.className = 'paid-badge';
240
+ creditInfoDiv.style.display = 'none';
241
+ upgradeBtn.style.display = 'none';
242
+ genBtn.disabled = false;
243
+ if(countdownInterval) clearInterval(countdownInterval);
244
+ } else {
245
+ subscriptionBadge.textContent = 'نسخه رایگان';
246
+ subscriptionBadge.className = 'free-badge';
247
+ checkFreeUserCredit();
248
+ }
249
+ subscriptionBadge.style.display = 'inline-block';
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');
257
+ const genBtn = document.getElementById('generateButton');
258
+
259
+ try {
260
+ const response = await fetch('/api/check-credit', {
261
+ method: 'POST',
262
+ headers: {'Content-Type': 'application/json'},
263
+ body: JSON.stringify({ fingerprint: userFingerprint })
264
+ });
265
+ const result = await response.json();
266
+
267
+ creditInfoDiv.style.display = 'block';
268
+ if (result.limit_reached) {
269
+ genBtn.disabled = true;
270
+ upgradeBtn.style.display = 'block';
271
+ startCountdown(result.reset_timestamp);
272
+ } else {
273
+ genBtn.disabled = false;
274
+ upgradeBtn.style.display = 'none';
275
+ if(countdownInterval) clearInterval(countdownInterval);
276
+ creditInfoDiv.textContent = `شما ${result.credits_remaining} اعتبار ساخت ویدیوی رایگان در این هفته دارید.`;
277
+ }
278
+ } catch (error) {
279
+ creditInfoDiv.textContent = "خطا در بررسی اعتبار.";
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 = () => {
288
+ const timeLeft = Math.max(0, resetTimestamp - (Date.now() / 1000));
289
+ if (timeLeft === 0) {
290
+ clearInterval(countdownInterval);
291
+ creditInfoDiv.textContent = 'در حال بروزرسانی اعتبار...';
292
+ setTimeout(checkFreeUserCredit, 2000);
293
+ return;
294
+ }
295
+ const d = Math.floor(timeLeft / 86400);
296
+ const h = Math.floor((timeLeft % 86400) / 3600);
297
+ const m = Math.floor((timeLeft % 3600) / 60);
298
+ creditInfoDiv.textContent = `اعتبار شما تمام شده. زمان باقی‌مانده: ${d} روز و ${h} ساعت و ${m} دقیقه`;
299
+ };
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
  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;
345
  let isProcessCompleted = false;
 
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
+ }
354
+ generateButton.click();
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');
 
372
  generateButton.disabled = false;
373
  }
374
 
 
 
 
 
 
 
 
 
375
  function hideCriticalError() {
376
  criticalErrorSection.classList.remove('active');
377
  criticalErrorSection.innerHTML = '';
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;
385
  resultContainer.classList.add('active');
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', {
428
+ method: 'POST',
429
+ headers: {'Content-Type': 'application/json'},
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');
 
486
  aiLoader.style.display = 'flex';
487
  setProgressBar(0);
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`, {
 
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) => {
529
  const data = JSON.parse(event.data);
530
+
 
531
  switch (data.msg) {
532
  case 'estimation':
533
  addStatusMessage(`در صف انتظار... زمان تخمینی: ${Math.round(data.rank_eta)} ثانیه`);
534
  break;
535
  case 'process_starts':
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':
 
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') {
576
+ await proceedWithVideoGeneration();
577
+ } else {
578
+ try {
579
+ const response = await fetch('/api/check-credit', {
580
+ method: 'POST',
581
+ headers: {'Content-Type': 'application/json'},
582
+ body: JSON.stringify({ fingerprint: userFingerprint })
583
+ });
584
+ const result = await response.json();
585
+ if(result.limit_reached) {
586
+ updateUIForSubscriptionStatus('free');
587
+ return;
588
+ }
589
+ await proceedWithVideoGeneration();
590
+ } catch (err) {
591
+ showCriticalError(`خطا در سیستم اعتبار: ${err.message}`);
592
+ }
593
+ }
594
  });
 
595
  })();
596
  </script>
597
  </body>