tts10 / templates /avatar.html
Opera8's picture
Update templates/avatar.html
fac4df2 verified
Raw
History Blame Contribute Delete
39.1 kB
<!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 -->
<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>
<!-- Tracking System Block -->
<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 Form & Dashboard -->
<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">
<!-- Media Upload Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Reference Image Upload -->
<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">&times;</button>
</div>
</div>
<!-- Audio Voice Upload -->
<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">&times;</button>
</div>
</div>
</div>
<!-- Text Prompt Behaviour -->
<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>
<!-- Generation Settings Grid -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Resolution selection -->
<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>
<!-- Numeric Seed -->
<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>
<!-- Audio Pre-Processing vocal mode -->
<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>
<!-- Accelerator selection -->
<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>
<!-- Submit Trigger button -->
<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>
<!-- Loading & Notification Panel -->
<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>
<!-- Result Box -->
<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>
<!-- History Dashboard (بخش درخواست‌های من) -->
<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>
// --- تنظیمات دیتابیس لوکال مرورگر (IndexedDB) ---
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 {
// گپ زمانی بین وب‌هوک و ساخته شدن اکشن جدید در گیت‌هاب (حدود 15 تا 20 ثانیه)
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;
}
}
// اتمام نهایی تمام ۲۰ فرصت با خطا (بعد از 4 بار چک کردن متوالی و نبود هیچ اکشن فعالی)
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>