Opera8 commited on
Commit
131ad61
·
verified ·
1 Parent(s): 6e3cd60

Update templates/audio.html

Browse files
Files changed (1) hide show
  1. templates/audio.html +362 -406
templates/audio.html CHANGED
@@ -3,458 +3,414 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>استودیو خلق آواتار هوشمند - LongCat 1.5</title>
7
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.003/Vazirmatn-font-face.css">
8
- <script src="https://cdn.tailwindcss.com"></script>
9
  <style>
10
  :root {
11
  --app-font: 'Vazirmatn', sans-serif;
12
- --accent-primary: #FF6B35;
13
- --accent-primary-hover: #E85A28;
14
- --accent-primary-glow: rgba(255, 107, 53, 0.25);
 
 
 
 
 
 
 
 
15
  --accent-secondary: #0FD4A8;
 
 
 
 
 
 
 
 
 
 
16
  }
 
 
17
  body {
18
  font-family: var(--app-font);
19
- background-color: #0B0E14;
20
- color: #F1F5F9;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  }
22
- @keyframes ring-rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
23
- @keyframes pulse-loader { 0% { box-shadow: 0 0 40px var(--accent-primary-glow); } 50% { box-shadow: 0 0 60px rgba(255, 107, 53, 0.5); } 100% { box-shadow: 0 0 40px var(--accent-primary-glow); } }
24
- @keyframes glow-text { 0%, 100% { opacity: 0.7; } 50% { opacity: 1; } }
25
- .orb-ring { animation: ring-rotate 10s linear infinite; }
26
- .orb-ring-reverse { animation: ring-rotate 12s linear infinite reverse; }
27
- .generator-container { animation: pulse-loader 5s infinite ease-in-out; }
28
- .text-overlay { animation: glow-text 4s infinite ease-in-out; }
29
  </style>
30
  </head>
31
- <body class="min-h-screen py-10 px-4">
32
- <div class="max-w-4xl mx-auto flex flex-col gap-6">
33
-
34
- <!-- Header -->
35
- <header class="relative text-center py-6 border-b border-gray-800/80">
36
- <div class="w-24 h-24 mx-auto mb-4 relative flex items-center justify-content-center">
37
- <div class="absolute inset-0 bg-orange-500/10 rounded-full blur-xl"></div>
38
- <div class="w-12 h-12 bg-gradient-to-tr from-orange-500 to-amber-400 rounded-full flex items-center justify-center relative z-10 shadow-lg shadow-orange-500/20">
39
- <span class="text-2xl">🐱</span>
40
- </div>
41
- <div class="absolute inset-0 border border-dashed border-orange-500/30 rounded-full orb-ring"></div>
42
- <div class="absolute -inset-2 border border-dotted border-amber-400/20 rounded-full orb-ring-reverse"></div>
43
- </div>
44
- <h1 class="text-3xl font-extrabold bg-gradient-to-l from-orange-500 to-amber-400 bg-clip-text text-transparent mb-2">استودیو ساخت آواتار ویدیویی سخنگو</h1>
45
- <p class="text-gray-400 text-sm">تولید نامحدود ویدیو! صوت طولانی خود را آپلود کنید تا سیستم آن را به صورت فریم به فریم و تکه‌تکه تولید و به هم متصل کند.</p>
46
- </header>
47
-
48
- <!-- Tracking System Block -->
49
- <div class="bg-gray-900/40 border border-gray-800 rounded-3xl p-6 backdrop-blur-xl">
50
- <h2 class="text-lg font-bold text-gray-200 mb-3 flex items-center gap-2">
51
- <span>🔍</span> پیگیری وضعیت درخواست با کد
52
- </h2>
53
- <div class="flex flex-col sm:flex-row gap-3">
54
- <input type="text" id="track-code-input" class="flex-1 bg-gray-950 border border-gray-800 focus:border-orange-500 rounded-xl p-3 text-sm text-gray-300 outline-none" placeholder="کد پیگیری درخواست خود را وارد کنید (مثال: avatar8e75...)" />
55
- <button type="button" id="btn-track-submit" class="bg-orange-600 hover:bg-orange-500 text-white font-bold py-3 px-6 rounded-xl text-sm transition-all duration-300">
56
- بررسی وضعیت
57
- </button>
58
- </div>
59
  </div>
60
 
61
- <!-- Main Form & Dashboard -->
62
- <main class="bg-gray-900/60 border border-gray-800 rounded-3xl p-6 md:p-8 backdrop-blur-xl shadow-2xl">
63
- <form id="avatar-form" class="space-y-6">
64
-
65
- <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
66
- <div>
67
- <label class="block text-sm font-bold text-gray-300 mb-3 flex items-center gap-2"><span>🖼️</span> تصویر کاراکتر مرجع</label>
68
- <div id="image-dropzone" class="border-2 border-dashed border-gray-700 hover:border-orange-500/50 bg-gray-950/40 rounded-2xl p-6 text-center cursor-pointer transition-all duration-300 min-h-[180px] flex flex-col items-center justify-center relative overflow-hidden">
69
- <input type="file" id="image-input" accept="image/*" class="hidden" />
70
- <div id="image-upload-prompt" class="space-y-2">
71
- <span class="text-4xl">📁</span>
72
- <p class="text-sm text-gray-400 font-semibold">کلیک کنید یا تصویر را به این قسمت بکشید</p>
73
- </div>
74
- <img id="image-preview" class="hidden absolute inset-0 w-full h-full object-contain p-2" />
75
- <button type="button" id="remove-image" class="hidden absolute top-3 right-3 bg-red-600 hover:bg-red-700 text-white rounded-full w-8 h-8 flex items-center justify-center font-bold z-10">&times;</button>
76
- </div>
77
- </div>
78
-
79
- <div>
80
- <label class="block text-sm font-bold text-gray-300 mb-3 flex items-center gap-2"><span>🎙️</span> فایل صوتی سخنرانی (هرچقدر طولانی!)</label>
81
- <div id="audio-dropzone" class="border-2 border-dashed border-gray-700 hover:border-orange-500/50 bg-gray-950/40 rounded-2xl p-6 text-center cursor-pointer transition-all duration-300 min-h-[180px] flex flex-col items-center justify-center relative overflow-hidden">
82
- <input type="file" id="audio-input" accept="audio/*" class="hidden" />
83
- <div id="audio-upload-prompt" class="space-y-2">
84
- <span class="text-4xl">🎵</span>
85
- <p class="text-sm text-gray-400 font-semibold">کلیک کنید یا فایل صوتی را به این قسمت بکشید</p>
86
- </div>
87
- <div id="audio-player-wrapper" class="hidden w-full px-4 space-y-2">
88
- <span class="text-3xl">🔊</span>
89
- <p id="audio-name" class="text-xs text-gray-400 font-semibold truncate"></p>
90
- <audio id="audio-preview" controls class="w-full h-8 mt-2"></audio>
91
- </div>
92
- <button type="button" id="remove-audio" class="hidden absolute top-3 right-3 bg-red-600 hover:bg-red-700 text-white rounded-full w-8 h-8 flex items-center justify-center font-bold z-10">&times;</button>
93
- </div>
94
- </div>
95
- </div>
96
-
97
- <div>
98
- <label for="prompt-input" class="block text-sm font-bold text-gray-300 mb-2 flex items-center gap-2"><span>✏️</span> توصیف حرکت و رفتار کاراکتر (به انگلیسی)</label>
99
- <textarea id="prompt-input" rows="3" class="w-full bg-gray-950/60 border border-gray-800 focus:border-orange-500 rounded-xl p-3 text-sm focus:ring-2 focus:ring-orange-500/20 outline-none text-gray-100 resize-none">A person is speaking expressively, looking at the camera.</textarea>
100
- </div>
101
 
102
- <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
103
- <div>
104
- <label class="block text-sm font-semibold text-gray-400 mb-2">کیفیت ویدیو</label>
105
- <select id="resolution-input" class="w-full bg-gray-950 border border-gray-800 rounded-xl p-3 text-sm text-gray-300 outline-none"><option value="480p" selected>480p</option><option value="720p">720p</option></select>
106
- </div>
107
- <div>
108
- <label class="block text-sm font-semibold text-gray-400 mb-2">هسته پردازش (Seed)</label>
109
- <input type="number" id="seed-input" value="42" class="w-full bg-gray-950 border border-gray-800 rounded-xl p-3 text-sm text-gray-300 outline-none" />
110
- </div>
111
- <div>
112
- <label class="block text-sm font-semibold text-gray-400 mb-2">پیش‌پردازش صدا</label>
113
- <select id="vocal-mode-input" class="w-full bg-gray-950 border border-gray-800 rounded-xl p-3 text-sm text-gray-300 outline-none"><option value="Clean speech (fast)" selected>Clean speech (fast)</option><option value="Isolate vocals (quality)">Isolate vocals (quality)</option></select>
114
  </div>
115
  </div>
116
-
117
- <div>
118
- <label class="block text-sm font-semibold text-gray-400 mb-2">سیستم شتاب‌دهنده</label>
119
- <select id="acceleration-input" class="w-full bg-gray-950 border border-gray-800 rounded-xl p-3 text-sm text-gray-300 outline-none"><option value="DBCache faster" selected>DBCache faster</option><option value="DBCache fast">DBCache fast</option><option value="Exact 8-step">Exact 8-step</option></select>
 
 
 
 
 
 
 
 
 
120
  </div>
121
-
122
- <button type="submit" id="submit-btn" class="w-full py-4 px-6 bg-gradient-to-l from-orange-600 to-amber-500 hover:from-orange-500 text-white font-extrabold rounded-xl shadow-lg transform transition-all text-lg">
123
- نوبت‌دهی و ثبت سفارش آواتار
124
- </button>
125
- </form>
126
-
127
- <!-- Loading Panel -->
128
- <div id="status-box" class="hidden mt-8 border-t border-gray-800/80 pt-6 flex flex-col items-center">
129
- <div id="tracking-info-header" class="text-center font-semibold text-orange-400 text-sm mb-4"></div>
130
- <div class="generator-container relative w-full max-w-[450px] h-[250px] border border-orange-500/30 rounded-2xl overflow-hidden bg-black flex flex-col items-center justify-center p-6 text-center">
131
- <div class="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(255,107,53,0.15),transparent_70%)]"></div>
132
- <div class="text-overlay text-gray-100 font-bold text-base z-10" id="status-text">آپلود و آماده‌سازی فایل‌ها...</div>
133
- <div class="absolute bottom-0 left-0 h-1.5 bg-gradient-to-r from-orange-500 to-amber-400 transition-all duration-300" id="progress-bar" style="width: 0%"></div>
134
  </div>
135
  </div>
 
 
 
136
 
137
- <!-- Result Box -->
138
- <div id="result-box" class="hidden mt-8 border-t border-gray-800/80 pt-6 space-y-4 flex flex-col items-center">
139
- <div id="result-tracking-header" class="text-center font-semibold text-green-400 text-sm"></div>
140
-
141
- <div id="video-display-wrapper" class="hidden w-full max-w-[500px] rounded-2xl overflow-hidden border border-gray-800 shadow-2xl">
142
- <video id="output-video" controls class="w-full bg-black"></video>
 
 
 
 
 
 
 
 
143
  </div>
144
-
145
- <div id="error-card-display" class="hidden w-full max-w-[500px] p-4 bg-red-950/20 border border-red-900/50 rounded-2xl text-center">
146
- <p class="text-red-400 font-bold mb-3">⚠️ خطا در فرآیند تولید آواتار سخنگو</p>
147
- <img id="error-card-img" class="w-full rounded-xl shadow-lg" src="" />
148
  </div>
149
-
150
- <button id="download-btn" class="hidden py-3 px-8 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-xl text-sm font-bold flex items-center gap-2">
151
- 📥 دانلود مستقیم ویدیوی کامل
152
- </button>
153
  </div>
154
- </main>
155
-
156
- <!-- History Dashboard -->
157
- <section class="bg-gray-900/40 border border-gray-800 rounded-3xl p-6 backdrop-blur-xl">
158
- <h2 class="text-lg font-bold text-gray-200 mb-4 flex items-center gap-2">📋 لیست درخواست‌های ثبت شده شما</h2>
159
- <div id="history-list" class="grid grid-cols-1 md:grid-cols-2 gap-4"></div>
160
- </section>
161
-
162
  </div>
163
 
164
  <script>
165
- // --- دیتابیس لوکال (IndexedDB) ---
166
- const dbName = 'AvatarAppDB';
167
- const storeName = 'AvatarQueue';
168
- let db;
169
-
170
- function initDB() {
171
- return new Promise((resolve, reject) => {
172
- const request = indexedDB.open(dbName, 1);
173
- request.onupgradeneeded = e => { db = e.target.result; if (!db.objectStoreNames.contains(storeName)) db.createObjectStore(storeName, { keyPath: 'id' }); };
174
- request.onsuccess = e => { db = e.target.result; resolve(db); };
175
- request.onerror = e => reject(e.target.error);
176
- });
177
  }
178
 
179
- function saveLocalRequest(id, prompt, imageBase64, status = 'processing', url = '') {
180
- return new Promise((resolve) => {
181
- const req = db.transaction([storeName], 'readwrite').objectStore(storeName).put({ id, prompt, image: imageBase64, timestamp: Date.now(), status, url });
182
- req.onsuccess = resolve;
 
 
 
 
 
183
  });
184
- }
185
- function getLocalRequest(id) { return new Promise(res => { db.transaction([storeName]).objectStore(storeName).get(id).onsuccess = e => res(e.target.result); }); }
186
- function getAllLocalRequests() { return new Promise(res => { db.transaction([storeName]).objectStore(storeName).getAll().onsuccess = e => res(e.target.result); }); }
187
- function deleteLocalRequest(id) { return new Promise(res => { db.transaction([storeName], 'readwrite').objectStore(storeName).delete(id).onsuccess = res; }); }
188
-
189
- // --- المان‌ها و متغیرها ---
190
- const form = document.getElementById('avatar-form');
191
- const submitBtn = document.getElementById('submit-btn');
192
- const statusBox = document.getElementById('status-box');
193
- const statusText = document.getElementById('status-text');
194
- const progressBar = document.getElementById('progress-bar');
195
- const resultBox = document.getElementById('result-box');
196
- const videoDisplayWrapper = document.getElementById('video-display-wrapper');
197
- const outputVideo = document.getElementById('output-video');
198
- const downloadBtn = document.getElementById('download-btn');
199
- const errorCardDisplay = document.getElementById('error-card-display');
200
- const errorCardImg = document.getElementById('error-card-img');
201
- const trackingInfoHeader = document.getElementById('tracking-info-header');
202
- const resultTrackingHeader = document.getElementById('result-tracking-header');
203
- const historyList = document.getElementById('history-list');
204
-
205
- let cachedImageBase64 = '';
206
- let currentRunId = '';
207
- let pollInterval = null;
208
-
209
- // --- هندلینگ فایل‌ها ---
210
- document.getElementById('image-dropzone').onclick = (e) => { if (e.target.id !== 'remove-image') document.getElementById('image-input').click(); };
211
- document.getElementById('image-input').onchange = e => handleImg(e.target.files[0]);
212
- function handleImg(file) {
213
- if (!file) return;
214
- const reader = new FileReader();
215
- reader.onload = e => {
216
- cachedImageBase64 = e.target.result;
217
- document.getElementById('image-preview').src = cachedImageBase64;
218
- document.getElementById('image-preview').classList.remove('hidden');
219
- document.getElementById('image-upload-prompt').classList.add('hidden');
220
- document.getElementById('remove-image').classList.remove('hidden');
221
- }; reader.readAsDataURL(file);
222
- }
223
- document.getElementById('remove-image').onclick = e => { e.stopPropagation(); document.getElementById('image-input').value = ''; cachedImageBase64 = ''; document.getElementById('image-preview').classList.add('hidden'); document.getElementById('image-upload-prompt').classList.remove('hidden'); e.target.classList.add('hidden'); };
224
-
225
- document.getElementById('audio-dropzone').onclick = (e) => { if (e.target.id !== 'remove-audio' && e.target.id !== 'audio-preview') document.getElementById('audio-input').click(); };
226
- document.getElementById('audio-input').onchange = e => handleAud(e.target.files[0]);
227
- function handleAud(file) {
228
- if (!file) return;
229
- document.getElementById('audio-preview').src = URL.createObjectURL(file);
230
- document.getElementById('audio-name').textContent = file.name;
231
- document.getElementById('audio-player-wrapper').classList.remove('hidden');
232
- document.getElementById('audio-upload-prompt').classList.add('hidden');
233
- document.getElementById('remove-audio').classList.remove('hidden');
234
- }
235
- document.getElementById('remove-audio').onclick = e => { e.stopPropagation(); document.getElementById('audio-input').value = ''; document.getElementById('audio-player-wrapper').classList.add('hidden'); document.getElementById('audio-upload-prompt').classList.remove('hidden'); e.target.classList.add('hidden'); };
236
 
237
- function safeBase64Encode(str) { return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode('0x' + p1))); }
 
 
 
 
 
 
 
 
238
 
239
- async function uploadFileToDocker(file, suffix) {
240
- const formData = new FormData();
241
- const ext = file.name.split('.').pop().toLowerCase();
242
- formData.append('run_id', `${currentRunId}_${suffix}`);
243
- formData.append('ext', ext);
244
- formData.append('file', file);
245
- await fetch('/api/webhook/upload', { method: 'POST', body: formData });
246
- return ext;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  }
248
 
249
- // رندر تاریخچه
250
- async function renderHistoryList() {
251
- const list = await getAllLocalRequests();
252
- historyList.innerHTML = '';
253
- if (list.length === 0) { historyList.innerHTML = '<div class="col-span-full text-center text-sm text-gray-500 py-4">هنوز هیچ درخواستی ثبت نکرده‌اید.</div>'; return; }
254
- list.sort((a, b) => b.timestamp - a.timestamp);
255
-
256
- list.forEach(item => {
257
- const dateStr = new Date(item.timestamp).toLocaleString('fa-IR');
258
- let badgeClass = item.status === 'completed' ? 'bg-emerald-950/40 text-emerald-400 border-emerald-900' : item.status === 'failed' ? 'bg-red-950/20 text-red-400 border-red-900/50' : 'bg-yellow-950/40 text-yellow-400 border-yellow-900';
259
- let statusText = item.status === 'completed' ? 'آماده شده 🎉' : item.status === 'failed' ? 'ناموفق ⚠️' : 'در حال پردازش ';
260
-
261
- const card = document.createElement('div');
262
- card.className = 'bg-gray-950/50 border border-gray-800 rounded-2xl p-4 flex gap-4 items-center justify-between';
263
- card.innerHTML = `
264
- <div class="flex gap-3 items-center min-w-0">
265
- <img src="${item.image || 'https://via.placeholder.com/60'}" class="w-14 h-14 object-cover rounded-xl border border-gray-800 flex-shrink-0" />
266
- <div class="min-w-0">
267
- <p class="text-xs text-gray-500 font-semibold mb-1">${dateStr}</p>
268
- <p class="text-sm text-gray-200 font-bold truncate max-w-[200px]">${item.prompt}</p>
269
- <p class="text-[10px] font-mono text-gray-400 mt-1 truncate max-w-[200px]">کد: ${item.id}</p>
270
- </div>
271
- </div>
272
- <div class="flex flex-col items-end gap-2">
273
- <span class="px-2 py-1 text-xs font-bold rounded ${badgeClass}">${statusText}</span>
274
- <div class="flex gap-2">
275
- <button onclick="checkSpecificCode('${item.id}')" class="text-xs bg-orange-600/20 hover:bg-orange-600 text-orange-400 hover:text-white px-2.5 py-1.5 rounded-lg transition-all">بررسی / پخش</button>
276
- <button onclick="handleDeleteHistoryItem('${item.id}')" class="text-xs bg-red-950/20 hover:bg-red-600 text-red-400 hover:text-white px-2 py-1.5 rounded-lg transition-all">✕</button>
277
- </div>
278
- </div>
279
- `;
280
- historyList.appendChild(card);
281
- });
282
  }
283
 
284
- window.handleDeleteHistoryItem = async function(id) { if (confirm("آیا از حذف این درخواست از حافظه مرورگر اطمینان دارید؟")) { await deleteLocalRequest(id); renderHistoryList(); } }
285
-
286
- // ثبت فرم
287
- form.addEventListener('submit', async (e) => {
288
- e.preventDefault();
289
- const imageFile = document.getElementById('image-input').files[0];
290
- const audioFile = document.getElementById('audio-input').files[0];
291
- const prompt = document.getElementById('prompt-input').value.trim();
292
- const resolution = document.getElementById('resolution-input').value;
293
- const seed = document.getElementById('seed-input').value;
294
- const vocalMode = document.getElementById('vocal-mode-input').value;
295
- const acceleration = document.getElementById('acceleration-input').value;
296
 
297
- if (!imageFile || !audioFile || !prompt) return alert('لطفاً فیلدها را کامل کنید.');
298
-
299
- submitBtn.disabled = true;
300
- statusBox.classList.remove('hidden');
301
- resultBox.classList.add('hidden');
302
- progressBar.style.width = '0%';
 
 
303
 
304
- currentRunId = 'avatar' + Math.random().toString(36).substring(2, 14);
 
 
 
 
 
305
 
306
  try {
307
- statusText.innerHTML = "<div>۱/۳: در حال آپلود امن فایل‌ها به سرور داکر...</div>";
308
- progressBar.style.width = '20%';
309
- const imgExt = await uploadFileToDocker(imageFile, 'avatar_img');
310
- const audExt = await uploadFileToDocker(audioFile, 'avatar_aud');
311
-
312
- statusText.innerHTML = "<div>۲/۳: ثبت نوبت و ارسال سیگنال پردازش طولانی به گیت‌هاب...</div>";
313
- progressBar.style.width = '50%';
314
-
315
- const b64Prompt = safeBase64Encode(prompt);
316
- const vocalSlug = vocalMode.includes('Clean') ? 'clean' : 'isolate';
317
- let accelSlug = 'dbfaster';
318
- if (acceleration.includes('DBCache fast')) accelSlug = 'dbfast';
319
- else if (acceleration.includes('Exact')) accelSlug = 'exact8';
320
-
321
- const avatarConfigPayload = `AVATARCONFIG_userRunId_${currentRunId}_imgExt_${imgExt}_audExt_${audExt}_res_${resolution}_seed_${seed}_vocal_${vocalSlug}_accel_${accelSlug}_prompt_${b64Prompt}`;
322
-
323
- const response = await fetch('/api/generate', {
324
- method: 'POST',
325
- headers: { 'Content-Type': 'application/json' },
326
- body: JSON.stringify({ prompt: avatarConfigPayload, width: 1024, height: 1024, action_name: 'avatar' })
327
- });
328
  const data = await response.json();
329
-
330
  if (data.status === 'success') {
331
- await saveLocalRequest(data.run_id, prompt, cachedImageBase64, 'processing');
332
- await renderHistoryList();
333
-
334
- progressBar.style.width = '100%';
335
- trackingInfoHeader.innerHTML = `کد پیگیری شما: <span class="font-mono text-white text-lg">${data.run_id}</span>`;
336
- statusText.innerHTML = `
337
- <div class="text-green-400 font-bold mb-2">🎉 درخواست ثبت شد!</div>
338
- <div class="text-sm text-gray-300">سیستم به صورت خودکار صوت شما را قطعه‌قطعه کرده و ویدیو را می‌سازد.</div>
339
- <div class="text-[11px] text-gray-400 mt-2">این فرآیند بسته به طول صوت ممکن است چند دقیقه طول بکشد. می‌توانید صفحه را ببندید و بعداً با کد پیگیری برگردید.</div>
340
- `;
341
- submitBtn.disabled = false;
342
- } else throw new Error('خطا در ارتباط با وب‌هوک.');
343
- } catch (err) {
344
- statusText.innerHTML = `<span class="text-red-500">خطا: ${err.message}</span>`;
345
- progressBar.style.width = '0%';
346
- submitBtn.disabled = false;
347
  }
348
  });
349
 
350
- // پیگیری دستی کد
351
- document.getElementById('btn-track-submit').addEventListener('click', () => {
352
- const code = document.getElementById('track-code-input').value.trim();
353
- if (!code) return alert('کد وارد نشده است.');
354
- checkSpecificCode(code);
355
- });
356
-
357
- window.checkSpecificCode = function(code) {
358
- if (pollInterval) clearInterval(pollInterval);
359
- statusBox.classList.remove('hidden');
360
- resultBox.classList.add('hidden');
361
- trackingInfoHeader.innerHTML = `در حال مانیتورینگ زنده کد: <span class="font-mono text-orange-400 font-bold">${code}</span>`;
362
- progressBar.style.width = '10%';
363
- statusText.innerText = 'ارتباط با سرور...';
364
- startPolling(code);
365
- }
366
-
367
- // سیستم پیگیری زنده و خواندن فایل progress.json
368
- function startPolling(runId) {
369
- if (pollInterval) clearInterval(pollInterval);
370
- let emptyCounter = 0;
371
-
372
- pollInterval = setInterval(async () => {
373
- try {
374
- // ۱. چک کردن ویدیو نهایی
375
- try {
376
- const mp4Check = await fetch(`/static/images/${runId}.mp4`, { method: 'HEAD' });
377
- if (mp4Check.ok) {
378
- clearInterval(pollInterval);
379
- statusBox.classList.add('hidden');
380
- errorCardDisplay.classList.add('hidden');
381
-
382
- resultTrackingHeader.innerText = `کد ${runId} با موفقیت به پایان رسید`;
383
- outputVideo.src = `/static/images/${runId}.mp4`;
384
- videoDisplayWrapper.classList.remove('hidden');
385
- downloadBtn.classList.remove('hidden');
386
- downloadBtn.onclick = () => { parent.postMessage({ type: 'DOWNLOAD_REQUEST', url: window.location.origin + `/static/images/${runId}.mp4` }, '*'); };
387
-
388
- const localData = await getLocalRequest(runId);
389
- if (localData) { await saveLocalRequest(runId, localData.prompt, localData.image, 'completed', `/static/images/${runId}.mp4`); renderHistoryList(); }
390
- resultBox.classList.remove('hidden');
391
- return;
392
- }
393
- } catch(e) {}
394
-
395
- // ۲. چک کردن وضعیت اکشن‌ها
396
- const activeRes = await fetch('/api/actions/active');
397
- const activeData = await activeRes.json();
398
- const hasActiveActions = activeData.status === 'success' && activeData.actions.length > 0;
399
-
400
- // ۳. بررسی فایل progress.json برای نمایش زنده مرحله‌ی کار (بخش X از Y)
401
- try {
402
- const progRes = await fetch(`/static/images/${runId}_progress.json?t=${Date.now()}`);
403
- if (progRes.ok) {
404
- const progData = await progRes.json();
405
- statusText.innerHTML = `
406
- <div class="text-orange-400 font-bold text-lg mb-2">در حال ساخت ویدیو...</div>
407
- <div class="text-gray-200">در حال تولید و اتصال قسمت <span class="text-white text-xl mx-1">${progData.current}</span> از <span class="text-white text-xl mx-1">${progData.total}</span></div>
408
- `;
409
- // محاسبه درصد پیشرفت بر اساس قطعات
410
- let basePercent = ((progData.current - 1) / progData.total) * 100;
411
- let currentChunkProgress = (1 / progData.total) * 50; // یک پیشرفت فرضی برای لودینگ
412
- progressBar.style.width = `${basePercent + currentChunkProgress}%`;
413
- } else {
414
- if (hasActiveActions) {
415
- statusText.innerText = "سیستم در حال راه‌اندازی فریم‌های اولیه است...";
416
- }
417
- }
418
- } catch(e) {}
419
 
420
- // ۴. چک کردن خطای نهایی
421
- const statusRes = await fetch(`/api/status/${runId}`);
422
- const statusData = await statusRes.json();
423
-
424
- if (statusData.status === 'ready') {
425
- const urlLower = statusData.url.toLowerCase();
426
- if (urlLower.endsWith('.png') || urlLower.endsWith('.jpg')) {
427
- if (hasActiveActions) {
428
- emptyCounter = 0;
429
- statusText.innerHTML = `<span class="text-yellow-400">تلاش رانر قبلی به مشکل خورد. راه‌اندازی رانر جایگزین...</span>`;
430
- return;
431
- } else {
432
- emptyCounter++;
433
- if (emptyCounter < 4) return;
434
- }
435
- clearInterval(pollInterval);
436
- statusBox.classList.add('hidden');
437
-
438
- resultTrackingHeader.innerText = `فرآیند برای کد ${runId} لغو شد`;
439
- videoDisplayWrapper.classList.add('hidden');
440
- errorCardImg.src = statusData.url;
441
- errorCardDisplay.classList.remove('hidden');
442
- downloadBtn.classList.add('hidden');
443
-
444
- const localData = await getLocalRequest(runId);
445
- if (localData) { await saveLocalRequest(runId, localData.prompt, localData.image, 'failed', statusData.url); renderHistoryList(); }
446
- resultBox.classList.remove('hidden');
447
- }
448
- } else if (!hasActiveActions && emptyCounter > 4) {
449
- statusText.innerText = "در انتظار تخصیص رانر جدید در صف گیت‌هاب...";
450
- }
451
- } catch (e) { console.error("Error polling:", e); }
452
- }, 4000);
453
- }
454
 
455
- document.addEventListener('DOMContentLoaded', async () => {
456
- await initDB();
457
- await renderHistoryList();
 
 
 
 
 
 
 
 
 
 
 
458
  });
459
  </script>
460
  </body>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>مولد صدای هوشمند MMAudio</title>
7
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.003/Vazirmatn-font-face.css">
 
8
  <style>
9
  :root {
10
  --app-font: 'Vazirmatn', sans-serif;
11
+ --app-bg: #F8F9FC;
12
+ --panel-bg: #FFFFFF;
13
+ --panel-border: #EAEFF7;
14
+ --input-bg: #F6F8FB;
15
+ --input-border: #E1E7EF;
16
+ --text-primary: #1A202C;
17
+ --text-secondary: #626F86;
18
+ --text-tertiary: #8A94A6;
19
+ --accent-primary: #4A6CFA;
20
+ --accent-primary-hover: #3553D6;
21
+ --accent-primary-glow: rgba(74, 108, 250, 0.25);
22
  --accent-secondary: #0FD4A8;
23
+ --accent-secondary-hover: #0DA986;
24
+ --accent-secondary-glow: rgba(15, 212, 168, 0.2);
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);
28
+ --shadow-xl: 0 20px 25px -5px rgba(26, 32, 44, 0.07), 0 8px 10px -6px rgba(26, 32, 44, 0.05);
29
+ --radius-card: 24px;
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
+ * { margin: 0; padding: 0; box-sizing: border-box; }
36
  body {
37
  font-family: var(--app-font);
38
+ background-color: var(--app-bg);
39
+ color: var(--text-primary);
40
+ min-height: 100vh;
41
+ overflow-x: hidden;
42
+ display: flex;
43
+ justify-content: center;
44
+ align-items: flex-start;
45
+ padding: 2.5rem 1rem;
46
+ background-image: radial-gradient(var(--text-tertiary) 0.5px, transparent 0.5px);
47
+ background-size: 20px 20px;
48
+ background-position: -10px -10px;
49
+ }
50
+ .container { max-width: 600px; width: 100%; margin: 0 auto; display: flex; flex-direction: column; }
51
+
52
+ .header { text-align: center; padding: 1rem 0 2rem; }
53
+ .logo { font-size: 3rem; margin-bottom: 10px; filter: drop-shadow(0 4px 8px rgba(0,0,0,0.1)); }
54
+ .title { font-size: 2.2rem; font-weight: 900; margin-bottom: 0.8rem; background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; letter-spacing: -1px; }
55
+ .subtitle { font-size: 1rem; color: var(--text-secondary); opacity: 0.9; }
56
+
57
+ .tabs { display: flex; background: var(--input-bg); border-radius: var(--radius-btn); padding: 6px; margin-bottom: 20px; border: 1px solid var(--panel-border); }
58
+ .tab { flex: 1; padding: 12px; text-align: center; border-radius: 10px; cursor: pointer; transition: var(--transition-smooth); font-weight: 600; font-size: 0.9rem; color: var(--text-secondary); }
59
+ .tab.active { background: var(--panel-bg); color: var(--text-primary); transform: translateY(-2px); box-shadow: var(--shadow-lg); }
60
+
61
+ .card { background: var(--panel-bg); border-radius: var(--radius-card); padding: 30px; border: 1px solid var(--panel-border); box-shadow: var(--shadow-xl); display: none; flex-direction: column; }
62
+ .tab-content.active { display: flex; }
63
+
64
+ .form-group { margin-bottom: 20px; }
65
+ .label { display: block; margin-bottom: 10px; font-size: 1rem; font-weight: 700; color: var(--text-primary); }
66
+
67
+ .input { width: 100%; padding: 15px; border: 1px solid var(--input-border); border-radius: var(--radius-input); background: var(--input-bg); color: var(--text-primary); font-size: 1rem; font-family: var(--app-font); outline: none; transition: var(--transition-smooth); box-shadow: var(--shadow-sm) inset; }
68
+ .input:focus { background: var(--panel-bg); border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-primary-glow), var(--shadow-sm) inset; }
69
+ .input-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
70
+ .file-upload { position: relative; background: var(--input-bg); border: 2px dashed var(--input-border); border-radius: var(--radius-input); padding: 20px; text-align: center; cursor: pointer; transition: var(--transition-smooth); min-height: 160px; display: flex; flex-direction: column; align-items: center; justify-content: center; overflow: hidden; }
71
+ .file-upload:hover, .file-upload.drag-over { background: white; border-color: var(--accent-primary); box-shadow: 0 0 15px var(--accent-primary-glow); transform: translateY(-2px); }
72
+ .file-upload input { position: absolute; left: -9999px; }
73
+ .upload-icon { font-size: 2.5rem; margin-bottom: 10px; color: var(--accent-primary); }
74
+ .upload-text { color: var(--text-secondary); font-size: 0.9rem; font-weight: 500; }
75
+ .file-upload.has-preview { border-style: solid; border-color: var(--accent-primary); padding: 10px; cursor: default; }
76
+ .preview-container { position: relative; width: 100%; height: 100%; }
77
+ .preview-container video { width: 100%; max-height: 250px; border-radius: var(--radius-input); display: block; }
78
+ .remove-btn { position: absolute; top: 10px; left: 10px; width: 32px; height: 32px; background-color: rgba(0, 0, 0, 0.6); color: white; border: none; border-radius: 50%; font-size: 20px; font-weight: bold; cursor: pointer; display: flex; align-items: center; justify-content: center; line-height: 1; transition: var(--transition-smooth); z-index: 10; }
79
+ .remove-btn:hover { background-color: #e53e3e; transform: scale(1.1); }
80
+
81
+ .btn { width: 100%; padding: 16px; border: none; border-radius: var(--radius-btn); font-size: 1.1rem; font-weight: 700; cursor: pointer; margin-top: 10px; transition: var(--transition-smooth); background: linear-gradient(95deg, var(--accent-secondary) 0%, var(--accent-primary) 100%); color: white; box-shadow: 0 6px 12px -3px var(--accent-primary-glow), 0 6px 12px -3px var(--accent-secondary-glow); }
82
+ .btn:hover:not(:disabled) { transform: translateY(-5px) scale(1.02); box-shadow: 0 8px 20px -4px var(--accent-primary-glow), 0 8px 20px -4px var(--accent-secondary-glow); }
83
+ .btn:disabled { background: var(--text-tertiary); color: var(--text-secondary); cursor: not-allowed; transform: none; box-shadow: none; opacity: 0.7; }
84
+ .btn:active:not(:disabled) { transform: translateY(0); }
85
+
86
+ .download-btn { display: inline-flex; align-items: center; justify-content: center; gap: 10px; padding: 12px 25px; margin-top: 20px; width: auto; border: none; border-radius: var(--radius-btn); font-size: 1rem; font-weight: 700; cursor: pointer; transition: var(--transition-smooth); background: var(--accent-primary); color: white; box-shadow: 0 6px 12px -3px var(--accent-primary-glow); }
87
+ .download-btn:hover { background: var(--accent-primary-hover); transform: translateY(-3px); box-shadow: 0 8px 15px -4px var(--accent-primary-glow); }
88
+
89
+ .result { margin-top: 20px; text-align: center; }
90
+ .status-text { color: var(--text-secondary); font-weight: 500; margin: 20px 0; line-height: 1.6; }
91
+ .status-text.error { color: #e53e3e; font-weight: 600; }
92
+ .status-text.success { color: #38a169; font-weight: 600; margin-bottom: 15px; }
93
+ audio, video { width: 100%; margin-top: 10px; border-radius: var(--radius-input); box-shadow: var(--shadow-md); outline: none; }
94
+ .loader { width: 40px; height: 40px; border: 4px solid var(--input-border); border-top: 4px solid var(--accent-primary); border-radius: 50%; animation: spin 1s linear infinite; margin: 20px auto; }
95
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
96
+ .icon { display: inline-block; margin-left: 8px; vertical-align: middle; }
97
+
98
+ /* ===== TOAST NOTIFICATION STYLES ===== */
99
+ #toast-container {
100
+ position: fixed;
101
+ top: 20px;
102
+ left: 50%;
103
+ transform: translateX(-50%);
104
+ z-index: 9999;
105
+ display: flex;
106
+ flex-direction: column;
107
+ gap: 10px;
108
+ }
109
+ .toast {
110
+ background: linear-gradient(135deg, #ff0844 0%, #ffb199 100%);
111
+ color: white;
112
+ padding: 15px 25px;
113
+ border-radius: 50px;
114
+ font-family: var(--app-font);
115
+ font-size: 1rem;
116
+ font-weight: bold;
117
+ box-shadow: 0 10px 25px rgba(255, 8, 68, 0.4);
118
+ display: flex;
119
+ align-items: center;
120
+ gap: 10px;
121
+ animation: slideDownBounce 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards;
122
+ opacity: 0;
123
+ }
124
+ .toast.fade-out {
125
+ animation: fadeOutUp 0.5s forwards;
126
+ }
127
+ @keyframes slideDownBounce {
128
+ 0% { transform: translateY(-50px) scale(0.8); opacity: 0; }
129
+ 100% { transform: translateY(0) scale(1); opacity: 1; }
130
+ }
131
+ @keyframes fadeOutUp {
132
+ to { opacity: 0; transform: translateY(-50px) scale(0.9); }
133
  }
 
 
 
 
 
 
 
134
  </style>
135
  </head>
136
+ <body>
137
+ <div id="toast-container"></div>
138
+
139
+ <div class="container">
140
+ <div class="header">
141
+ <div class="logo">🎧</div>
142
+ <h1 class="title">مولد صدای هوشمند</h1>
143
+ <p class="subtitle">با کمک هوش مصنوعی از متن یا ویدیو، صدا بسازید</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  </div>
145
 
146
+ <div class="tabs">
147
+ <div class="tab active" data-tab="video-to-audio"><span class="icon">🎬</span>صدا برای ویدیو</div>
148
+ <div class="tab" data-tab="text-to-audio"><span class="icon">✏️</span>متن به صدا</div>
149
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
+ <!-- بخش ساخت صدا برای ویدیو -->
152
+ <div class="card tab-content active" id="video-to-audio-content">
153
+ <div class="form-group">
154
+ <label class="label">فایل ویدیو را انتخاب کنید:</label>
155
+ <div class="file-upload" id="vta-file-upload-area">
156
+ <input type="file" id="vta-video" accept="video/*">
157
+ <div class="upload-content">
158
+ <div class="upload-icon">📹</div>
159
+ <div class="upload-text">برای انتخاب کلیک کنید یا فایل را اینجا بکشید</div>
 
 
 
160
  </div>
161
  </div>
162
+ </div>
163
+ <div class="form-group">
164
+ <label class="label" for="vta-prompt">متن اصلی (اختیاری):</label>
165
+ <input type="text" id="vta-prompt" class="input" placeholder="می‌توانید برای هدایت بهتر، صدا را توصیف کنید">
166
+ </div>
167
+ <div class="form-group">
168
+ <label class="label" for="vta-negative-prompt">متن منفی (مواردی که نمی‌خواهید):</label>
169
+ <input type="text" id="vta-negative-prompt" class="input" placeholder="مثلا: م��سیقی، صدای انسان">
170
+ </div>
171
+ <div class="input-grid">
172
+ <div class="form-group">
173
+ <label class="label" for="vta-seed">Seed:</label>
174
+ <input type="number" id="vta-seed" class="input" value="-1">
175
  </div>
176
+ <div class="form-group">
177
+ <label class="label" for="vta-duration">(مدت (ثانیه:</label>
178
+ <input type="number" id="vta-duration" class="input" value="8" max="60" min="1">
 
 
 
 
 
 
 
 
 
 
179
  </div>
180
  </div>
181
+ <div id="vta-result" class="result"></div>
182
+ <button id="generate-video-audio" class="btn"><span class="icon">🪄</span>ایجاد صدا برای ویدیو</button>
183
+ </div>
184
 
185
+ <!-- بخش تبدیل متن به صدا -->
186
+ <div class="card tab-content" id="text-to-audio-content">
187
+ <div class="form-group">
188
+ <label class="label" for="tta-prompt">متن اصلی (توضیح صدا):</label>
189
+ <input type="text" id="tta-prompt" class="input" placeholder="مثلا: صدای امواج دریا و مرغان دریایی">
190
+ </div>
191
+ <div class="form-group">
192
+ <label class="label" for="tta-negative-prompt">متن منفی (مواردی که نمی‌خواهید):</label>
193
+ <input type="text" id="tta-negative-prompt" class="input" placeholder="مثلا: موسیقی، نویز زیاد">
194
+ </div>
195
+ <div class="input-grid">
196
+ <div class="form-group">
197
+ <label class="label" for="tta-seed">Seed:</label>
198
+ <input type="number" id="tta-seed" class="input" value="-1">
199
  </div>
200
+ <div class="form-group">
201
+ <label class="label" for="tta-duration">(مدت (ثانیه:</label>
202
+ <input type="number" id="tta-duration" class="input" value="8" max="60" min="1">
 
203
  </div>
 
 
 
 
204
  </div>
205
+ <div id="tta-result" class="result"></div>
206
+ <button id="generate-text-audio" class="btn"><span class="icon">✨</span>ایجاد صدا</button>
207
+ </div>
 
 
 
 
 
208
  </div>
209
 
210
  <script>
211
+ // --- تابع انیمیشن خطای اختصاصی زیبا ---
212
+ function showToast(message) {
213
+ const container = document.getElementById('toast-container');
214
+ const toast = document.createElement('div');
215
+ toast.className = 'toast';
216
+ toast.innerHTML = `<span>⚠️</span> ${message}`;
217
+ container.appendChild(toast);
218
+
219
+ setTimeout(() => {
220
+ toast.classList.add('fade-out');
221
+ setTimeout(() => toast.remove(), 500);
222
+ }, 4000);
223
  }
224
 
225
+ const tabs = document.querySelectorAll('.tab');
226
+ const tabContents = document.querySelectorAll('.tab-content');
227
+
228
+ tabs.forEach(tab => {
229
+ tab.addEventListener('click', () => {
230
+ tabs.forEach(t => t.classList.remove('active'));
231
+ tabContents.forEach(tc => tc.classList.remove('active'));
232
+ tab.classList.add('active');
233
+ document.getElementById(tab.getAttribute('data-tab') + '-content').classList.add('active');
234
  });
235
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
 
237
+ const vtaFileInput = document.getElementById('vta-video');
238
+ const vtaFileUploadArea = document.getElementById('vta-file-upload-area');
239
+ const vtaUploadContent = vtaFileUploadArea.querySelector('.upload-content');
240
+
241
+ vtaFileUploadArea.addEventListener('click', (e) => {
242
+ if (!vtaFileUploadArea.classList.contains('has-preview') || e.target === vtaFileUploadArea) {
243
+ vtaFileInput.click();
244
+ }
245
+ });
246
 
247
+ const handleFileSelect = () => {
248
+ const file = vtaFileInput.files[0];
249
+ const existingPreview = vtaFileUploadArea.querySelector('.preview-container');
250
+ if (existingPreview) existingPreview.remove();
251
+
252
+ if (file) {
253
+ // بررسی مدت زمان فایل آپلودی
254
+ const videoElement = document.createElement('video');
255
+ videoElement.preload = 'metadata';
256
+ videoElement.onloadedmetadata = function() {
257
+ window.URL.revokeObjectURL(videoElement.src);
258
+
259
+ if (videoElement.duration > 60) {
260
+ showToast('ویدیوی انتخابی طولانی است! حداکثر زمان مجاز ۱ دقیقه است.');
261
+ vtaFileInput.value = '';
262
+ vtaUploadContent.style.display = 'flex';
263
+ vtaFileUploadArea.classList.remove('has-preview');
264
+ return;
265
+ }
266
+
267
+ // در صورت مجاز بودن ایجاد پیش‌نمایش
268
+ const fileURL = URL.createObjectURL(file);
269
+ const previewContainer = document.createElement('div');
270
+ previewContainer.className = 'preview-container';
271
+ const videoPreview = document.createElement('video');
272
+ videoPreview.src = fileURL;
273
+ videoPreview.controls = true;
274
+ videoPreview.muted = true;
275
+ const removeBtn = document.createElement('button');
276
+ removeBtn.className = 'remove-btn';
277
+ removeBtn.innerHTML = '&times;';
278
+ removeBtn.onclick = (e) => {
279
+ e.stopPropagation();
280
+ vtaFileInput.value = '';
281
+ previewContainer.remove();
282
+ vtaUploadContent.style.display = 'flex';
283
+ vtaFileUploadArea.classList.remove('has-preview');
284
+ };
285
+ previewContainer.appendChild(videoPreview);
286
+ previewContainer.appendChild(removeBtn);
287
+ vtaUploadContent.style.display = 'none';
288
+ vtaFileUploadArea.appendChild(previewContainer);
289
+ vtaFileUploadArea.classList.add('has-preview');
290
+ };
291
+ videoElement.src = URL.createObjectURL(file);
292
+ } else {
293
+ vtaUploadContent.style.display = 'flex';
294
+ vtaFileUploadArea.classList.remove('has-preview');
295
+ }
296
+ };
297
+
298
+ vtaFileInput.addEventListener('change', handleFileSelect);
299
+
300
+ function createDownloadButton(fileUrl) {
301
+ const button = document.createElement('button');
302
+ button.className = 'download-btn';
303
+ button.innerHTML = `<span class="icon">⬇️</span> دانلود`;
304
+ button.onclick = () => {
305
+ parent.postMessage({ type: 'DOWNLOAD_REQUEST', url: fileUrl }, '*');
306
+ };
307
+ return button;
308
  }
309
 
310
+ async function pollStatus(runId, resultContainer) {
311
+ const interval = setInterval(async () => {
312
+ try {
313
+ const res = await fetch(`/api/status/${runId}`);
314
+ const data = await res.json();
315
+ if (data.status === 'ready') {
316
+ clearInterval(interval);
317
+ resultContainer.innerHTML = '';
318
+ const successText = document.createElement('p');
319
+ successText.className = 'status-text success';
320
+ successText.textContent = 'صدا با موفقیت ایجاد شد!';
321
+
322
+ const media = document.createElement('video');
323
+ media.controls = true;
324
+ media.src = data.url;
325
+ media.style.maxHeight = '250px';
326
+
327
+ const downloadBtn = createDownloadButton(data.url);
328
+
329
+ resultContainer.appendChild(successText);
330
+ resultContainer.appendChild(media);
331
+ resultContainer.appendChild(downloadBtn);
332
+ }
333
+ } catch (e) {
334
+ console.error('Polling error:', e);
335
+ }
336
+ }, 3000);
 
 
 
 
 
 
337
  }
338
 
339
+ const ttaButton = document.getElementById('generate-text-audio');
340
+ const ttaResult = document.getElementById('tta-result');
341
+ ttaButton.addEventListener('click', async () => {
342
+ const prompt = document.getElementById('tta-prompt').value;
343
+ const durationVal = parseInt(document.getElementById('tta-duration').value);
 
 
 
 
 
 
 
344
 
345
+ if (!prompt) { alert('لطفا متن اصلی را وارد کنید.'); return; }
346
+ if (durationVal > 60) {
347
+ showToast('زمان تولید نمی‌تواند بیشتر از ۶۰ ثانیه باشد!');
348
+ return;
349
+ }
350
+
351
+ ttaButton.disabled = true;
352
+ ttaResult.innerHTML = '<div class="loader"></div><p class="status-text">۱. در حال ارسال دستور پردازش...</p>';
353
 
354
+ const formData = new FormData();
355
+ formData.append('type', 'text');
356
+ formData.append('prompt', prompt);
357
+ formData.append('negative_prompt', document.getElementById('tta-negative-prompt').value);
358
+ formData.append('seed', document.getElementById('tta-seed').value);
359
+ formData.append('duration', durationVal);
360
 
361
  try {
362
+ const response = await fetch('/api/generate-audio', { method: 'POST', body: formData });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
  const data = await response.json();
 
364
  if (data.status === 'success') {
365
+ ttaResult.innerHTML = `<div class="loader"></div><p class="status-text">۲. دستور با موفقیت ارسال شد.<br>در حال ��ولید صدای درخواستی، تولید صدا ممکنه زمان بر باشه لطفاً صبور باشید...</p>`;
366
+ pollStatus(data.run_id, ttaResult);
367
+ } else {
368
+ ttaResult.innerHTML = `<p class="status-text error">خطا: ${data.message}</p>`;
369
+ }
370
+ } catch (error) {
371
+ ttaResult.innerHTML = `<p class="status-text error">خطای ارتباط: ${error.message}</p>`;
372
+ } finally {
373
+ ttaButton.disabled = false;
 
 
 
 
 
 
 
374
  }
375
  });
376
 
377
+ const vtaButton = document.getElementById('generate-video-audio');
378
+ const vtaResult = document.getElementById('vta-result');
379
+ vtaButton.addEventListener('click', async () => {
380
+ const videoFile = document.getElementById('vta-video').files[0];
381
+ const durationVal = parseInt(document.getElementById('vta-duration').value);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
 
383
+ if (!videoFile) { alert('لطفا یک فایل ویدیویی انتخاب کنید.'); return; }
384
+ if (durationVal > 60) {
385
+ showToast('زمان تولید نمی‌تواند بیشتر از ۶۰ ثانیه باشد!');
386
+ return;
387
+ }
388
+
389
+ vtaButton.disabled = true;
390
+ vtaResult.innerHTML = '<div class="loader"></div><p class="status-text">۱. در حال ارسال ویدیو و دستورات پردازش...</p>';
391
+
392
+ const formData = new FormData();
393
+ formData.append('type', 'video');
394
+ formData.append('file', videoFile);
395
+ formData.append('prompt', document.getElementById('vta-prompt').value);
396
+ formData.append('negative_prompt', document.getElementById('vta-negative-prompt').value);
397
+ formData.append('seed', document.getElementById('vta-seed').value);
398
+ formData.append('duration', durationVal);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
 
400
+ try {
401
+ const response = await fetch('/api/generate-audio', { method: 'POST', body: formData });
402
+ const data = await response.json();
403
+ if (data.status === 'success') {
404
+ vtaResult.innerHTML = `<div class="loader"></div><p class="status-text">۲. فایل ویدیویی ارسال شد.<br>در حال ساخت صدای سینمایی منطبق بر تصویر... (ساخت صدا ممکنه زمان بر باشه لطفاً صبور باشید)</p>`;
405
+ pollStatus(data.run_id, vtaResult);
406
+ } else {
407
+ vtaResult.innerHTML = `<p class="status-text error">خطا: ${data.message}</p>`;
408
+ }
409
+ } catch (error) {
410
+ vtaResult.innerHTML = `<p class="status-text error">خطای ارتباط: ${error.message}</p>`;
411
+ } finally {
412
+ vtaButton.disabled = false;
413
+ }
414
  });
415
  </script>
416
  </body>