Sada8888 commited on
Commit
cb9c834
·
verified ·
1 Parent(s): 77c432f

Update templates/video.html

Browse files
Files changed (1) hide show
  1. 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">ابتدا تصویر خود را با فلاکس پرو تولید کنید و بعد تصویر خود را اینجا آپلود کرده و ویدیوی خود را بسازید.</p>
 
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="0.5" max="5.0" value="5.0" step="0.1">
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
- // DOM Elements
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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('ارسال ویدیوها برای میکس در مرورگر / سرور...', 'info');
696
- // از آنجا که API /api/merge-videos در کد شما نبود، فقط ویدیوی جدید را برمی‌گردانیم تا فعلاً ارور ندهد
697
- // شما می‌توانید در بک‌اند Flask خود این را اضافه کنید.
698
- const formData = new FormData(); formData.append('base_video', baseVideoBlob, 'base.mp4'); formData.append('new_clip', newClipBlob, 'new.mp4');
699
- const response = await fetch('/api/merge-videos', { method: 'POST', body: formData }).catch(()=>null);
 
 
700
 
701
- if (response && response.ok) {
702
  return await response.blob();
703
  } else {
704
- addStatusMessage('هشدار: سیستم میکس ویدیو روی سرور یافت نشد، فقط کلیپ جدید نمایش داده می‌شود.', 'error');
705
- return newClipBlob; // در صورت عدم وجود این API، کلیپ جدید جایگزین می‌شود
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 += 2;
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 () => { try { await projectDB.init(); await initializeForm(); await loadStoredProject(); } catch (error) { console.error("Failed to init DB or load project:", error); } });
 
 
 
 
 
 
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) {