| <!DOCTYPE html> |
| <html lang="fa" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>استودیو خلق آواتار هوشمند - LongCat 1.5</title> |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.003/Vazirmatn-font-face.css"> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <style> |
| :root { |
| --app-font: 'Vazirmatn', sans-serif; |
| --accent-primary: #FF6B35; |
| --accent-primary-hover: #E85A28; |
| --accent-primary-glow: rgba(255, 107, 53, 0.25); |
| --accent-secondary: #0FD4A8; |
| } |
| body { |
| font-family: var(--app-font); |
| background-color: #0B0E14; |
| color: #F1F5F9; |
| } |
| @keyframes ring-rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } |
| @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); } } |
| @keyframes glow-text { 0%, 100% { opacity: 0.7; } 50% { opacity: 1; } } |
| .orb-ring { animation: ring-rotate 10s linear infinite; } |
| .orb-ring-reverse { animation: ring-rotate 12s linear infinite reverse; } |
| .generator-container { animation: pulse-loader 5s infinite ease-in-out; } |
| .text-overlay { animation: glow-text 4s infinite ease-in-out; } |
| </style> |
| </head> |
| <body class="min-h-screen py-10 px-4"> |
| <div class="max-w-4xl mx-auto flex flex-col gap-6"> |
| |
| |
| <header class="relative text-center py-6 border-b border-gray-800/80"> |
| <div class="w-24 h-24 mx-auto mb-4 relative flex items-center justify-content-center"> |
| <div class="absolute inset-0 bg-orange-500/10 rounded-full blur-xl"></div> |
| <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"> |
| <span class="text-2xl">🐱</span> |
| </div> |
| <div class="absolute inset-0 border border-dashed border-orange-500/30 rounded-full orb-ring"></div> |
| <div class="absolute -inset-2 border border-dotted border-amber-400/20 rounded-full orb-ring-reverse"></div> |
| </div> |
| <h1 class="text-3xl font-extrabold bg-gradient-to-l from-orange-500 to-amber-400 bg-clip-text text-transparent mb-2">استودیو ساخت آواتار ویدیویی سخنگو</h1> |
| <p class="text-gray-400 text-sm">تبدیل تصویر به ویدیوی آواتار سخنگوی متحرک بر اساس فایل صوتی انتخابی</p> |
| </header> |
|
|
| |
| <div class="bg-gray-900/40 border border-gray-800 rounded-3xl p-6 backdrop-blur-xl"> |
| <h2 class="text-lg font-bold text-gray-200 mb-3 flex items-center gap-2"> |
| <span>🔍</span> پیگیری وضعیت درخواست با کد |
| </h2> |
| <div class="flex flex-col sm:flex-row gap-3"> |
| <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="کد پیگیری درخواست خود را وارد کنید (مثال: 8e756672-6efe...)" /> |
| <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"> |
| بررسی وضعیت |
| </button> |
| </div> |
| </div> |
|
|
| |
| <main class="bg-gray-900/60 border border-gray-800 rounded-3xl p-6 md:p-8 backdrop-blur-xl shadow-2xl"> |
| <form id="avatar-form" class="space-y-6"> |
| |
| |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> |
| |
| |
| <div> |
| <label class="block text-sm font-bold text-gray-300 mb-3 flex items-center gap-2"> |
| <span>🖼️</span> تصویر کاراکتر مرجع |
| </label> |
| <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"> |
| <input type="file" id="image-input" accept="image/*" class="hidden" /> |
| <div id="image-upload-prompt" class="space-y-2"> |
| <span class="text-4xl">📁</span> |
| <p class="text-sm text-gray-400 font-semibold">کلیک کنید یا تصویر را به این قسمت بکشید</p> |
| <p class="text-xs text-gray-500">فرمتهای مجاز: PNG, JPG, WEBP</p> |
| </div> |
| <img id="image-preview" class="hidden absolute inset-0 w-full h-full object-contain p-2" /> |
| <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 transition-colors">×</button> |
| </div> |
| </div> |
|
|
| |
| <div> |
| <label class="block text-sm font-bold text-gray-300 mb-3 flex items-center gap-2"> |
| <span>🎙️</span> فایل صوتی سخنرانی |
| </label> |
| <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"> |
| <input type="file" id="audio-input" accept="audio/*" class="hidden" /> |
| <div id="audio-upload-prompt" class="space-y-2"> |
| <span class="text-4xl">🎵</span> |
| <p class="text-sm text-gray-400 font-semibold">کلیک کنید یا فایل صوتی را به این قسمت بکشید</p> |
| <p class="text-xs text-gray-500">فرمتهای مجاز: MP3, WAV, M4A</p> |
| </div> |
| <div id="audio-player-wrapper" class="hidden w-full px-4 space-y-2"> |
| <span class="text-3xl">🔊</span> |
| <p id="audio-name" class="text-xs text-gray-400 font-semibold truncate"></p> |
| <audio id="audio-preview" controls class="w-full h-8 mt-2"></audio> |
| </div> |
| <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 transition-colors">×</button> |
| </div> |
| </div> |
|
|
| </div> |
|
|
| |
| <div> |
| <label for="prompt-input" class="block text-sm font-bold text-gray-300 mb-2 flex items-center gap-2"> |
| <span>✏️</span> توصیف حرکت و رفتار کاراکتر (به انگلیسی) |
| </label> |
| <textarea id="prompt-input" rows="3" class="w-full bg-gray-950/60 border border-gray-800 hover:border-gray-700 focus:border-orange-500 rounded-xl p-3 text-sm focus:ring-2 focus:ring-orange-500/20 outline-none transition-all resize-none text-gray-100" placeholder="توصیف جزییات رفتار آواتار...">A person is speaking expressively, looking at the camera.</textarea> |
| </div> |
|
|
| |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-6"> |
| |
| |
| <div> |
| <label for="resolution-input" class="block text-sm font-semibold text-gray-400 mb-2">کیفیت ویدیو (Resolution)</label> |
| <select id="resolution-input" class="w-full bg-gray-950 border border-gray-800 focus:border-orange-500 rounded-xl p-3 text-sm text-gray-300 outline-none"> |
| <option value="480p" selected>480p</option> |
| <option value="720p">720p</option> |
| </select> |
| </div> |
|
|
| |
| <div> |
| <label for="seed-input" class="block text-sm font-semibold text-gray-400 mb-2">هسته پردازش (Seed)</label> |
| <input type="number" id="seed-input" value="42" class="w-full bg-gray-950 border border-gray-800 focus:border-orange-500 rounded-xl p-3 text-sm text-gray-300 outline-none" /> |
| </div> |
|
|
| |
| <div> |
| <label for="vocal-mode-input" class="block text-sm font-semibold text-gray-400 mb-2">پیشپردازش صدا (Vocal Mode)</label> |
| <select id="vocal-mode-input" class="w-full bg-gray-950 border border-gray-800 focus:border-orange-500 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> |
| </div> |
|
|
| </div> |
|
|
| |
| <div> |
| <label for="acceleration-input" class="block text-sm font-semibold text-gray-400 mb-2">سیستم شتابدهنده سختافزاری</label> |
| <select id="acceleration-input" class="w-full bg-gray-950 border border-gray-800 focus:border-orange-500 rounded-xl p-3 text-sm text-gray-300 outline-none"> |
| <option value="DBCache faster" selected>DBCache faster (35% سریعتر)</option> |
| <option value="DBCache fast">DBCache fast</option> |
| <option value="Exact 8-step">Exact 8-step (بسیار دقیق)</option> |
| </select> |
| </div> |
|
|
| |
| <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 hover:to-amber-400 text-white font-extrabold rounded-xl shadow-lg shadow-orange-500/10 hover:shadow-orange-500/20 transform hover:-translate-y-0.5 transition-all flex items-center justify-center gap-2 text-lg"> |
| <span>✨</span> نوبتدهی و ثبت سفارش آواتار |
| </button> |
| </form> |
|
|
| |
| <div id="status-box" class="hidden mt-8 border-t border-gray-800/80 pt-6 flex flex-col items-center"> |
| <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"> |
| <div class="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(255,107,53,0.15),transparent_70%)]"></div> |
| <div class="text-overlay text-gray-100 font-bold text-base z-10 space-y-4" id="status-text">آپلود و آمادهسازی فایلها...</div> |
| <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> |
| </div> |
| </div> |
|
|
| |
| <div id="result-box" class="hidden mt-8 border-t border-gray-800/80 pt-6 space-y-4 flex flex-col items-center"> |
| <div id="tracking-info-header" class="text-center font-semibold text-orange-400 text-sm"></div> |
| |
| <div id="video-display-wrapper" class="hidden w-full max-w-[500px] rounded-2xl overflow-hidden border border-gray-800 shadow-2xl"> |
| <video id="output-video" controls class="w-full bg-black"></video> |
| </div> |
|
|
| |
| <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"> |
| <p class="text-red-400 font-bold mb-3">⚠️ تمام تلاشها ناموفق بود و ساخت آواتار لغو شد</p> |
| <img id="error-card-img" class="w-full rounded-xl border border-red-500/20 shadow-lg" src="" alt="Error Card" /> |
| </div> |
|
|
| <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 hover:scale-[1.02] active:scale-[0.98] transition-all"> |
| <span>📥</span> دانلود مستقیم ویدیو آواتار |
| </button> |
| </div> |
|
|
| </main> |
|
|
| |
| <section class="bg-gray-900/40 border border-gray-800 rounded-3xl p-6 backdrop-blur-xl"> |
| <h2 class="text-lg font-bold text-gray-200 mb-4 flex items-center gap-2"> |
| <span>📋</span> لیست درخواستهای ثبت شده شما |
| </h2> |
| <div id="history-list" class="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| |
| </div> |
| </section> |
|
|
| </div> |
|
|
| <script> |
| |
| const dbName = 'AvatarAppDB'; |
| const storeName = 'AvatarQueue'; |
| let db; |
| |
| function initDB() { |
| return new Promise((resolve, reject) => { |
| const request = indexedDB.open(dbName, 1); |
| request.onerror = (e) => reject(e.target.error); |
| request.onsuccess = (e) => { |
| db = e.target.result; |
| resolve(db); |
| }; |
| request.onupgradeneeded = (e) => { |
| db = e.target.result; |
| if (!db.objectStoreNames.contains(storeName)) { |
| db.createObjectStore(storeName, { keyPath: 'id' }); |
| } |
| }; |
| }); |
| } |
| |
| function saveLocalRequest(id, prompt, imageBase64, status = 'processing', url = '') { |
| return new Promise((resolve, reject) => { |
| const transaction = db.transaction([storeName], 'readwrite'); |
| const store = transaction.objectStore(storeName); |
| const item = { |
| id: id, |
| prompt: prompt, |
| image: imageBase64, |
| timestamp: Date.now(), |
| status: status, |
| url: url |
| }; |
| const req = store.put(item); |
| req.onsuccess = () => resolve(); |
| req.onerror = () => reject(req.error); |
| }); |
| } |
| |
| function getLocalRequest(id) { |
| return new Promise((resolve, reject) => { |
| const transaction = db.transaction([storeName], 'readonly'); |
| const store = transaction.objectStore(storeName); |
| const req = store.get(id); |
| req.onsuccess = () => resolve(req.result); |
| req.onerror = () => reject(req.error); |
| }); |
| } |
| |
| function getAllLocalRequests() { |
| return new Promise((resolve, reject) => { |
| const transaction = db.transaction([storeName], 'readonly'); |
| const store = transaction.objectStore(storeName); |
| const req = store.getAll(); |
| req.onsuccess = () => resolve(req.result); |
| req.onerror = () => reject(req.error); |
| }); |
| } |
| |
| function deleteLocalRequest(id) { |
| return new Promise((resolve, reject) => { |
| const transaction = db.transaction([storeName], 'readwrite'); |
| const store = transaction.objectStore(storeName); |
| const req = store.delete(id); |
| req.onsuccess = () => resolve(); |
| req.onerror = () => reject(req.error); |
| }); |
| } |
| |
| |
| const form = document.getElementById('avatar-form'); |
| const submitBtn = document.getElementById('submit-btn'); |
| const statusBox = document.getElementById('status-box'); |
| const statusText = document.getElementById('status-text'); |
| const progressBar = document.getElementById('progress-bar'); |
| const resultBox = document.getElementById('result-box'); |
| const videoDisplayWrapper = document.getElementById('video-display-wrapper'); |
| const outputVideo = document.getElementById('output-video'); |
| const downloadBtn = document.getElementById('download-btn'); |
| const errorCardDisplay = document.getElementById('error-card-display'); |
| const errorCardImg = document.getElementById('error-card-img'); |
| const trackingInfoHeader = document.getElementById('tracking-info-header'); |
| |
| const trackCodeInput = document.getElementById('track-code-input'); |
| const btnTrackSubmit = document.getElementById('btn-track-submit'); |
| const historyList = document.getElementById('history-list'); |
| |
| |
| const imageDropzone = document.getElementById('image-dropzone'); |
| const imageInput = document.getElementById('image-input'); |
| const imagePreview = document.getElementById('image-preview'); |
| const imagePrompt = document.getElementById('image-upload-prompt'); |
| const removeImage = document.getElementById('remove-image'); |
| |
| let cachedImageBase64 = ''; |
| |
| imageDropzone.onclick = (e) => { if (e.target !== removeImage && !removeImage.contains(e.target)) imageInput.click(); }; |
| imageInput.onchange = () => handleImageSelect(imageInput.files[0]); |
| imageDropzone.ondragover = (e) => { e.preventDefault(); imageDropzone.classList.add('border-orange-500'); }; |
| imageDropzone.ondragleave = () => imageDropzone.classList.remove('border-orange-500'); |
| imageDropzone.ondrop = (e) => { e.preventDefault(); imageDropzone.classList.remove('border-orange-500'); if (e.dataTransfer.files[0]) handleImageSelect(e.dataTransfer.files[0]); }; |
| |
| function handleImageSelect(file) { |
| if (!file) return; |
| const reader = new FileReader(); |
| reader.onload = (e) => { |
| imagePreview.src = e.target.result; |
| cachedImageBase64 = e.target.result; |
| imagePreview.classList.remove('hidden'); |
| imagePrompt.classList.add('hidden'); |
| removeImage.classList.remove('hidden'); |
| }; |
| reader.readAsDataURL(file); |
| } |
| |
| removeImage.onclick = (e) => { |
| e.stopPropagation(); |
| imageInput.value = ''; |
| cachedImageBase64 = ''; |
| imagePreview.src = ''; |
| imagePreview.classList.add('hidden'); |
| imagePrompt.classList.remove('hidden'); |
| removeImage.classList.add('hidden'); |
| }; |
| |
| |
| const audioDropzone = document.getElementById('audio-dropzone'); |
| const audioInput = document.getElementById('audio-input'); |
| const audioPreview = document.getElementById('audio-preview'); |
| const audioPrompt = document.getElementById('audio-upload-prompt'); |
| const removeAudio = document.getElementById('remove-audio'); |
| const audioName = document.getElementById('audio-name'); |
| const audioPlayerWrapper = document.getElementById('audio-player-wrapper'); |
| |
| audioDropzone.onclick = (e) => { if (e.target !== removeAudio && !removeAudio.contains(e.target) && !audioPreview.contains(e.target)) audioInput.click(); }; |
| audioInput.onchange = () => handleAudioSelect(audioInput.files[0]); |
| audioDropzone.ondragover = (e) => { e.preventDefault(); audioDropzone.classList.add('border-orange-500'); }; |
| audioDropzone.ondragleave = () => audioDropzone.classList.remove('border-orange-500'); |
| audioDropzone.ondrop = (e) => { e.preventDefault(); audioDropzone.classList.remove('border-orange-500'); if (e.dataTransfer.files[0]) handleAudioSelect(e.dataTransfer.files[0]); }; |
| |
| function handleAudioSelect(file) { |
| if (!file) return; |
| const fileURL = URL.createObjectURL(file); |
| audioPreview.src = fileURL; |
| audioName.textContent = file.name; |
| audioPlayerWrapper.classList.remove('hidden'); |
| audioPrompt.classList.add('hidden'); |
| removeAudio.classList.remove('hidden'); |
| } |
| |
| removeAudio.onclick = (e) => { |
| e.stopPropagation(); |
| audioInput.value = ''; |
| audioPreview.src = ''; |
| audioPlayerWrapper.classList.add('hidden'); |
| audioPrompt.classList.remove('hidden'); |
| removeAudio.classList.add('hidden'); |
| }; |
| |
| |
| function safeBase64Encode(str) { |
| return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) => { |
| return String.fromCharCode('0x' + p1); |
| })); |
| } |
| |
| async function uploadFileToDocker(file, suffix) { |
| const formData = new FormData(); |
| const ext = file.name.split('.').pop().toLowerCase(); |
| formData.append('run_id', `${currentRunId}_${suffix}`); |
| formData.append('ext', ext); |
| formData.append('file', file); |
| |
| const response = await fetch('/api/webhook/upload', { method: 'POST', body: formData }); |
| if (!response.ok) throw new Error("خطا در آپلود فایل."); |
| return ext; |
| } |
| |
| let currentRunId = ''; |
| let pollInterval = null; |
| let emptyActionCounter = 0; |
| |
| |
| async function renderHistoryList() { |
| const list = await getAllLocalRequests(); |
| historyList.innerHTML = ''; |
| |
| if (list.length === 0) { |
| historyList.innerHTML = '<div class="col-span-full text-center text-sm text-gray-500 py-4">هنوز هیچ درخواستی ثبت نکردهاید.</div>'; |
| return; |
| } |
| |
| |
| list.sort((a, b) => b.timestamp - a.timestamp); |
| |
| list.forEach(item => { |
| const dateStr = new Date(item.timestamp).toLocaleString('fa-IR'); |
| let badgeClass = 'bg-yellow-950/40 text-yellow-400 border border-yellow-900'; |
| let statusText = 'در حال پردازش ⏳'; |
| |
| if (item.status === 'completed') { |
| badgeClass = 'bg-emerald-950/40 text-emerald-400 border border-emerald-900'; |
| statusText = 'آماده شده 🎉'; |
| } else if (item.status === 'failed') { |
| badgeClass = 'bg-red-950/20 text-red-400 border border-red-900/50'; |
| statusText = 'ناموفق ⚠️'; |
| } |
| |
| const card = document.createElement('div'); |
| card.className = 'bg-gray-950/50 border border-gray-800 rounded-2xl p-4 flex gap-4 items-center justify-between'; |
| card.innerHTML = ` |
| <div class="flex gap-3 items-center min-w-0"> |
| <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" /> |
| <div class="min-w-0"> |
| <p class="text-xs text-gray-500 font-semibold mb-1">${dateStr}</p> |
| <p class="text-sm text-gray-200 font-bold truncate max-w-[200px]" dir="auto">${item.prompt}</p> |
| <p class="text-[10px] font-mono text-gray-400 mt-1 truncate max-w-[200px]">کد: ${item.id}</p> |
| </div> |
| </div> |
| <div class="flex flex-col items-end gap-2"> |
| <span class="px-2 py-1 text-xs font-bold rounded ${badgeClass}">${statusText}</span> |
| <div class="flex gap-2"> |
| <button onclick="checkSpecificCode('${item.id}')" class="text-xs bg-orange-600/20 hover:bg-orange-600 border border-orange-500/30 text-orange-400 hover:text-white px-2.5 py-1.5 rounded-lg font-semibold transition-all">بررسی</button> |
| <button onclick="handleDeleteHistoryItem('${item.id}')" class="text-xs bg-red-950/20 hover:bg-red-600 border border-red-900/30 text-red-400 hover:text-white px-2 py-1.5 rounded-lg font-semibold transition-all">✕</button> |
| </div> |
| </div> |
| `; |
| historyList.appendChild(card); |
| }); |
| } |
| |
| window.handleDeleteHistoryItem = async function(id) { |
| if (confirm("آیا از حذف این درخواست از حافظه مرورگر اطمینان دارید؟")) { |
| await deleteLocalRequest(id); |
| renderHistoryList(); |
| } |
| } |
| |
| |
| form.addEventListener('submit', async (e) => { |
| e.preventDefault(); |
| |
| const imageFile = imageInput.files[0]; |
| const audioFile = audioInput.files[0]; |
| const prompt = document.getElementById('prompt-input').value.trim(); |
| const resolution = document.getElementById('resolution-input').value; |
| const seed = document.getElementById('seed-input').value; |
| const vocalMode = document.getElementById('vocal-mode-input').value; |
| const acceleration = document.getElementById('acceleration-input').value; |
| |
| if (!imageFile || !audioFile || !prompt) { |
| alert('لطفاً تصویر، فایل صوتی و توضیحات حرکت را کامل کنید.'); |
| return; |
| } |
| |
| submitBtn.disabled = true; |
| statusBox.classList.remove('hidden'); |
| resultBox.classList.add('hidden'); |
| progressBar.style.width = '0%'; |
| |
| currentRunId = 'avatar' + Math.random().toString(36).substring(2, 14); |
| |
| try { |
| statusText.innerHTML = "<div>۱/۴: در حال آپلود تصویر کاراکتر...</div>"; |
| progressBar.style.width = '15%'; |
| const imgExt = await uploadFileToDocker(imageFile, 'avatar_img'); |
| |
| statusText.innerHTML = "<div>۲/۴: در حال آپلود فایل صوتی سخنرانی...</div>"; |
| progressBar.style.width = '35%'; |
| const audExt = await uploadFileToDocker(audioFile, 'avatar_aud'); |
| |
| statusText.innerHTML = "<div>۳/۴: ثبت نوبت و ارسال سیگنال گیتهاب...</div>"; |
| progressBar.style.width = '55%'; |
| |
| const b64Prompt = safeBase64Encode(prompt); |
| const vocalSlug = vocalMode.includes('Clean') ? 'clean' : 'isolate'; |
| let accelSlug = 'dbfaster'; |
| if (acceleration.includes('DBCache fast')) accelSlug = 'dbfast'; |
| else if (acceleration.includes('Exact')) accelSlug = 'exact8'; |
| |
| const avatarConfigPayload = `AVATARCONFIG_userRunId_${currentRunId}_imgExt_${imgExt}_audExt_${audExt}_res_${resolution}_seed_${seed}_vocal_${vocalSlug}_accel_${accelSlug}_prompt_${b64Prompt}`; |
| |
| const response = await fetch('/api/generate', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| prompt: avatarConfigPayload, |
| width: 1024, |
| height: 1024, |
| action_name: 'avatar' |
| }) |
| }); |
| |
| const data = await response.json(); |
| |
| if (data.status === 'success') { |
| |
| await saveLocalRequest(data.run_id, prompt, cachedImageBase64, 'processing'); |
| await renderHistoryList(); |
| |
| |
| progressBar.style.width = '100%'; |
| statusText.innerHTML = ` |
| <div class="text-green-400 font-bold mb-2">🎉 درخواست با موفقیت ثبت شد!</div> |
| <div class="text-sm text-gray-300">درخواست شما به صف گیتهاب اکشن منتقل شد.</div> |
| <div class="bg-gray-950 p-2.5 rounded-xl border border-gray-800/80 my-3 select-all font-mono text-xs text-orange-400 font-bold cursor-pointer" title="برای کپی کلیک کنید">${data.run_id}</div> |
| <div class="text-[11px] text-gray-400">میتوانید این صفحه را ببندید و در زمانهای بعدی با کد فوق وضعیت درخواست را پیگیری کنید.</div> |
| `; |
| |
| |
| submitBtn.disabled = false; |
| } else { |
| throw new Error(data.message || 'خطا در ارتباط با وبهوک گیتهاب.'); |
| } |
| |
| } catch (err) { |
| statusText.innerHTML = `<span class="text-red-500">خطا: ${err.message}</span>`; |
| progressBar.style.width = '0%'; |
| submitBtn.disabled = false; |
| } |
| }); |
| |
| |
| btnTrackSubmit.addEventListener('click', () => { |
| const code = trackCodeInput.value.trim(); |
| if (!code) { alert('لطفاً کد پیگیری را وارد کنید.'); return; } |
| checkSpecificCode(code); |
| }); |
| |
| |
| window.checkSpecificCode = function(code) { |
| if (pollInterval) clearInterval(pollInterval); |
| |
| |
| statusBox.classList.add('hidden'); |
| resultBox.classList.remove('hidden'); |
| videoDisplayWrapper.classList.add('hidden'); |
| errorCardDisplay.classList.add('hidden'); |
| downloadBtn.classList.add('hidden'); |
| emptyActionCounter = 0; |
| |
| trackingInfoHeader.innerHTML = `در حال مانیتورینگ آنلاین کد: <span class="font-mono text-orange-400 font-bold">${code}</span>`; |
| |
| startPolling(code); |
| } |
| |
| |
| function startPolling(runId) { |
| if (pollInterval) clearInterval(pollInterval); |
| emptyActionCounter = 0; |
| |
| pollInterval = setInterval(async () => { |
| try { |
| |
| try { |
| const mp4Check = await fetch(`/static/images/${runId}.mp4`, { method: 'HEAD' }); |
| if (mp4Check.ok) { |
| |
| clearInterval(pollInterval); |
| |
| errorCardDisplay.classList.add('hidden'); |
| outputVideo.src = `/static/images/${runId}.mp4`; |
| videoDisplayWrapper.classList.remove('hidden'); |
| downloadBtn.classList.remove('hidden'); |
| |
| downloadBtn.onclick = () => { |
| parent.postMessage({ |
| type: 'DOWNLOAD_REQUEST', |
| url: window.location.origin + `/static/images/${runId}.mp4` |
| }, '*'); |
| }; |
| |
| const localData = await getLocalRequest(runId); |
| if (localData) { |
| await saveLocalRequest(runId, localData.prompt, localData.image, 'completed', `/static/images/${runId}.mp4`); |
| renderHistoryList(); |
| } |
| return; |
| } |
| } catch(e) {} |
| |
| |
| const activeRes = await fetch('/api/actions/active'); |
| const activeData = await activeRes.json(); |
| const hasActiveActions = activeData.status === 'success' && activeData.actions.length > 0; |
| |
| |
| const statusRes = await fetch(`/api/status/${runId}`); |
| const statusData = await statusRes.json(); |
| |
| if (statusData.status === 'ready') { |
| const urlLower = statusData.url.toLowerCase(); |
| if (urlLower.endsWith('.png') || urlLower.endsWith('.jpg') || urlLower.endsWith('.jpeg')) { |
| |
| |
| if (hasActiveActions) { |
| emptyActionCounter = 0; |
| trackingInfoHeader.innerHTML = `کد پیگیری: <span class="font-mono text-orange-400 font-bold">${runId}</span><br><span class="text-xs text-yellow-400">تلاش رانر قبلی به مشکل خورد. در صف تلاش با اکشن موازی جدید...</span>`; |
| return; |
| } else { |
| |
| emptyActionCounter++; |
| if (emptyActionCounter < 4) { |
| trackingInfoHeader.innerHTML = `کد پیگیری: <span class="font-mono text-orange-400 font-bold">${runId}</span><br><span class="text-xs text-yellow-400">در حال راهاندازی و تخصیص رانر جدید گیتهاب (لطفا صبور باشید)...</span>`; |
| return; |
| } |
| } |
| |
| |
| clearInterval(pollInterval); |
| videoDisplayWrapper.classList.add('hidden'); |
| errorCardImg.src = statusData.url; |
| errorCardDisplay.classList.remove('hidden'); |
| downloadBtn.classList.add('hidden'); |
| |
| const localData = await getLocalRequest(runId); |
| if (localData) { |
| await saveLocalRequest(runId, localData.prompt, localData.image, 'failed', statusData.url); |
| renderHistoryList(); |
| } |
| } |
| } else { |
| |
| if (hasActiveActions) { |
| emptyActionCounter = 0; |
| trackingInfoHeader.innerHTML = `کد پیگیری: <span class="font-mono text-orange-400 font-bold">${runId}</span><br><span class="text-xs text-blue-400 animate-pulse">در حال رندر و مدلسازی در رانر فعال...</span>`; |
| } else { |
| |
| trackingInfoHeader.innerHTML = `کد پیگیری: <span class="font-mono text-orange-400 font-bold">${runId}</span><br><span class="text-xs text-gray-500">در انتظار راهاندازی رانر گیتهاب...</span>`; |
| } |
| } |
| } catch (e) { |
| console.error("Error polling:", e); |
| } |
| }, 4000); |
| } |
| |
| |
| document.addEventListener('DOMContentLoaded', async () => { |
| try { |
| await initDB(); |
| await renderHistoryList(); |
| } catch (e) { |
| console.error("Failed to load IndexedDB:", e); |
| } |
| }); |
| </script> |
| </body> |
| </html> |