File size: 39,059 Bytes
3a154e4 fac4df2 a153560 3a154e4 a153560 3a154e4 fac4df2 3a154e4 a153560 3a154e4 a153560 3a154e4 3c6ed88 3a154e4 a153560 3c6ed88 3a154e4 3c6ed88 a153560 b88cd52 fac4df2 3c6ed88 b88cd52 3c6ed88 3a154e4 a153560 3a154e4 a153560 3a154e4 3c6ed88 3a154e4 b88cd52 a153560 3a154e4 fac4df2 a153560 3a154e4 a153560 3a154e4 a153560 3a154e4 376db2f 3a154e4 a153560 3a154e4 a153560 fac4df2 3a154e4 fac4df2 3a154e4 a153560 fac4df2 a153560 3a154e4 b88cd52 3a154e4 a153560 3a154e4 a153560 3a154e4 a153560 3a154e4 b88cd52 3a154e4 b88cd52 3a154e4 b88cd52 3a154e4 a153560 3a154e4 a153560 3a154e4 a153560 fac4df2 a153560 fac4df2 3a154e4 fac4df2 3a154e4 fac4df2 3a154e4 fac4df2 27fe395 fac4df2 3a154e4 b88cd52 27fe395 fac4df2 27fe395 fac4df2 27fe395 fac4df2 27fe395 fac4df2 3c6ed88 b88cd52 3c6ed88 a153560 b88cd52 3c6ed88 fac4df2 27fe395 fac4df2 a153560 27fe395 fac4df2 27fe395 3a154e4 a153560 3a154e4 a153560 3a154e4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 | <!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">×</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">×</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> |