Update templates/video.html
Browse files- templates/video.html +103 -20
templates/video.html
CHANGED
|
@@ -139,6 +139,12 @@
|
|
| 139 |
#generateButton:hover:not(:disabled) { transform: translateY(-5px) scale(1.02); box-shadow: 0 8px 20px -4px var(--accent-primary-glow); }
|
| 140 |
#generateButton:disabled { background: var(--text-tertiary); cursor: not-allowed; box-shadow: none; opacity: 0.7; }
|
| 141 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
#results-container { min-height: 250px; position: relative; padding: 1rem; background-color: var(--input-bg); border-radius: var(--radius-card); border: 2px dashed var(--input-border); box-shadow: var(--shadow-sm) inset; transition: var(--transition-smooth); display: flex; flex-direction: column; align-items: center; justify-content: center; }
|
| 143 |
#statusSection { display: none; width: 100%; }
|
| 144 |
#statusSection.active { display: block; animation: fadeIn 0.5s; }
|
|
@@ -214,7 +220,8 @@
|
|
| 214 |
</div>
|
| 215 |
<h1>استودیو ویدیو هوش مصنوعی</h1>
|
| 216 |
<p class="subtitle">تصاویر خود را با قدرت هوش مصنوعی به ویدیو سینمایی تبدیل کنید</p>
|
| 217 |
-
<p class="instruction">ابتدا تصویر خود را
|
|
|
|
| 218 |
</div>
|
| 219 |
</header>
|
| 220 |
<main>
|
|
@@ -251,11 +258,14 @@
|
|
| 251 |
</div>
|
| 252 |
<div class="slider-wrapper">
|
| 253 |
<svg class="slider-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.72"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.72-1.72"></path></svg>
|
| 254 |
-
<input type="range" id="durationSlider" min="
|
| 255 |
<svg class="slider-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
|
| 256 |
</div>
|
| 257 |
</div>
|
| 258 |
|
|
|
|
|
|
|
|
|
|
| 259 |
<button id="generateButton">
|
| 260 |
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 3L12 8L17 10L12 12L10 17L8 12L3 10L8 8L10 3z"/></svg>
|
| 261 |
<span>ساخت ویدیو جادویی</span>
|
|
@@ -390,9 +400,8 @@
|
|
| 390 |
</div>
|
| 391 |
</div>
|
| 392 |
|
| 393 |
-
|
| 394 |
<script>
|
| 395 |
-
// دیتابیس حافظه مرورگر برای ذخیره سوابق
|
| 396 |
const projectDB = {
|
| 397 |
db: null,
|
| 398 |
init() { return new Promise((resolve, reject) => { const request = indexedDB.open('AIStudioDB_V2', 1); request.onupgradeneeded = event => { const db = event.target.result; if (!db.objectStoreNames.contains('projectState')) { db.createObjectStore('projectState'); } }; request.onsuccess = event => { this.db = event.target.result; resolve(); }; request.onerror = event => { console.error('IndexedDB error:', event.target.errorCode); reject(event.target.errorCode); }; }); },
|
|
@@ -401,7 +410,52 @@
|
|
| 401 |
delete(key) { return new Promise((resolve, reject) => { if (!this.db) return reject("DB not initialized"); const transaction = this.db.transaction(['projectState'], 'readwrite'); const store = transaction.objectStore('projectState'); const request = store.delete(key); request.onsuccess = () => resolve(); request.onerror = (event) => reject(event.target.error); }); }
|
| 402 |
};
|
| 403 |
|
| 404 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
const mainControls = document.getElementById('main-controls');
|
| 406 |
const resultsContainer = document.getElementById('results-container');
|
| 407 |
const latestResultWrapper = document.getElementById('latest-result-wrapper');
|
|
@@ -447,7 +501,7 @@
|
|
| 447 |
const customReportSection = document.getElementById('custom-report-section');
|
| 448 |
const reportSuccessMessage = document.getElementById('report-success-message');
|
| 449 |
|
| 450 |
-
// State Variables
|
| 451 |
let currentImageFile = null, currentImageObjectURL = null, lastAttemptedPayload = null, pollTimer = null;
|
| 452 |
let videoBlobs = new Map();
|
| 453 |
let originalUserPromptForExtension = '';
|
|
@@ -688,21 +742,21 @@
|
|
| 688 |
}
|
| 689 |
}
|
| 690 |
|
| 691 |
-
// اگر مایل باشید ادغام ویدیوها در بکاند انجام شود، این تابع فرضی باقی میماند
|
| 692 |
-
// در غیر این صورت فقط ویدیو جدید را ذخیره میکنیم
|
| 693 |
async function mergeVideosOnServer(baseVideoBlob, newClipBlob) {
|
| 694 |
try {
|
| 695 |
-
setAiLoaderText("میکس ویدیوها در سرور..."); setProgressBar(95); addStatusMessage('ارسال ویدیوها برای میکس در
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
|
|
|
|
|
|
| 700 |
|
| 701 |
-
if (response
|
| 702 |
return await response.blob();
|
| 703 |
} else {
|
| 704 |
-
|
| 705 |
-
|
| 706 |
}
|
| 707 |
} catch (error) {
|
| 708 |
showCriticalError(`خطا در میکس ویدیو در سرور: ${error.message}`);
|
|
@@ -726,6 +780,12 @@
|
|
| 726 |
} else {
|
| 727 |
finalBlob = newClipBlob;
|
| 728 |
infoText = `ویدیو اولیه`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 729 |
}
|
| 730 |
|
| 731 |
const existingLatestCard = latestResultWrapper.querySelector('.video-result-card');
|
|
@@ -807,7 +867,7 @@
|
|
| 807 |
addStatusMessage(`دستور شما ترجمه شد: ${data.translated}`, 'success');
|
| 808 |
}
|
| 809 |
setProgressBar(30);
|
| 810 |
-
setAiLoaderText('۲/۳: در حال
|
| 811 |
|
| 812 |
let progressValue = 30;
|
| 813 |
pollTimer = setInterval(async () => {
|
|
@@ -827,7 +887,7 @@
|
|
| 827 |
await handleVideoResult(newClipBlob, data.run_id);
|
| 828 |
} else {
|
| 829 |
if (progressValue < 90) {
|
| 830 |
-
progressValue +=
|
| 831 |
setProgressBar(progressValue);
|
| 832 |
}
|
| 833 |
}
|
|
@@ -855,7 +915,6 @@
|
|
| 855 |
if (!videoElement) { showCriticalError("ویدیوی مرجع برای گرفتن فریم آخر پیدا نشد!"); return; }
|
| 856 |
addStatusMessage('در حال گرفتن آخرین فریم از ویدیو...', 'info');
|
| 857 |
try {
|
| 858 |
-
// تبدیل Blob به File تا با سرور سازگارتر باشد
|
| 859 |
const rawBlob = await captureLastFrame(videoElement);
|
| 860 |
imageToProcess = new File([rawBlob], "last_frame.png", { type: "image/png" });
|
| 861 |
} catch (e) {
|
|
@@ -880,6 +939,24 @@
|
|
| 880 |
}
|
| 881 |
|
| 882 |
lastAttemptedPayload = { imageFile: imageToProcess, userPrompt: promptToUse, duration: parseFloat(durationSlider.value), isExtension: isExtensionRequest, baseVideoBlob, clipCount };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 883 |
await proceedWithVideoGeneration();
|
| 884 |
}
|
| 885 |
|
|
@@ -950,7 +1027,13 @@
|
|
| 950 |
} catch(error) { console.error("Could not load project from DB:", error); }
|
| 951 |
}
|
| 952 |
|
| 953 |
-
document.addEventListener('DOMContentLoaded', async () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 954 |
durationSlider.addEventListener('input', () => { const value = parseFloat(durationSlider.value); durationValueDisplay.textContent = `${value.toFixed(1)} ثانیه`; updateSliderStyle(durationSlider); });
|
| 955 |
|
| 956 |
function captureFrameAtTime(videoElement, time) {
|
|
|
|
| 139 |
#generateButton:hover:not(:disabled) { transform: translateY(-5px) scale(1.02); box-shadow: 0 8px 20px -4px var(--accent-primary-glow); }
|
| 140 |
#generateButton:disabled { background: var(--text-tertiary); cursor: not-allowed; box-shadow: none; opacity: 0.7; }
|
| 141 |
|
| 142 |
+
#subscription-status-badge { display: none; padding: 6px 16px; border-radius: 20px; font-size: 0.9em; font-weight: 700; margin-top: 1.25rem; letter-spacing: 0.5px; animation: badge-fade-in 0.6s 0.5s ease-out backwards; }
|
| 143 |
+
#subscription-status-badge.free-badge { background: linear-gradient(45deg, #6c757d, #495057); color: white; box-shadow: 0 4px 10px rgba(108, 117, 125, 0.3); }
|
| 144 |
+
#subscription-status-badge.paid-badge { background: linear-gradient(45deg, var(--accent-premium), #ffca2c); color: #333; box-shadow: 0 4px 10px var(--accent-premium-glow); }
|
| 145 |
+
#credit-info-section { margin-top: 1.5rem; margin-bottom: 0.5rem; padding: 1rem; background-color: var(--input-bg); border-radius: var(--radius-input); text-align: center; font-size: 0.95rem; font-weight: 500; color: var(--text-secondary); border: 1px solid var(--input-border); display: none; }
|
| 146 |
+
#upgrade-button { margin-top: 1rem; width: 100%; padding: 1rem; font-size: 1.1rem; font-weight: 700; background: linear-gradient(95deg, #FFD54F, #FFC107 100%); color: #212529; border: none; border-radius: var(--radius-btn); cursor: pointer; box-shadow: 0 8px 20px -5px rgba(255, 193, 7, 0.3); display: none; }
|
| 147 |
+
|
| 148 |
#results-container { min-height: 250px; position: relative; padding: 1rem; background-color: var(--input-bg); border-radius: var(--radius-card); border: 2px dashed var(--input-border); box-shadow: var(--shadow-sm) inset; transition: var(--transition-smooth); display: flex; flex-direction: column; align-items: center; justify-content: center; }
|
| 149 |
#statusSection { display: none; width: 100%; }
|
| 150 |
#statusSection.active { display: block; animation: fadeIn 0.5s; }
|
|
|
|
| 220 |
</div>
|
| 221 |
<h1>استودیو ویدیو هوش مصنوعی</h1>
|
| 222 |
<p class="subtitle">تصاویر خود را با قدرت هوش مصنوعی به ویدیو سینمایی تبدیل کنید</p>
|
| 223 |
+
<p class="instruction">ابتدا تصویر خود را تولید کرده و اینجا آپلود کنید تا ویدیوی خود را بسازید.</p>
|
| 224 |
+
<div id="subscription-status-badge" style="display:inline-block;"></div>
|
| 225 |
</div>
|
| 226 |
</header>
|
| 227 |
<main>
|
|
|
|
| 258 |
</div>
|
| 259 |
<div class="slider-wrapper">
|
| 260 |
<svg class="slider-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.72"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.72-1.72"></path></svg>
|
| 261 |
+
<input type="range" id="durationSlider" min="1.0" max="5.0" value="5.0" step="1.0">
|
| 262 |
<svg class="slider-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
|
| 263 |
</div>
|
| 264 |
</div>
|
| 265 |
|
| 266 |
+
<div id="credit-info-section"></div>
|
| 267 |
+
<button id="upgrade-button">⭐️ ارتقا به نسخه کامل و نامحدود</button>
|
| 268 |
+
|
| 269 |
<button id="generateButton">
|
| 270 |
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 3L12 8L17 10L12 12L10 17L8 12L3 10L8 8L10 3z"/></svg>
|
| 271 |
<span>ساخت ویدیو جادویی</span>
|
|
|
|
| 400 |
</div>
|
| 401 |
</div>
|
| 402 |
|
|
|
|
| 403 |
<script>
|
| 404 |
+
// --- دیتابیس حافظه مرورگر برای ذخیره سوابق ---
|
| 405 |
const projectDB = {
|
| 406 |
db: null,
|
| 407 |
init() { return new Promise((resolve, reject) => { const request = indexedDB.open('AIStudioDB_V2', 1); request.onupgradeneeded = event => { const db = event.target.result; if (!db.objectStoreNames.contains('projectState')) { db.createObjectStore('projectState'); } }; request.onsuccess = event => { this.db = event.target.result; resolve(); }; request.onerror = event => { console.error('IndexedDB error:', event.target.errorCode); reject(event.target.errorCode); }; }); },
|
|
|
|
| 410 |
delete(key) { return new Promise((resolve, reject) => { if (!this.db) return reject("DB not initialized"); const transaction = this.db.transaction(['projectState'], 'readwrite'); const store = transaction.objectStore('projectState'); const request = store.delete(key); request.onsuccess = () => resolve(); request.onerror = (event) => reject(event.target.error); }); }
|
| 411 |
};
|
| 412 |
|
| 413 |
+
// --- سیستم احراز و اعتبار ---
|
| 414 |
+
let userSubscriptionStatus = 'free', userFingerprint = null, countdownInterval = null; const PREMIUM_PAGE_ID = '1149636';
|
| 415 |
+
async function getBrowserFingerprint() { const components = [navigator.userAgent, navigator.language, screen.width + 'x' + screen.height, new Date().getTimezoneOffset(), "canvas-test"]; const fingerprintString = components.join('---'); let hash = 0; for (let i = 0; i < fingerprintString.length; i++) { hash = ((hash << 5) - hash) + fingerprintString.charCodeAt(i); hash |= 0; } return 'fp_' + Math.abs(hash).toString(16); }
|
| 416 |
+
function isUserPaid(userObject) { return userObject?.isLogin && userObject.accessible_pages?.some(p => p == PREMIUM_PAGE_ID); }
|
| 417 |
+
function updateUIForSubscriptionStatus(status) {
|
| 418 |
+
userSubscriptionStatus = status; const creditInfoDiv = document.getElementById('credit-info-section'); const upgradeBtn = document.getElementById('upgrade-button'); const genBtn = document.getElementById('generateButton'); const subscriptionBadge = document.getElementById('subscription-status-badge');
|
| 419 |
+
if (status === 'paid') {
|
| 420 |
+
subscriptionBadge.textContent = 'نسخه نامحدود'; subscriptionBadge.className = 'paid-badge'; creditInfoDiv.style.display = 'none'; upgradeBtn.style.display = 'none'; genBtn.disabled = false; if(countdownInterval) clearInterval(countdownInterval);
|
| 421 |
+
} else {
|
| 422 |
+
subscriptionBadge.textContent = 'نسخه رایگان'; subscriptionBadge.className = 'free-badge'; checkFreeUserCredit();
|
| 423 |
+
}
|
| 424 |
+
subscriptionBadge.style.display = 'inline-block';
|
| 425 |
+
}
|
| 426 |
+
async function checkFreeUserCredit() {
|
| 427 |
+
if (!userFingerprint) return; const creditInfoDiv = document.getElementById('credit-info-section'); const upgradeBtn = document.getElementById('upgrade-button'); const genBtn = document.getElementById('generateButton');
|
| 428 |
+
try {
|
| 429 |
+
const response = await fetch('/api/check-credit', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ fingerprint: userFingerprint }) });
|
| 430 |
+
const result = await response.json();
|
| 431 |
+
if (userSubscriptionStatus !== 'paid') { creditInfoDiv.style.display = 'block'; }
|
| 432 |
+
if (result.limit_reached) {
|
| 433 |
+
genBtn.disabled = true; upgradeBtn.style.display = 'block'; startCountdown(result.reset_timestamp);
|
| 434 |
+
} else {
|
| 435 |
+
genBtn.disabled = false; upgradeBtn.style.display = 'none'; if(countdownInterval) clearInterval(countdownInterval); creditInfoDiv.textContent = `شما ${result.credits_remaining} اعتبار ساخت ویدیوی رایگان در این هفته دارید.`;
|
| 436 |
+
}
|
| 437 |
+
} catch (error) { creditInfoDiv.textContent = "خطا در بررسی اعتبار."; genBtn.disabled = true; }
|
| 438 |
+
}
|
| 439 |
+
function startCountdown(resetTimestamp) {
|
| 440 |
+
if (countdownInterval) clearInterval(countdownInterval); const creditInfoDiv = document.getElementById('credit-info-section');
|
| 441 |
+
const updateTimer = () => {
|
| 442 |
+
const timeLeft = Math.max(0, resetTimestamp - (Date.now() / 1000));
|
| 443 |
+
if (timeLeft === 0) { clearInterval(countdownInterval); creditInfoDiv.textContent = 'در حال بروزرسانی اعتبار...'; setTimeout(checkFreeUserCredit, 2000); return; }
|
| 444 |
+
const d = Math.floor(timeLeft / 86400); const h = Math.floor((timeLeft % 86400) / 3600); const m = Math.floor((timeLeft % 3600) / 60);
|
| 445 |
+
creditInfoDiv.textContent = `اعتبار شما تمام شده. زمان باقیمانده: ${d} روز و ${h} ساعت و ${m} دقیقه`;
|
| 446 |
+
};
|
| 447 |
+
updateTimer(); countdownInterval = setInterval(updateTimer, 60000);
|
| 448 |
+
}
|
| 449 |
+
window.addEventListener('message', (event) => {
|
| 450 |
+
if (event.data?.type === 'USER_DATA_RESPONSE') {
|
| 451 |
+
if (event.data.error || !event.data.payload) { updateUIForSubscriptionStatus('free'); return; }
|
| 452 |
+
try { const userObject = JSON.parse(event.data.payload); updateUIForSubscriptionStatus(isUserPaid(userObject) ? 'paid' : 'free'); }
|
| 453 |
+
catch (e) { updateUIForSubscriptionStatus('free'); }
|
| 454 |
+
}
|
| 455 |
+
});
|
| 456 |
+
document.getElementById('upgrade-button').addEventListener('click', () => { parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM' }, '*'); });
|
| 457 |
+
|
| 458 |
+
// --- DOM Elements ---
|
| 459 |
const mainControls = document.getElementById('main-controls');
|
| 460 |
const resultsContainer = document.getElementById('results-container');
|
| 461 |
const latestResultWrapper = document.getElementById('latest-result-wrapper');
|
|
|
|
| 501 |
const customReportSection = document.getElementById('custom-report-section');
|
| 502 |
const reportSuccessMessage = document.getElementById('report-success-message');
|
| 503 |
|
| 504 |
+
// --- State Variables ---
|
| 505 |
let currentImageFile = null, currentImageObjectURL = null, lastAttemptedPayload = null, pollTimer = null;
|
| 506 |
let videoBlobs = new Map();
|
| 507 |
let originalUserPromptForExtension = '';
|
|
|
|
| 742 |
}
|
| 743 |
}
|
| 744 |
|
|
|
|
|
|
|
| 745 |
async function mergeVideosOnServer(baseVideoBlob, newClipBlob) {
|
| 746 |
try {
|
| 747 |
+
setAiLoaderText("میکس ویدیوها در سرور..."); setProgressBar(95); addStatusMessage('ارسال ویدیوها برای میکس در سرور...', 'info');
|
| 748 |
+
|
| 749 |
+
const formData = new FormData();
|
| 750 |
+
formData.append('base_video', baseVideoBlob, 'base.mp4');
|
| 751 |
+
formData.append('new_clip', newClipBlob, 'new.mp4');
|
| 752 |
+
|
| 753 |
+
const response = await fetch('/api/merge-videos', { method: 'POST', body: formData });
|
| 754 |
|
| 755 |
+
if (response.ok) {
|
| 756 |
return await response.blob();
|
| 757 |
} else {
|
| 758 |
+
const errText = await response.text();
|
| 759 |
+
throw new Error(errText);
|
| 760 |
}
|
| 761 |
} catch (error) {
|
| 762 |
showCriticalError(`خطا در میکس ویدیو در سرور: ${error.message}`);
|
|
|
|
| 780 |
} else {
|
| 781 |
finalBlob = newClipBlob;
|
| 782 |
infoText = `ویدیو اولیه`;
|
| 783 |
+
|
| 784 |
+
if (userSubscriptionStatus === 'free') {
|
| 785 |
+
addStatusMessage('ویدیو ساخته شد، در حال ثبت اعتبار...', 'info');
|
| 786 |
+
try { await fetch('/api/use-credit', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ fingerprint: userFingerprint }) }); checkFreeUserCredit(); }
|
| 787 |
+
catch (creditError) { console.error("Error deducting credit:", creditError); }
|
| 788 |
+
}
|
| 789 |
}
|
| 790 |
|
| 791 |
const existingLatestCard = latestResultWrapper.querySelector('.video-result-card');
|
|
|
|
| 867 |
addStatusMessage(`دستور شما ترجمه شد: ${data.translated}`, 'success');
|
| 868 |
}
|
| 869 |
setProgressBar(30);
|
| 870 |
+
setAiLoaderText('۲/۳: در حال ساخت ویدیو در گیتهاب (حدود ۲ تا ۵ دقیقه زمان میبرد)...');
|
| 871 |
|
| 872 |
let progressValue = 30;
|
| 873 |
pollTimer = setInterval(async () => {
|
|
|
|
| 887 |
await handleVideoResult(newClipBlob, data.run_id);
|
| 888 |
} else {
|
| 889 |
if (progressValue < 90) {
|
| 890 |
+
progressValue += 1;
|
| 891 |
setProgressBar(progressValue);
|
| 892 |
}
|
| 893 |
}
|
|
|
|
| 915 |
if (!videoElement) { showCriticalError("ویدیوی مرجع برای گرفتن فریم آخر پیدا نشد!"); return; }
|
| 916 |
addStatusMessage('در حال گرفتن آخرین فریم از ویدیو...', 'info');
|
| 917 |
try {
|
|
|
|
| 918 |
const rawBlob = await captureLastFrame(videoElement);
|
| 919 |
imageToProcess = new File([rawBlob], "last_frame.png", { type: "image/png" });
|
| 920 |
} catch (e) {
|
|
|
|
| 939 |
}
|
| 940 |
|
| 941 |
lastAttemptedPayload = { imageFile: imageToProcess, userPrompt: promptToUse, duration: parseFloat(durationSlider.value), isExtension: isExtensionRequest, baseVideoBlob, clipCount };
|
| 942 |
+
|
| 943 |
+
// چک کردن سیستم اعتبار برای کاربران رایگان قبل از شروع پردازش
|
| 944 |
+
if (userSubscriptionStatus !== 'paid') {
|
| 945 |
+
try {
|
| 946 |
+
const response = await fetch('/api/check-credit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fingerprint: userFingerprint }) });
|
| 947 |
+
const result = await response.json();
|
| 948 |
+
if (result.limit_reached) {
|
| 949 |
+
updateUIForSubscriptionStatus('free');
|
| 950 |
+
showCriticalError("محدودیت ساخت ویدیوی رایگان شما برای این هفته به پایان رسیده است.");
|
| 951 |
+
return;
|
| 952 |
+
}
|
| 953 |
+
} catch (err) {
|
| 954 |
+
const errorMessage = !navigator.onLine ? "عدم اتصال به اینترنت." : "خطا در سیستم اعتبار.";
|
| 955 |
+
showCriticalError(errorMessage);
|
| 956 |
+
return;
|
| 957 |
+
}
|
| 958 |
+
}
|
| 959 |
+
|
| 960 |
await proceedWithVideoGeneration();
|
| 961 |
}
|
| 962 |
|
|
|
|
| 1027 |
} catch(error) { console.error("Could not load project from DB:", error); }
|
| 1028 |
}
|
| 1029 |
|
| 1030 |
+
document.addEventListener('DOMContentLoaded', async () => {
|
| 1031 |
+
userFingerprint = await getBrowserFingerprint();
|
| 1032 |
+
parent.postMessage({ type: 'REQUEST_USER_DATA' }, '*');
|
| 1033 |
+
try { await projectDB.init(); await initializeForm(); await loadStoredProject(); }
|
| 1034 |
+
catch (error) { console.error("Failed to init DB or load project:", error); }
|
| 1035 |
+
});
|
| 1036 |
+
|
| 1037 |
durationSlider.addEventListener('input', () => { const value = parseFloat(durationSlider.value); durationValueDisplay.textContent = `${value.toFixed(1)} ثانیه`; updateSliderStyle(durationSlider); });
|
| 1038 |
|
| 1039 |
function captureFrameAtTime(videoElement, time) {
|