webolavo commited on
Commit
52fd7ac
·
verified ·
1 Parent(s): 0cfe6dd

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +147 -203
index.html CHANGED
@@ -28,7 +28,7 @@ body {
28
  overflow-x: hidden;
29
  }
30
 
31
- /* ─── Grid Background ─── */
32
  body::before {
33
  content: '';
34
  position: fixed;
@@ -49,7 +49,7 @@ body::before {
49
  z-index: 1;
50
  }
51
 
52
- /* ─── Header ─── */
53
  .header {
54
  text-align: center;
55
  margin-bottom: 48px;
@@ -83,7 +83,7 @@ body::before {
83
  font-weight: 300;
84
  }
85
 
86
- /* ─── API Status ─── */
87
  .api-status {
88
  display: flex;
89
  align-items: center;
@@ -109,7 +109,7 @@ body::before {
109
  50% { opacity: 0.7; box-shadow: 0 0 0 4px transparent; }
110
  }
111
 
112
- /* ─── Upload Zone ─── */
113
  .upload-zone {
114
  border: 1px dashed var(--border);
115
  border-radius: 8px;
@@ -158,7 +158,7 @@ body::before {
158
 
159
  #fileInput { display: none; }
160
 
161
- /* ─── Video Preview ─── */
162
  .video-preview {
163
  display: none;
164
  background: var(--surface);
@@ -189,7 +189,7 @@ body::before {
189
  .video-meta span { display: flex; align-items: center; gap: 6px; }
190
  .video-meta strong { color: var(--text); }
191
 
192
- /* ─── Upload Progress ─── */
193
  .upload-progress {
194
  display: none;
195
  margin-bottom: 24px;
@@ -220,7 +220,7 @@ body::before {
220
  box-shadow: 0 0 8px var(--accent);
221
  }
222
 
223
- /* ─── Action Buttons ─── */
224
  .actions {
225
  display: flex;
226
  gap: 12px;
@@ -280,7 +280,7 @@ body::before {
280
  transform: translateY(-2px);
281
  }
282
 
283
- /* ─── Timeline ─── */
284
  .timeline-section {
285
  display: none;
286
  margin-bottom: 32px;
@@ -416,7 +416,7 @@ body::before {
416
  .timeline-details .warn { color: var(--warn); }
417
  .timeline-details .danger { color: var(--accent2); }
418
 
419
- /* ─── Frame Log ─── */
420
  .frame-log {
421
  display: flex;
422
  flex-wrap: wrap;
@@ -444,7 +444,7 @@ body::before {
444
  background: rgba(255,51,102,0.05);
445
  }
446
 
447
- /* ─── Results ─── */
448
  .results-section {
449
  display: none;
450
  margin-bottom: 32px;
@@ -523,7 +523,7 @@ body::before {
523
  display: block;
524
  }
525
 
526
- /* ─── Video Timeline Bar ─── */
527
  .video-timeline {
528
  background: rgba(0,0,0,0.3);
529
  border-radius: 4px;
@@ -559,7 +559,7 @@ body::before {
559
  margin-top: 4px;
560
  }
561
 
562
- /* ─── Download ─── */
563
  .download-section {
564
  display: none;
565
  text-align: center;
@@ -583,7 +583,7 @@ body::before {
583
  margin-bottom: 24px;
584
  }
585
 
586
- /* ─── Spinner ─── */
587
  .spinner {
588
  width: 16px; height: 16px;
589
  border: 2px solid rgba(0,0,0,0.3);
@@ -595,7 +595,7 @@ body::before {
595
 
596
  @keyframes spin { to { transform: rotate(360deg); } }
597
 
598
- /* ─── Alert ─── */
599
  .alert {
600
  padding: 12px 16px;
601
  border-radius: 6px;
@@ -616,14 +616,14 @@ body::before {
616
  <!-- Header -->
617
  <div class="header">
618
  <div class="header-badge">AI VIDEO FILTER</div>
619
- <h1>تنقية <span>الفيديو</span> الإعلاني</h1>
620
- <p>إزالة مقاطع النساء تلقائياً باستخدام BLIP + Florence-2</p>
621
  </div>
622
 
623
  <!-- API Status -->
624
  <div class="api-status">
625
  <div class="status-dot" id="statusDot"></div>
626
- <span id="statusText" style="font-family:'IBM Plex Mono',monospace;font-size:12px;color:var(--muted)">جاري الاتصال بالـ API...</span>
627
  </div>
628
 
629
  <!-- Alert -->
@@ -631,9 +631,9 @@ body::before {
631
 
632
  <!-- Upload Zone -->
633
  <div class="upload-zone" id="uploadZone">
634
- <span class="upload-icon">🎬</span>
635
- <h3>اسحب الفيديو هنا أو اضغط للاختيار</h3>
636
- <p>MP4 / MOV / AVI · حد أقصى 200MB</p>
637
  <input type="file" id="fileInput" accept="video/*">
638
  </div>
639
 
@@ -646,7 +646,7 @@ body::before {
646
  <!-- Upload Progress -->
647
  <div class="upload-progress" id="uploadProgress">
648
  <div class="progress-label">
649
- <span id="progressLabel">جاري الرفع...</span>
650
  <span id="progressPct">0%</span>
651
  </div>
652
  <div class="progress-bar">
@@ -657,20 +657,20 @@ body::before {
657
  <!-- Action Buttons -->
658
  <div class="actions" id="actionsBar" style="display:none">
659
  <button class="btn btn-danger" id="btnQuickCheck" disabled>
660
- <span>🔍</span> فحص سريع
661
  </button>
662
  <button class="btn btn-primary" id="btnAnalyze" disabled>
663
- <span>⚙️</span> تحليل وتقطيع
664
  </button>
665
  <button class="btn btn-secondary" id="btnReset" onclick="resetAll()">
666
- <span></span> إعادة
667
  </button>
668
  </div>
669
 
670
  <!-- Timeline -->
671
  <div class="timeline-section" id="timelineSection">
672
  <div class="timeline-header">
673
- <h2> سجل العمليات</h2>
674
  </div>
675
  <div class="timeline" id="timeline"></div>
676
  </div>
@@ -680,10 +680,10 @@ body::before {
680
 
681
  <!-- Download -->
682
  <div class="download-section" id="downloadSection">
683
- <h3> الفيديو النظيف جاهز</h3>
684
  <p id="downloadDesc"></p>
685
  <button class="btn btn-primary" id="btnDownload" style="max-width:240px;margin:0 auto">
686
- <span>⬇️</span> تحميل الفيديو النظيف
687
  </button>
688
  </div>
689
 
@@ -695,7 +695,7 @@ let currentFile = null;
695
  let currentJobId = null;
696
  let apiReady = false;
697
 
698
- // ─── API Status Check ──────────────────────────────────────────
699
  async function checkApiStatus() {
700
  try {
701
  const res = await fetch(`${API_BASE}/health`);
@@ -705,28 +705,28 @@ async function checkApiStatus() {
705
 
706
  if (data.status === 'ready') {
707
  dot.className = 'status-dot ready';
708
- txt.textContent = ' النماذج جاهزة BLIP + Florence-2';
709
  txt.style.color = 'var(--accent)';
710
  apiReady = true;
711
  } else if (data.status === 'loading') {
712
  dot.className = 'status-dot loading';
713
- txt.textContent = ` ${data.message}`;
714
  txt.style.color = 'var(--warn)';
715
  setTimeout(checkApiStatus, 3000);
716
  } else {
717
  dot.className = 'status-dot error';
718
- txt.textContent = ` ${data.message}`;
719
  txt.style.color = 'var(--accent2)';
720
  }
721
  } catch (e) {
722
  const dot = document.getElementById('statusDot');
723
  dot.className = 'status-dot error';
724
- document.getElementById('statusText').textContent = ' لا يمكن الاتصال بالـ API';
725
  setTimeout(checkApiStatus, 5000);
726
  }
727
  }
728
 
729
- // ─── Upload Zone ───────────────────────────────────────────────
730
  const uploadZone = document.getElementById('uploadZone');
731
  const fileInput = document.getElementById('fileInput');
732
 
@@ -742,17 +742,22 @@ fileInput.addEventListener('change', e => { if (e.target.files[0]) handleFile(e.
742
 
743
  function handleFile(file) {
744
  if (!file.type.startsWith('video/')) {
745
- showAlert('الملف ليس فيديو! يُقبل فقط MP4, MOV, AVI', 'error');
746
  return;
747
  }
748
  if (file.size > 200 * 1024 * 1024) {
749
- showAlert('حجم الفيديو يتجاوز 200MB', 'warn');
750
  return;
751
  }
752
 
753
  currentFile = file;
754
  showVideoPreview(file);
755
- uploadFile(file);
 
 
 
 
 
756
  }
757
 
758
  function showVideoPreview(file) {
@@ -768,16 +773,16 @@ function showVideoPreview(file) {
768
  const dur = formatDuration(player.duration);
769
  const size = (file.size / 1024 / 1024).toFixed(1);
770
  meta.innerHTML = `
771
- <span>🎬 <strong>${file.name}</strong></span>
772
- <span> مدة: <strong>${dur}</strong></span>
773
- <span>💾 الحجم: <strong>${size} MB</strong></span>
774
- <span>📐 ${player.videoWidth}×${player.videoHeight}</span>
775
  `;
776
  };
777
  }
778
 
779
- // ─── Upload with Progress ──────────────────────────────────
780
- function uploadFile(file) {
781
  const uploadProgress = document.getElementById('uploadProgress');
782
  const progressFill = document.getElementById('progressFill');
783
  const progressPct = document.getElementById('progressPct');
@@ -785,44 +790,58 @@ function uploadFile(file) {
785
 
786
  uploadProgress.style.display = 'block';
787
  showTimeline();
788
- addTimelineItem('upload', 'active', '📤 تحميل الفيديو', 'UPLOAD', `جاري تحضير ${file.name}...`);
789
-
790
- // ✅ نحضّر الفيديو محلياً فقط - لا نرفعه الآن
791
- // الرفع يحدث فقط عند الضغط على أزرار الإجراءات
792
- let progress = 0;
793
- const interval = setInterval(() => {
794
- progress = Math.min(progress + 10, 90);
795
- progressFill.style.width = progress + '%';
796
- progressPct.textContent = progress + '%';
797
- progressLabel.textContent = `جاري تحضير الفيديو...`;
798
- if (progress >= 90) {
799
- clearInterval(interval);
800
- setTimeout(() => {
801
- progressFill.style.width = '100%';
802
- progressPct.textContent = '100%';
803
- setTimeout(() => {
804
- uploadProgress.style.display = 'none';
805
- updateTimelineItem('upload', 'done', `✅ الفيديو جاهز: ${file.name}`);
806
- document.getElementById('actionsBar').style.display = 'flex';
807
- document.getElementById('btnQuickCheck').disabled = !apiReady;
808
- document.getElementById('btnAnalyze').disabled = !apiReady;
809
- addTimelineItem('ready', 'done', '⏸ بانتظار الإجراء', 'READY',
810
- 'الفيديو جاهز — اختر "فحص سريع" للتحقق السريع أو "تحليل وتقطيع" للمعالجة الكاملة');
811
- }, 300);
812
- }, 200);
813
  }
814
- }, 80);
815
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
816
 
817
- // ─── Quick Check ──────────────────────────────────────────────
 
 
 
 
 
 
 
 
818
  document.getElementById('btnQuickCheck').addEventListener('click', async () => {
819
  if (!currentFile || !apiReady) return;
820
  setButtonsDisabled(true);
821
- addTimelineItem('quickcheck', 'active', '🔍 فحص سريع للفيديو', 'QUICK-CHECK',
822
- 'جاري فحص أول frame من الفيديو للتحقق السريع من وجود نساء...');
823
 
824
  try {
825
- // نستخرج أول frame من الفيديو
826
  const canvas = document.createElement('canvas');
827
  const video = document.getElementById('videoPlayer');
828
  video.currentTime = 1;
@@ -837,150 +856,73 @@ document.getElementById('btnQuickCheck').addEventListener('click', async () => {
837
  const formData = new FormData();
838
  formData.append('file', blob, 'frame.jpg');
839
 
840
- const res = await fetch(`${API_BASE}/analyze-frame`, { method: 'POST', body: formData });
841
  const data = await res.json();
842
 
843
  if (data.decision === 'BLOCK' || data.decision === 'block') {
844
  updateTimelineItem('quickcheck', 'done',
845
- `🔴 تم اكتشاف امرأة في الـ frame الأول يُحتمل وجود نساء في الفيديو`);
846
- addTimelineItem('qc-result', 'active', '⚠️ تحذير: يوجد محتوى أنثوي', 'DETECTED',
847
- 'الفحص السريع اكتشف امرأة. يُنصح بالمتابعة إلى "تحليل وتقطيع" للمعالجة الكاملة.');
848
  } else {
849
  updateTimelineItem('quickcheck', 'done',
850
- `🟢 الـ frame الأول نظيف لا يوجد نساء في البداية`);
851
- addTimelineItem('qc-result', 'done', ' الفحص الأولي نظيف', 'CLEAN',
852
- 'لم يُكتشف محتوى أنثوي في الـ frame الأول. يُنصح بالمتابعة لتحليل كامل الفيديو.');
853
  }
854
 
855
  setButtonsDisabled(false);
856
  }, 'image/jpeg', 0.9);
857
 
858
  } catch (e) {
859
- updateTimelineItem('quickcheck', 'error', ` خطأ: ${e.message}`);
860
  setButtonsDisabled(false);
861
  }
862
  });
863
 
864
- // ─── Full Analysis ─────────────────────────────────────────────
865
  document.getElementById('btnAnalyze').addEventListener('click', () => {
866
  if (!currentFile || !apiReady) return;
867
  setButtonsDisabled(true);
868
-
869
- const uploadProgress = document.getElementById('uploadProgress');
870
- const progressFill = document.getElementById('progressFill');
871
- const progressPct = document.getElementById('progressPct');
872
- const progressLabel = document.getElementById('progressLabel');
873
-
874
- // ─── المرحلة 1: رفع الفيديو مع progress ──────────────────────
875
- addTimelineItem('upload-video', 'active', '📤 رفع الفيديو للتحليل', 'UPLOADING',
876
- `جاري رفع ${currentFile.name} إلى السيرفر...`);
877
-
878
- uploadProgress.style.display = 'block';
879
- progressFill.style.width = '0%';
880
- progressPct.textContent = '0%';
881
-
882
- const formData = new FormData();
883
- formData.append('file', currentFile);
884
-
885
- const xhr = new XMLHttpRequest();
886
- const startTime = Date.now();
887
-
888
- // ─── progress رفع الملف ───────────────────────────────────────
889
- xhr.upload.onprogress = e => {
890
- if (e.lengthComputable) {
891
- const pct = Math.round(e.loaded / e.total * 100);
892
- progressFill.style.width = pct + '%';
893
- progressPct.textContent = pct + '%';
894
- progressLabel.textContent = `رفع الفيديو: ${(e.loaded/1024/1024).toFixed(1)}MB / ${(e.total/1024/1024).toFixed(1)}MB`;
895
- }
896
- };
897
-
898
- xhr.upload.onload = () => {
899
- // ─── اكتمل الرفع → بدء التحليل على السيرفر ─────────────────
900
- uploadProgress.style.display = 'none';
901
- updateTimelineItem('upload-video', 'done', `✅ تم رفع الفيديو — جاري التحليل على السيرفر...`);
902
- addTimelineItem('analyze', 'active', '⚙️ تحليل الفيديو frame بـ frame', 'ANALYZING',
903
- 'السيرفر يحلل كل ثانية من الفيديو باستخدام BLIP + Florence-2...');
904
- };
905
-
906
- xhr.onload = () => {
907
- uploadProgress.style.display = 'none';
908
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
909
-
910
- if (xhr.status === 200) {
911
- try {
912
- const data = JSON.parse(xhr.responseText);
913
- currentJobId = data.output_job_id || null;
914
- updateTimelineItem('analyze', 'done', `✅ اكتمل التحليل في ${elapsed}s`);
915
- handleAnalysisResult(data, elapsed);
916
- } catch(e) {
917
- updateTimelineItem('analyze', 'error', `❌ خطأ في تحليل الاستجابة`);
918
- }
919
- } else {
920
- try {
921
- const err = JSON.parse(xhr.responseText);
922
- updateTimelineItem('analyze', 'error', `❌ ${err.detail || 'خطأ في السيرفر'}`);
923
- } catch {
924
- updateTimelineItem('analyze', 'error', `❌ خطأ ${xhr.status}`);
925
- }
926
- }
927
- setButtonsDisabled(false);
928
- };
929
-
930
- xhr.onerror = () => {
931
- uploadProgress.style.display = 'none';
932
- updateTimelineItem('upload-video', 'error', '❌ خطأ في الاتصال بالسيرفر');
933
- setButtonsDisabled(false);
934
- };
935
-
936
- xhr.ontimeout = () => {
937
- uploadProgress.style.display = 'none';
938
- updateTimelineItem('analyze', 'error', '❌ انتهت مهلة الانتظار');
939
- setButtonsDisabled(false);
940
- };
941
-
942
- // timeout طويل لأن التحليل يستغرق وقتاً على CPU
943
- xhr.timeout = 60 * 60 * 1000; // ساعة كاملة
944
- xhr.open('POST', `${API_BASE}/analyze-video`);
945
- xhr.send(formData);
946
  });
947
 
948
- // ─── Handle Analysis Result ───────────────────────────────────
949
  function handleAnalysisResult(data, elapsed) {
950
  const resultsSection = document.getElementById('resultsSection');
951
  resultsSection.style.display = 'block';
952
 
953
  if (!data.has_female) {
954
- // ── نظيف ──
955
- addTimelineItem('result', 'done', '🟢 الفيديو نظيف تماماً', 'CLEAN',
956
- 'لم يُكتشف أي محتوى أنثوي في الفيديو كاملاً. الفيديو جاهز للنشر.');
957
 
958
  resultsSection.innerHTML = `
959
  <div class="result-card">
960
  <div class="result-card-header">
961
- <span>📊</span>
962
- <h3>نتيجة التحليل</h3>
963
  </div>
964
  <div class="result-card-body">
965
  <div class="verdict clean">
966
- <div class="verdict-icon"></div>
967
  <div class="verdict-text">
968
- <h4 style="color:var(--accent)">الفيديو نظيف</h4>
969
- <p>لا يحتوي على أي محتوى أنثوي</p>
970
  </div>
971
  </div>
972
  <div class="stats-grid">
973
  <div class="stat-box">
974
- <span class="val">${data.analysis_log ? data.analysis_log.length : ''}</span>
975
- <span class="lbl">frames تم تحليلها</span>
976
  </div>
977
  <div class="stat-box">
978
- <span class="val">${data.analysis_time || elapsed || ''}s</span>
979
- <span class="lbl">وقت التحليل</span>
980
  </div>
981
  <div class="stat-box">
982
  <span class="val" style="color:var(--accent)">0</span>
983
- <span class="lbl">مقاطع محذوفة</span>
984
  </div>
985
  </div>
986
  ${renderFrameLog(data.analysis_log)}
@@ -990,54 +932,56 @@ function handleAnalysisResult(data, elapsed) {
990
  return;
991
  }
992
 
993
- // ── يحتوي على نساء ──
994
  const femaleSegs = data.female_segments || [];
995
  const keptSegs = data.kept_segments || [];
996
  const totalRemoved = data.total_removed_sec || 0;
997
 
998
  if (femaleSegs.length > 0) {
999
- addTimelineItem('cutting', 'done', `✂️ تم تقطيع ${femaleSegs.length} مقطع`, 'CUTTING',
1000
- `تم حذف ${totalRemoved.toFixed(1)} ثانية تحتوي على نساء وبناء الفيديو النظيف.`);
1001
  }
1002
 
1003
  addTimelineItem('result', data.output_available ? 'done' : 'active',
1004
- data.output_available ? '📦 الفيديو النظيف جاهز' : '⚠️ الفيديو كله يحتوي نساء',
1005
  'RESULT', data.message);
1006
 
1007
- // حساب المدة التقريبية
1008
- const totalDur = data.duration_sec || 0;
 
 
1009
 
1010
  resultsSection.innerHTML = `
1011
  <div class="result-card">
1012
  <div class="result-card-header">
1013
- <span>📊</span>
1014
- <h3>نتيجة التحليل</h3>
1015
  </div>
1016
  <div class="result-card-body">
1017
  <div class="verdict female">
1018
- <div class="verdict-icon">⚠️</div>
1019
  <div class="verdict-text">
1020
- <h4 style="color:var(--accent2)">تم اكتشاف محتوى أنثوي</h4>
1021
- <p>${femaleSegs.length} مقطع يحتوي على نساء تم حذفها</p>
1022
  </div>
1023
  </div>
1024
 
1025
  <div class="stats-grid">
1026
  <div class="stat-box">
1027
  <span class="val" style="color:var(--accent2)">${femaleSegs.length}</span>
1028
- <span class="lbl">مقاطع محذوفة</span>
1029
  </div>
1030
  <div class="stat-box">
1031
  <span class="val" style="color:var(--accent2)">${totalRemoved.toFixed(1)}s</span>
1032
- <span class="lbl">مدة محذوفة</span>
1033
  </div>
1034
  <div class="stat-box">
1035
- <span class="val">${data.analysis_log ? data.analysis_log.length : ''}</span>
1036
- <span class="lbl">frames تم تحليلها</span>
1037
  </div>
1038
  <div class="stat-box">
1039
- <span class="val">${data.analysis_time || elapsed || ''}s</span>
1040
- <span class="lbl">وقت التحليل</span>
1041
  </div>
1042
  </div>
1043
 
@@ -1052,7 +996,7 @@ function handleAnalysisResult(data, elapsed) {
1052
  const dl = document.getElementById('downloadSection');
1053
  dl.style.display = 'block';
1054
  document.getElementById('downloadDesc').textContent =
1055
- `تم حذف ${totalRemoved.toFixed(1)} ثانية الفيديو النظيف جاهز للتحميل`;
1056
 
1057
  document.getElementById('btnDownload').onclick = () => {
1058
  window.location.href = `${API_BASE}/download/${currentJobId}`;
@@ -1060,7 +1004,7 @@ function handleAnalysisResult(data, elapsed) {
1060
  }
1061
  }
1062
 
1063
- // ─── Render Helpers ────────────────────────────────────────────
1064
  function renderVideoTimeline(femaleSegs, keptSegs, totalDur) {
1065
  if (!totalDur || totalDur === 0) return '';
1066
  const allSegs = [
@@ -1077,7 +1021,7 @@ function renderVideoTimeline(femaleSegs, keptSegs, totalDur) {
1077
  return `
1078
  <div style="margin:16px 0">
1079
  <div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--muted);margin-bottom:6px">
1080
- خريطة الفيديو <span style="color:var(--accent)">■ نظيف</span> <span style="color:var(--accent2)">■ محذوف</span>
1081
  </div>
1082
  <div class="video-timeline">${bars}</div>
1083
  <div class="timeline-label"><span>0s</span><span>${totalDur.toFixed(0)}s</span></div>
@@ -1099,13 +1043,13 @@ function renderSegmentsTable(segs, type) {
1099
 
1100
  return `
1101
  <div style="margin-top:12px">
1102
- <div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--muted);margin-bottom:6px">المقاطع المحذوفة</div>
1103
  <table style="width:100%;border-collapse:collapse;background:rgba(0,0,0,0.3);border-radius:4px;overflow:hidden">
1104
  <tr style="background:rgba(255,255,255,0.03)">
1105
  <th style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--muted);padding:6px 8px;text-align:right;font-weight:400">#</th>
1106
- <th style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--muted);padding:6px 8px;text-align:right;font-weight:400">البداية</th>
1107
- <th style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--muted);padding:6px 8px;text-align:right;font-weight:400">النهاية</th>
1108
- <th style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--muted);padding:6px 8px;text-align:right;font-weight:400">المدة</th>
1109
  </tr>
1110
  ${rows}
1111
  </table>
@@ -1121,14 +1065,14 @@ function renderFrameLog(log) {
1121
  return `
1122
  <div style="margin-top:12px">
1123
  <div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--muted);margin-bottom:6px">
1124
- سجل الـ frames <span style="color:var(--accent)">■ نظيف</span> <span style="color:var(--accent2)"> يحتوي نساء</span>
1125
  </div>
1126
  <div class="frame-log">${badges}</div>
1127
  </div>
1128
  `;
1129
  }
1130
 
1131
- // ─── Timeline Helpers ──────────────────────────────────────────
1132
  function showTimeline() {
1133
  document.getElementById('timelineSection').style.display = 'block';
1134
  }
@@ -1162,12 +1106,12 @@ function updateTimelineItem(id, status, desc) {
1162
  item.className = `timeline-item status-${status} visible`;
1163
  const descEl = document.getElementById(`tl-desc-${id}`);
1164
  if (descEl && desc) descEl.textContent = desc;
1165
- // إزالة spinner
1166
  const spinner = item.querySelector('.spinner');
1167
  if (spinner) spinner.remove();
1168
  }
1169
 
1170
- // ─── Utils ─────────────────────────────────────────────────────
1171
  function formatDuration(sec) {
1172
  const m = Math.floor(sec / 60);
1173
  const s = Math.floor(sec % 60);
@@ -1201,7 +1145,7 @@ function resetAll() {
1201
  fileInput.value = '';
1202
  }
1203
 
1204
- // ─── Init ──────────────────────────────────────────────────────
1205
  checkApiStatus();
1206
  </script>
1207
  </body>
 
28
  overflow-x: hidden;
29
  }
30
 
31
+ /* ─── Grid Background ─── */
32
  body::before {
33
  content: '';
34
  position: fixed;
 
49
  z-index: 1;
50
  }
51
 
52
+ /* ─── Header ─── */
53
  .header {
54
  text-align: center;
55
  margin-bottom: 48px;
 
83
  font-weight: 300;
84
  }
85
 
86
+ /* ─── API Status ─── */
87
  .api-status {
88
  display: flex;
89
  align-items: center;
 
109
  50% { opacity: 0.7; box-shadow: 0 0 0 4px transparent; }
110
  }
111
 
112
+ /* ─── Upload Zone ─── */
113
  .upload-zone {
114
  border: 1px dashed var(--border);
115
  border-radius: 8px;
 
158
 
159
  #fileInput { display: none; }
160
 
161
+ /* ─── Video Preview ─── */
162
  .video-preview {
163
  display: none;
164
  background: var(--surface);
 
189
  .video-meta span { display: flex; align-items: center; gap: 6px; }
190
  .video-meta strong { color: var(--text); }
191
 
192
+ /* ─── Upload Progress ─── */
193
  .upload-progress {
194
  display: none;
195
  margin-bottom: 24px;
 
220
  box-shadow: 0 0 8px var(--accent);
221
  }
222
 
223
+ /* ─── Action Buttons ─── */
224
  .actions {
225
  display: flex;
226
  gap: 12px;
 
280
  transform: translateY(-2px);
281
  }
282
 
283
+ /* ─── Timeline ─── */
284
  .timeline-section {
285
  display: none;
286
  margin-bottom: 32px;
 
416
  .timeline-details .warn { color: var(--warn); }
417
  .timeline-details .danger { color: var(--accent2); }
418
 
419
+ /* ─── Frame Log ─── */
420
  .frame-log {
421
  display: flex;
422
  flex-wrap: wrap;
 
444
  background: rgba(255,51,102,0.05);
445
  }
446
 
447
+ /* ─── Results ─── */
448
  .results-section {
449
  display: none;
450
  margin-bottom: 32px;
 
523
  display: block;
524
  }
525
 
526
+ /* ─── Video Timeline Bar ─── */
527
  .video-timeline {
528
  background: rgba(0,0,0,0.3);
529
  border-radius: 4px;
 
559
  margin-top: 4px;
560
  }
561
 
562
+ /* ─── Download ─── */
563
  .download-section {
564
  display: none;
565
  text-align: center;
 
583
  margin-bottom: 24px;
584
  }
585
 
586
+ /* ─── Spinner ─── */
587
  .spinner {
588
  width: 16px; height: 16px;
589
  border: 2px solid rgba(0,0,0,0.3);
 
595
 
596
  @keyframes spin { to { transform: rotate(360deg); } }
597
 
598
+ /* ─── Alert ─── */
599
  .alert {
600
  padding: 12px 16px;
601
  border-radius: 6px;
 
616
  <!-- Header -->
617
  <div class="header">
618
  <div class="header-badge">AI VIDEO FILTER</div>
619
+ <h1>تنقية <span>الفيديو</span> الإعلاني</h1>
620
+ <p>إزالة مقاطع النساء تلقائياً باستخدام BLIP + Florence-2</p>
621
  </div>
622
 
623
  <!-- API Status -->
624
  <div class="api-status">
625
  <div class="status-dot" id="statusDot"></div>
626
+ <span id="statusText" style="font-family:'IBM Plex Mono',monospace;font-size:12px;color:var(--muted)">جاري الاتصال بالـ API...</span>
627
  </div>
628
 
629
  <!-- Alert -->
 
631
 
632
  <!-- Upload Zone -->
633
  <div class="upload-zone" id="uploadZone">
634
+ <span class="upload-icon">🎬</span>
635
+ <h3>اسحب الفيديو هنا أو اضغط للاختيار</h3>
636
+ <p>MP4 / MOV / AVI · حد أقصى 200MB</p>
637
  <input type="file" id="fileInput" accept="video/*">
638
  </div>
639
 
 
646
  <!-- Upload Progress -->
647
  <div class="upload-progress" id="uploadProgress">
648
  <div class="progress-label">
649
+ <span id="progressLabel">جاري الرفع...</span>
650
  <span id="progressPct">0%</span>
651
  </div>
652
  <div class="progress-bar">
 
657
  <!-- Action Buttons -->
658
  <div class="actions" id="actionsBar" style="display:none">
659
  <button class="btn btn-danger" id="btnQuickCheck" disabled>
660
+ <span>🔍</span> فحص سريع
661
  </button>
662
  <button class="btn btn-primary" id="btnAnalyze" disabled>
663
+ <span>⚙️</span> تحليل وتقطيع
664
  </button>
665
  <button class="btn btn-secondary" id="btnReset" onclick="resetAll()">
666
+ <span>↺</span> إعادة
667
  </button>
668
  </div>
669
 
670
  <!-- Timeline -->
671
  <div class="timeline-section" id="timelineSection">
672
  <div class="timeline-header">
673
+ <h2>⬡ سجل العمليات</h2>
674
  </div>
675
  <div class="timeline" id="timeline"></div>
676
  </div>
 
680
 
681
  <!-- Download -->
682
  <div class="download-section" id="downloadSection">
683
+ <h3>✅ الفيديو النظيف جاهز</h3>
684
  <p id="downloadDesc"></p>
685
  <button class="btn btn-primary" id="btnDownload" style="max-width:240px;margin:0 auto">
686
+ <span>⬇️</span> تحميل الفيديو النظيف
687
  </button>
688
  </div>
689
 
 
695
  let currentJobId = null;
696
  let apiReady = false;
697
 
698
+ // ─── API Status Check ──────────────────────────────────────────
699
  async function checkApiStatus() {
700
  try {
701
  const res = await fetch(`${API_BASE}/health`);
 
705
 
706
  if (data.status === 'ready') {
707
  dot.className = 'status-dot ready';
708
+ txt.textContent = '✅ النماذج جاهزة — BLIP + Florence-2';
709
  txt.style.color = 'var(--accent)';
710
  apiReady = true;
711
  } else if (data.status === 'loading') {
712
  dot.className = 'status-dot loading';
713
+ txt.textContent = `⏳ ${data.message}`;
714
  txt.style.color = 'var(--warn)';
715
  setTimeout(checkApiStatus, 3000);
716
  } else {
717
  dot.className = 'status-dot error';
718
+ txt.textContent = `❌ ${data.message}`;
719
  txt.style.color = 'var(--accent2)';
720
  }
721
  } catch (e) {
722
  const dot = document.getElementById('statusDot');
723
  dot.className = 'status-dot error';
724
+ document.getElementById('statusText').textContent = '❌ لا يمكن الاتصال بالـ API';
725
  setTimeout(checkApiStatus, 5000);
726
  }
727
  }
728
 
729
+ // ─── Upload Zone ───────────────────────────────────────────────
730
  const uploadZone = document.getElementById('uploadZone');
731
  const fileInput = document.getElementById('fileInput');
732
 
 
742
 
743
  function handleFile(file) {
744
  if (!file.type.startsWith('video/')) {
745
+ showAlert('الملف ليس فيديو! يُقبل فقط MP4, MOV, AVI', 'error');
746
  return;
747
  }
748
  if (file.size > 200 * 1024 * 1024) {
749
+ showAlert('حجم الفيديو يتجاوز 200MB', 'warn');
750
  return;
751
  }
752
 
753
  currentFile = file;
754
  showVideoPreview(file);
755
+ showTimeline();
756
+ document.getElementById('actionsBar').style.display = 'flex';
757
+ document.getElementById('btnQuickCheck').disabled = !apiReady;
758
+ document.getElementById('btnAnalyze').disabled = !apiReady;
759
+ addTimelineItem('ready', 'done', '⏸ بانتظار الإجراء', 'READY',
760
+ 'الفيديو جاهز. اختر "فحص سريع" أو "تحليل وتقطيع".');
761
  }
762
 
763
  function showVideoPreview(file) {
 
773
  const dur = formatDuration(player.duration);
774
  const size = (file.size / 1024 / 1024).toFixed(1);
775
  meta.innerHTML = `
776
+ <span>🎬 <strong>${file.name}</strong></span>
777
+ <span>⏱ مدة: <strong>${dur}</strong></span>
778
+ <span>💾 الحجم: <strong>${size} MB</strong></span>
779
+ <span>📐 ${player.videoWidth}×${player.videoHeight}</span>
780
  `;
781
  };
782
  }
783
 
784
+ // ─── Upload with Progress ──────────────────────────────────────
785
+ function analyzeVideo(file) {
786
  const uploadProgress = document.getElementById('uploadProgress');
787
  const progressFill = document.getElementById('progressFill');
788
  const progressPct = document.getElementById('progressPct');
 
790
 
791
  uploadProgress.style.display = 'block';
792
  showTimeline();
793
+ addTimelineItem('analyze', 'active', '⚙️ بدء التحليل الكامل', 'ANALYZING',
794
+ `جاري رفع ${file.name} وتحليله...`);
795
+
796
+ const xhr = new XMLHttpRequest();
797
+ const formData = new FormData();
798
+ formData.append('file', file);
799
+
800
+ xhr.upload.onprogress = e => {
801
+ if (e.lengthComputable) {
802
+ const pct = Math.round(e.loaded / e.total * 100);
803
+ progressFill.style.width = pct + '%';
804
+ progressPct.textContent = pct + '%';
805
+ progressLabel.textContent = `جاري الرفع... ${(e.loaded/1024/1024).toFixed(1)}MB / ${(e.total/1024/1024).toFixed(1)}MB`;
 
 
 
 
 
 
 
 
 
 
 
 
806
  }
807
+ };
808
+
809
+ xhr.onload = () => {
810
+ uploadProgress.style.display = 'none';
811
+ try {
812
+ const data = JSON.parse(xhr.responseText || '{}');
813
+ if (xhr.status !== 200) {
814
+ updateTimelineItem('analyze', 'error', `❌ فشل التحليل: ${data.detail || xhr.status}`);
815
+ setButtonsDisabled(false);
816
+ return;
817
+ }
818
+ const elapsed = data.analysis_time || '—';
819
+ currentJobId = data.output_job_id || null;
820
+ updateTimelineItem('analyze', 'done', `✅ اكتمل التحليل في ${elapsed}s`);
821
+ handleAnalysisResult(data, elapsed);
822
+ } catch(e) {
823
+ updateTimelineItem('analyze', 'error', '❌ خطأ في تحليل الاستجابة');
824
+ }
825
+ setButtonsDisabled(false);
826
+ };
827
 
828
+ xhr.onerror = () => {
829
+ uploadProgress.style.display = 'none';
830
+ updateTimelineItem('analyze', 'error', '❌ خطأ في الاتصال');
831
+ setButtonsDisabled(false);
832
+ };
833
+
834
+ xhr.open('POST', `${API_BASE}/analyze-video`);
835
+ xhr.send(formData);
836
+ }
837
  document.getElementById('btnQuickCheck').addEventListener('click', async () => {
838
  if (!currentFile || !apiReady) return;
839
  setButtonsDisabled(true);
840
+ addTimelineItem('quickcheck', 'active', '🔍 فحص سريع للفيديو', 'QUICK-CHECK',
841
+ 'جاري فحص أول frame من الفيديو للتحقق السريع من وجود نساء...');
842
 
843
  try {
844
+ // نستخرج أول frame من الفيديو
845
  const canvas = document.createElement('canvas');
846
  const video = document.getElementById('videoPlayer');
847
  video.currentTime = 1;
 
856
  const formData = new FormData();
857
  formData.append('file', blob, 'frame.jpg');
858
 
859
+ const res = await fetch(`${API_BASE}/analyze-file`, { method: 'POST', body: formData });
860
  const data = await res.json();
861
 
862
  if (data.decision === 'BLOCK' || data.decision === 'block') {
863
  updateTimelineItem('quickcheck', 'done',
864
+ `🔴 تم اكتشاف امرأة في الـ frame الأول — يُحتمل وجود نساء في الفيديو`);
865
+ addTimelineItem('qc-result', 'active', '⚠️ تحذير: يوجد محتوى أنثوي', 'DETECTED',
866
+ 'الفحص السريع اكتشف امرأة. يُنصح بالمتابعة إلى "تحليل وتقطيع" للمعالجة الكاملة.');
867
  } else {
868
  updateTimelineItem('quickcheck', 'done',
869
+ `🟢 الـ frame الأول نظيف — لا يوجد نساء في البداية`);
870
+ addTimelineItem('qc-result', 'done', '✅ الفحص الأولي نظيف', 'CLEAN',
871
+ 'لم يُكتشف محتوى أنثوي في الـ frame الأول. يُنصح بالمتابعة لتحليل كامل الفيديو.');
872
  }
873
 
874
  setButtonsDisabled(false);
875
  }, 'image/jpeg', 0.9);
876
 
877
  } catch (e) {
878
+ updateTimelineItem('quickcheck', 'error', `❌ خطأ: ${e.message}`);
879
  setButtonsDisabled(false);
880
  }
881
  });
882
 
883
+ // ─── Full Analysis ─────────────────────────────────────────────
884
  document.getElementById('btnAnalyze').addEventListener('click', () => {
885
  if (!currentFile || !apiReady) return;
886
  setButtonsDisabled(true);
887
+ analyzeVideo(currentFile);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
888
  });
889
 
890
+ // ─── Handle Analysis Result ───────────────────────────────────
891
  function handleAnalysisResult(data, elapsed) {
892
  const resultsSection = document.getElementById('resultsSection');
893
  resultsSection.style.display = 'block';
894
 
895
  if (!data.has_female) {
896
+ // ── نظيف ──
897
+ addTimelineItem('result', 'done', '🟢 الفيديو نظيف تماماً', 'CLEAN',
898
+ 'لم يُكتشف أي محتوى أنثوي في الفيديو كاملاً. الفيديو جاهز للنشر.');
899
 
900
  resultsSection.innerHTML = `
901
  <div class="result-card">
902
  <div class="result-card-header">
903
+ <span>📊</span>
904
+ <h3>نتيجة التحليل</h3>
905
  </div>
906
  <div class="result-card-body">
907
  <div class="verdict clean">
908
+ <div class="verdict-icon">✅</div>
909
  <div class="verdict-text">
910
+ <h4 style="color:var(--accent)">الفيديو نظيف</h4>
911
+ <p>لا يحتوي على أي محتوى أنثوي</p>
912
  </div>
913
  </div>
914
  <div class="stats-grid">
915
  <div class="stat-box">
916
+ <span class="val">${data.analysis_log ? data.analysis_log.length : '—'}</span>
917
+ <span class="lbl">frames تم تحليلها</span>
918
  </div>
919
  <div class="stat-box">
920
+ <span class="val">${data.analysis_time || elapsed || '—'}s</span>
921
+ <span class="lbl">وقت التحليل</span>
922
  </div>
923
  <div class="stat-box">
924
  <span class="val" style="color:var(--accent)">0</span>
925
+ <span class="lbl">مقاطع محذوفة</span>
926
  </div>
927
  </div>
928
  ${renderFrameLog(data.analysis_log)}
 
932
  return;
933
  }
934
 
935
+ // ── يحتوي على نساء ──
936
  const femaleSegs = data.female_segments || [];
937
  const keptSegs = data.kept_segments || [];
938
  const totalRemoved = data.total_removed_sec || 0;
939
 
940
  if (femaleSegs.length > 0) {
941
+ addTimelineItem('cutting', 'done', `✂️ تم تقطيع ${femaleSegs.length} مقطع`, 'CUTTING',
942
+ `تم حذف ${totalRemoved.toFixed(1)} ثانية تحتوي على نساء وبناء الفيديو النظيف.`);
943
  }
944
 
945
  addTimelineItem('result', data.output_available ? 'done' : 'active',
946
+ data.output_available ? '📦 الفيديو النظيف جاهز' : '⚠️ الفيديو كله يحتوي نساء',
947
  'RESULT', data.message);
948
 
949
+ // حساب المدة التقريبية
950
+ const totalDur = femaleSegs.length > 0 && keptSegs.length > 0
951
+ ? (keptSegs[keptSegs.length-1][1] || 0)
952
+ : 0;
953
 
954
  resultsSection.innerHTML = `
955
  <div class="result-card">
956
  <div class="result-card-header">
957
+ <span>📊</span>
958
+ <h3>نتيجة التحليل</h3>
959
  </div>
960
  <div class="result-card-body">
961
  <div class="verdict female">
962
+ <div class="verdict-icon">⚠️</div>
963
  <div class="verdict-text">
964
+ <h4 style="color:var(--accent2)">تم اكتشاف محتوى أنثوي</h4>
965
+ <p>${femaleSegs.length} مقطع يحتوي على نساء — تم حذفها</p>
966
  </div>
967
  </div>
968
 
969
  <div class="stats-grid">
970
  <div class="stat-box">
971
  <span class="val" style="color:var(--accent2)">${femaleSegs.length}</span>
972
+ <span class="lbl">مقاطع محذوفة</span>
973
  </div>
974
  <div class="stat-box">
975
  <span class="val" style="color:var(--accent2)">${totalRemoved.toFixed(1)}s</span>
976
+ <span class="lbl">مدة محذوفة</span>
977
  </div>
978
  <div class="stat-box">
979
+ <span class="val">${data.analysis_log ? data.analysis_log.length : '—'}</span>
980
+ <span class="lbl">frames تم تحليلها</span>
981
  </div>
982
  <div class="stat-box">
983
+ <span class="val">${data.analysis_time || elapsed || '—'}s</span>
984
+ <span class="lbl">وقت التحليل</span>
985
  </div>
986
  </div>
987
 
 
996
  const dl = document.getElementById('downloadSection');
997
  dl.style.display = 'block';
998
  document.getElementById('downloadDesc').textContent =
999
+ `تم حذف ${totalRemoved.toFixed(1)} ثانية — الفيديو النظيف جاهز للتحميل`;
1000
 
1001
  document.getElementById('btnDownload').onclick = () => {
1002
  window.location.href = `${API_BASE}/download/${currentJobId}`;
 
1004
  }
1005
  }
1006
 
1007
+ // ─── Render Helpers ────────────────────────────────────────────
1008
  function renderVideoTimeline(femaleSegs, keptSegs, totalDur) {
1009
  if (!totalDur || totalDur === 0) return '';
1010
  const allSegs = [
 
1021
  return `
1022
  <div style="margin:16px 0">
1023
  <div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--muted);margin-bottom:6px">
1024
+ خريطة الفيديو — <span style="color:var(--accent)">■ نظيف</span> <span style="color:var(--accent2)">■ محذوف</span>
1025
  </div>
1026
  <div class="video-timeline">${bars}</div>
1027
  <div class="timeline-label"><span>0s</span><span>${totalDur.toFixed(0)}s</span></div>
 
1043
 
1044
  return `
1045
  <div style="margin-top:12px">
1046
+ <div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--muted);margin-bottom:6px">المقاطع المحذوفة</div>
1047
  <table style="width:100%;border-collapse:collapse;background:rgba(0,0,0,0.3);border-radius:4px;overflow:hidden">
1048
  <tr style="background:rgba(255,255,255,0.03)">
1049
  <th style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--muted);padding:6px 8px;text-align:right;font-weight:400">#</th>
1050
+ <th style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--muted);padding:6px 8px;text-align:right;font-weight:400">البداية</th>
1051
+ <th style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--muted);padding:6px 8px;text-align:right;font-weight:400">النهاية</th>
1052
+ <th style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--muted);padding:6px 8px;text-align:right;font-weight:400">المدة</th>
1053
  </tr>
1054
  ${rows}
1055
  </table>
 
1065
  return `
1066
  <div style="margin-top:12px">
1067
  <div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--muted);margin-bottom:6px">
1068
+ سجل الـ frames — <span style="color:var(--accent)">■ نظيف</span> <span style="color:var(--accent2)">■ يحتوي نساء</span>
1069
  </div>
1070
  <div class="frame-log">${badges}</div>
1071
  </div>
1072
  `;
1073
  }
1074
 
1075
+ // ─── Timeline Helpers ──────────────────────────────────────────
1076
  function showTimeline() {
1077
  document.getElementById('timelineSection').style.display = 'block';
1078
  }
 
1106
  item.className = `timeline-item status-${status} visible`;
1107
  const descEl = document.getElementById(`tl-desc-${id}`);
1108
  if (descEl && desc) descEl.textContent = desc;
1109
+ // إزالة spinner
1110
  const spinner = item.querySelector('.spinner');
1111
  if (spinner) spinner.remove();
1112
  }
1113
 
1114
+ // ─── Utils ─────────────────────────────────────────────────────
1115
  function formatDuration(sec) {
1116
  const m = Math.floor(sec / 60);
1117
  const s = Math.floor(sec % 60);
 
1145
  fileInput.value = '';
1146
  }
1147
 
1148
+ // ─── Init ──────────────────────────────────────────────────────
1149
  checkApiStatus();
1150
  </script>
1151
  </body>