| <!DOCTYPE html> |
| <html lang="ar" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Video Filter AI</title> |
| <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=Tajawal:wght@300;400;700;900&display=swap" rel="stylesheet"> |
| <style> |
| :root { |
| --bg: #0a0a0f; |
| --surface: #111118; |
| --border: #1e1e2e; |
| --accent: #00ff88; |
| --accent2: #ff3366; |
| --accent3: #4488ff; |
| --text: #e8e8f0; |
| --muted: #555570; |
| --warn: #ffaa00; |
| } |
| |
| * { margin: 0; padding: 0; box-sizing: border-box; } |
| |
| body { |
| font-family: 'Tajawal', sans-serif; |
| background: var(--bg); |
| color: var(--text); |
| min-height: 100vh; |
| overflow-x: hidden; |
| } |
| |
| |
| body::before { |
| content: ''; |
| position: fixed; |
| inset: 0; |
| background-image: |
| linear-gradient(rgba(0,255,136,0.03) 1px, transparent 1px), |
| linear-gradient(90deg, rgba(0,255,136,0.03) 1px, transparent 1px); |
| background-size: 40px 40px; |
| pointer-events: none; |
| z-index: 0; |
| } |
| |
| .container { |
| max-width: 900px; |
| margin: 0 auto; |
| padding: 40px 20px; |
| position: relative; |
| z-index: 1; |
| } |
| |
| |
| .header { |
| text-align: center; |
| margin-bottom: 48px; |
| } |
| |
| .header-badge { |
| display: inline-block; |
| font-family: 'IBM Plex Mono', monospace; |
| font-size: 11px; |
| color: var(--accent); |
| border: 1px solid var(--accent); |
| padding: 4px 12px; |
| border-radius: 2px; |
| letter-spacing: 3px; |
| margin-bottom: 16px; |
| text-transform: uppercase; |
| } |
| |
| .header h1 { |
| font-size: 42px; |
| font-weight: 900; |
| line-height: 1.1; |
| margin-bottom: 8px; |
| } |
| |
| .header h1 span { color: var(--accent); } |
| |
| .header p { |
| color: var(--muted); |
| font-size: 15px; |
| font-weight: 300; |
| } |
| |
| |
| .api-status { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| justify-content: center; |
| margin-bottom: 32px; |
| font-family: 'IBM Plex Mono', monospace; |
| font-size: 12px; |
| } |
| |
| .status-dot { |
| width: 8px; height: 8px; |
| border-radius: 50%; |
| background: var(--muted); |
| animation: none; |
| } |
| .status-dot.ready { background: var(--accent); animation: pulse 2s infinite; } |
| .status-dot.loading { background: var(--warn); animation: pulse 1s infinite; } |
| .status-dot.error { background: var(--accent2); } |
| |
| @keyframes pulse { |
| 0%, 100% { opacity: 1; box-shadow: 0 0 0 0 currentColor; } |
| 50% { opacity: 0.7; box-shadow: 0 0 0 4px transparent; } |
| } |
| |
| |
| .upload-zone { |
| border: 1px dashed var(--border); |
| border-radius: 8px; |
| padding: 60px 40px; |
| text-align: center; |
| cursor: pointer; |
| transition: all 0.3s ease; |
| background: var(--surface); |
| position: relative; |
| overflow: hidden; |
| } |
| |
| .upload-zone::before { |
| content: ''; |
| position: absolute; |
| inset: 0; |
| background: linear-gradient(135deg, rgba(0,255,136,0.03), transparent); |
| opacity: 0; |
| transition: opacity 0.3s; |
| } |
| |
| .upload-zone:hover, .upload-zone.drag-over { |
| border-color: var(--accent); |
| background: rgba(0,255,136,0.03); |
| } |
| |
| .upload-zone:hover::before, .upload-zone.drag-over::before { opacity: 1; } |
| |
| .upload-icon { |
| font-size: 48px; |
| margin-bottom: 16px; |
| display: block; |
| } |
| |
| .upload-zone h3 { |
| font-size: 18px; |
| font-weight: 700; |
| margin-bottom: 8px; |
| } |
| |
| .upload-zone p { |
| color: var(--muted); |
| font-size: 13px; |
| font-family: 'IBM Plex Mono', monospace; |
| } |
| |
| #fileInput { display: none; } |
| |
| |
| .video-preview { |
| display: none; |
| background: var(--surface); |
| border: 1px solid var(--border); |
| border-radius: 8px; |
| overflow: hidden; |
| margin-bottom: 24px; |
| } |
| |
| .video-preview video { |
| width: 100%; |
| max-height: 300px; |
| display: block; |
| background: #000; |
| } |
| |
| .video-meta { |
| padding: 16px 20px; |
| display: flex; |
| gap: 24px; |
| font-family: 'IBM Plex Mono', monospace; |
| font-size: 12px; |
| color: var(--muted); |
| border-top: 1px solid var(--border); |
| flex-wrap: wrap; |
| } |
| |
| .video-meta span { display: flex; align-items: center; gap: 6px; } |
| .video-meta strong { color: var(--text); } |
| |
| |
| .upload-progress { |
| display: none; |
| margin-bottom: 24px; |
| } |
| |
| .progress-label { |
| display: flex; |
| justify-content: space-between; |
| font-family: 'IBM Plex Mono', monospace; |
| font-size: 12px; |
| color: var(--muted); |
| margin-bottom: 8px; |
| } |
| |
| .progress-bar { |
| height: 3px; |
| background: var(--border); |
| border-radius: 2px; |
| overflow: hidden; |
| } |
| |
| .progress-fill { |
| height: 100%; |
| background: var(--accent); |
| border-radius: 2px; |
| transition: width 0.3s ease; |
| width: 0%; |
| box-shadow: 0 0 8px var(--accent); |
| } |
| |
| |
| .actions { |
| display: flex; |
| gap: 12px; |
| margin-bottom: 32px; |
| flex-wrap: wrap; |
| } |
| |
| .btn { |
| flex: 1; |
| padding: 14px 24px; |
| border: none; |
| border-radius: 6px; |
| font-family: 'Tajawal', sans-serif; |
| font-size: 15px; |
| font-weight: 700; |
| cursor: pointer; |
| transition: all 0.2s ease; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| gap: 8px; |
| min-width: 160px; |
| } |
| |
| .btn:disabled { |
| opacity: 0.3; |
| cursor: not-allowed; |
| transform: none !important; |
| } |
| |
| .btn-primary { |
| background: var(--accent); |
| color: #000; |
| } |
| .btn-primary:not(:disabled):hover { |
| transform: translateY(-2px); |
| box-shadow: 0 8px 24px rgba(0,255,136,0.3); |
| } |
| |
| .btn-danger { |
| background: transparent; |
| color: var(--accent2); |
| border: 1px solid var(--accent2); |
| } |
| .btn-danger:not(:disabled):hover { |
| background: rgba(255,51,102,0.1); |
| transform: translateY(-2px); |
| } |
| |
| .btn-secondary { |
| background: transparent; |
| color: var(--accent3); |
| border: 1px solid var(--accent3); |
| } |
| .btn-secondary:not(:disabled):hover { |
| background: rgba(68,136,255,0.1); |
| transform: translateY(-2px); |
| } |
| |
| |
| .timeline-section { |
| display: none; |
| margin-bottom: 32px; |
| } |
| |
| .timeline-header { |
| display: flex; |
| align-items: center; |
| gap: 12px; |
| margin-bottom: 20px; |
| } |
| |
| .timeline-header h2 { |
| font-size: 16px; |
| font-weight: 700; |
| font-family: 'IBM Plex Mono', monospace; |
| color: var(--accent); |
| letter-spacing: 1px; |
| text-transform: uppercase; |
| } |
| |
| .timeline { |
| position: relative; |
| padding-right: 32px; |
| } |
| |
| .timeline::before { |
| content: ''; |
| position: absolute; |
| right: 11px; |
| top: 0; |
| bottom: 0; |
| width: 1px; |
| background: var(--border); |
| } |
| |
| .timeline-item { |
| position: relative; |
| padding: 0 24px 24px 0; |
| opacity: 0; |
| transform: translateX(10px); |
| transition: all 0.4s ease; |
| } |
| |
| .timeline-item.visible { |
| opacity: 1; |
| transform: translateX(0); |
| } |
| |
| .timeline-dot { |
| position: absolute; |
| right: -21px; |
| top: 4px; |
| width: 14px; |
| height: 14px; |
| border-radius: 50%; |
| border: 2px solid var(--border); |
| background: var(--bg); |
| transition: all 0.3s ease; |
| } |
| |
| .timeline-item.status-done .timeline-dot { |
| background: var(--accent); |
| border-color: var(--accent); |
| box-shadow: 0 0 12px var(--accent); |
| } |
| .timeline-item.status-active .timeline-dot { |
| background: var(--warn); |
| border-color: var(--warn); |
| animation: pulse 1s infinite; |
| } |
| .timeline-item.status-error .timeline-dot { |
| background: var(--accent2); |
| border-color: var(--accent2); |
| } |
| |
| .timeline-content { |
| background: var(--surface); |
| border: 1px solid var(--border); |
| border-radius: 6px; |
| padding: 14px 16px; |
| transition: border-color 0.3s; |
| } |
| |
| .timeline-item.status-done .timeline-content { border-color: rgba(0,255,136,0.3); } |
| .timeline-item.status-active .timeline-content { border-color: rgba(255,170,0,0.3); } |
| .timeline-item.status-error .timeline-content { border-color: rgba(255,51,102,0.3); } |
| |
| .timeline-title { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| margin-bottom: 4px; |
| } |
| |
| .timeline-title strong { |
| font-size: 14px; |
| font-weight: 700; |
| } |
| |
| .timeline-title .tag { |
| font-family: 'IBM Plex Mono', monospace; |
| font-size: 10px; |
| padding: 2px 8px; |
| border-radius: 2px; |
| background: rgba(255,255,255,0.05); |
| color: var(--muted); |
| letter-spacing: 1px; |
| } |
| |
| .timeline-desc { |
| font-size: 13px; |
| color: var(--muted); |
| line-height: 1.5; |
| } |
| |
| .timeline-details { |
| margin-top: 10px; |
| padding: 10px 12px; |
| background: rgba(0,0,0,0.3); |
| border-radius: 4px; |
| font-family: 'IBM Plex Mono', monospace; |
| font-size: 11px; |
| color: var(--muted); |
| max-height: 0; |
| overflow: hidden; |
| transition: max-height 0.4s ease; |
| line-height: 1.8; |
| } |
| |
| .timeline-details.expanded { max-height: 400px; } |
| .timeline-details .highlight { color: var(--accent); } |
| .timeline-details .warn { color: var(--warn); } |
| .timeline-details .danger { color: var(--accent2); } |
| |
| |
| .frame-log { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 4px; |
| margin-top: 8px; |
| } |
| |
| .frame-badge { |
| font-family: 'IBM Plex Mono', monospace; |
| font-size: 10px; |
| padding: 2px 6px; |
| border-radius: 2px; |
| border: 1px solid; |
| } |
| |
| .frame-badge.clean { |
| color: var(--accent); |
| border-color: rgba(0,255,136,0.3); |
| background: rgba(0,255,136,0.05); |
| } |
| |
| .frame-badge.female { |
| color: var(--accent2); |
| border-color: rgba(255,51,102,0.3); |
| background: rgba(255,51,102,0.05); |
| } |
| |
| |
| .results-section { |
| display: none; |
| margin-bottom: 32px; |
| } |
| |
| .result-card { |
| background: var(--surface); |
| border: 1px solid var(--border); |
| border-radius: 8px; |
| overflow: hidden; |
| margin-bottom: 16px; |
| } |
| |
| .result-card-header { |
| padding: 16px 20px; |
| display: flex; |
| align-items: center; |
| gap: 12px; |
| border-bottom: 1px solid var(--border); |
| } |
| |
| .result-card-header h3 { font-size: 15px; font-weight: 700; } |
| |
| .result-card-body { padding: 16px 20px; } |
| |
| .verdict { |
| display: flex; |
| align-items: center; |
| gap: 16px; |
| padding: 20px; |
| border-radius: 6px; |
| margin-bottom: 16px; |
| } |
| |
| .verdict.clean { |
| background: rgba(0,255,136,0.05); |
| border: 1px solid rgba(0,255,136,0.2); |
| } |
| |
| .verdict.female { |
| background: rgba(255,51,102,0.05); |
| border: 1px solid rgba(255,51,102,0.2); |
| } |
| |
| .verdict-icon { font-size: 32px; } |
| .verdict-text h4 { font-size: 18px; font-weight: 900; } |
| .verdict-text p { font-size: 13px; color: var(--muted); margin-top: 4px; } |
| |
| .stats-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); |
| gap: 12px; |
| margin-bottom: 16px; |
| } |
| |
| .stat-box { |
| background: rgba(0,0,0,0.3); |
| border: 1px solid var(--border); |
| border-radius: 6px; |
| padding: 14px; |
| text-align: center; |
| } |
| |
| .stat-box .val { |
| font-family: 'IBM Plex Mono', monospace; |
| font-size: 22px; |
| font-weight: 600; |
| color: var(--accent); |
| display: block; |
| } |
| |
| .stat-box .lbl { |
| font-size: 12px; |
| color: var(--muted); |
| margin-top: 4px; |
| display: block; |
| } |
| |
| |
| .video-timeline { |
| background: rgba(0,0,0,0.3); |
| border-radius: 4px; |
| height: 40px; |
| position: relative; |
| overflow: hidden; |
| margin: 16px 0; |
| } |
| |
| .video-timeline .segment { |
| position: absolute; |
| height: 100%; |
| top: 0; |
| border-radius: 2px; |
| } |
| |
| .video-timeline .seg-clean { |
| background: rgba(0,255,136,0.3); |
| border: 1px solid rgba(0,255,136,0.5); |
| } |
| |
| .video-timeline .seg-female { |
| background: rgba(255,51,102,0.3); |
| border: 1px solid rgba(255,51,102,0.5); |
| } |
| |
| .timeline-label { |
| display: flex; |
| justify-content: space-between; |
| font-family: 'IBM Plex Mono', monospace; |
| font-size: 11px; |
| color: var(--muted); |
| margin-top: 4px; |
| } |
| |
| |
| .download-section { |
| display: none; |
| text-align: center; |
| padding: 32px; |
| background: var(--surface); |
| border: 1px solid rgba(0,255,136,0.2); |
| border-radius: 8px; |
| margin-bottom: 32px; |
| } |
| |
| .download-section h3 { |
| font-size: 20px; |
| font-weight: 900; |
| margin-bottom: 8px; |
| color: var(--accent); |
| } |
| |
| .download-section p { |
| color: var(--muted); |
| font-size: 13px; |
| margin-bottom: 24px; |
| } |
| |
| |
| .spinner { |
| width: 16px; height: 16px; |
| border: 2px solid rgba(0,0,0,0.3); |
| border-top-color: currentColor; |
| border-radius: 50%; |
| animation: spin 0.8s linear infinite; |
| display: inline-block; |
| } |
| |
| @keyframes spin { to { transform: rotate(360deg); } } |
| |
| |
| .alert { |
| padding: 12px 16px; |
| border-radius: 6px; |
| font-size: 13px; |
| margin-bottom: 16px; |
| display: none; |
| } |
| .alert.show { display: block; } |
| .alert-warn { background: rgba(255,170,0,0.1); border: 1px solid rgba(255,170,0,0.3); color: var(--warn); } |
| .alert-error { background: rgba(255,51,102,0.1); border: 1px solid rgba(255,51,102,0.3); color: var(--accent2); } |
| |
| </style> |
| </head> |
| <body> |
|
|
| <div class="container"> |
|
|
| |
| <div class="header"> |
| <div class="header-badge">AI VIDEO FILTER</div> |
| <h1>تنقية <span>الÙيديو</span> الإعلاني</h1> |
| <p>إزالة مقاطع النساء تلقائياً باستخدام BLIP + Florence-2</p> |
| </div> |
|
|
| |
| <div class="api-status"> |
| <div class="status-dot" id="statusDot"></div> |
| <span id="statusText" style="font-family:'IBM Plex Mono',monospace;font-size:12px;color:var(--muted)">جاري الاتصال بالـ API...</span> |
| </div> |
|
|
| |
| <div class="alert alert-warn" id="alertBox"></div> |
|
|
| |
| <div class="upload-zone" id="uploadZone"> |
| <span class="upload-icon">🎬</span> |
| <h3>Ø§Ø³ØØ¨ الÙيديو هنا أو اضغط للاختيار</h3> |
| <p>MP4 / MOV / AVI · ØØ¯ أقصى 200MB</p> |
| <input type="file" id="fileInput" accept="video/*"> |
| </div> |
|
|
| |
| <div class="video-preview" id="videoPreview"> |
| <video id="videoPlayer" controls></video> |
| <div class="video-meta" id="videoMeta"></div> |
| </div> |
|
|
| |
| <div class="upload-progress" id="uploadProgress"> |
| <div class="progress-label"> |
| <span id="progressLabel">جاري Ø§Ù„Ø±ÙØ¹...</span> |
| <span id="progressPct">0%</span> |
| </div> |
| <div class="progress-bar"> |
| <div class="progress-fill" id="progressFill"></div> |
| </div> |
| </div> |
|
|
| |
| <div class="actions" id="actionsBar" style="display:none"> |
| <button class="btn btn-danger" id="btnQuickCheck" disabled> |
| <span>ðŸ”</span> ÙØØµ سريع |
| </button> |
| <button class="btn btn-primary" id="btnAnalyze" disabled> |
| <span>âš™ï¸</span> تØÙ„يل وتقطيع |
| </button> |
| <button class="btn btn-secondary" id="btnReset" onclick="resetAll()"> |
| <span>↺</span> إعادة |
| </button> |
| </div> |
|
|
| |
| <div class="timeline-section" id="timelineSection"> |
| <div class="timeline-header"> |
| <h2>⬡ سجل العمليات</h2> |
| </div> |
| <div class="timeline" id="timeline"></div> |
| </div> |
|
|
| |
| <div class="results-section" id="resultsSection"></div> |
|
|
| |
| <div class="download-section" id="downloadSection"> |
| <h3>✅ الÙيديو النظي٠جاهز</h3> |
| <p id="downloadDesc"></p> |
| <button class="btn btn-primary" id="btnDownload" style="max-width:240px;margin:0 auto"> |
| <span>⬇ï¸</span> تØÙ…يل الÙيديو النظي٠|
| </button> |
| </div> |
|
|
| </div> |
|
|
| <script> |
| const API_BASE = window.location.origin; |
| let currentFile = null; |
| let currentJobId = null; |
| let apiReady = false; |
| |
| |
| async function checkApiStatus() { |
| try { |
| const res = await fetch(`${API_BASE}/health`); |
| const data = await res.json(); |
| const dot = document.getElementById('statusDot'); |
| const txt = document.getElementById('statusText'); |
| |
| if (data.status === 'ready') { |
| dot.className = 'status-dot ready'; |
| txt.textContent = '✅ النماذج جاهزة — BLIP + Florence-2'; |
| txt.style.color = 'var(--accent)'; |
| apiReady = true; |
| } else if (data.status === 'loading') { |
| dot.className = 'status-dot loading'; |
| txt.textContent = `â³ ${data.message}`; |
| txt.style.color = 'var(--warn)'; |
| setTimeout(checkApiStatus, 3000); |
| } else { |
| dot.className = 'status-dot error'; |
| txt.textContent = `⌠${data.message}`; |
| txt.style.color = 'var(--accent2)'; |
| } |
| } catch (e) { |
| const dot = document.getElementById('statusDot'); |
| dot.className = 'status-dot error'; |
| document.getElementById('statusText').textContent = '⌠لا يمكن الاتصال بالـ API'; |
| setTimeout(checkApiStatus, 5000); |
| } |
| } |
| |
| |
| const uploadZone = document.getElementById('uploadZone'); |
| const fileInput = document.getElementById('fileInput'); |
| |
| uploadZone.addEventListener('click', () => fileInput.click()); |
| uploadZone.addEventListener('dragover', e => { e.preventDefault(); uploadZone.classList.add('drag-over'); }); |
| uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('drag-over')); |
| uploadZone.addEventListener('drop', e => { |
| e.preventDefault(); |
| uploadZone.classList.remove('drag-over'); |
| if (e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]); |
| }); |
| fileInput.addEventListener('change', e => { if (e.target.files[0]) handleFile(e.target.files[0]); }); |
| |
| function handleFile(file) { |
| if (!file.type.startsWith('video/')) { |
| showAlert('المل٠ليس Ùيديو! ÙŠÙقبل Ùقط MP4, MOV, AVI', 'error'); |
| return; |
| } |
| if (file.size > 200 * 1024 * 1024) { |
| showAlert('ØØ¬Ù… الÙيديو يتجاوز 200MB', 'warn'); |
| return; |
| } |
| |
| currentFile = file; |
| showVideoPreview(file); |
| showTimeline(); |
| document.getElementById('actionsBar').style.display = 'flex'; |
| document.getElementById('btnQuickCheck').disabled = !apiReady; |
| document.getElementById('btnAnalyze').disabled = !apiReady; |
| addTimelineItem('ready', 'done', '⸠بانتظار الإجراء', 'READY', |
| 'الÙيديو جاهز. اختر "ÙØØµ سريع" أو "تØÙ„يل وتقطيع".'); |
| } |
| |
| function showVideoPreview(file) { |
| const preview = document.getElementById('videoPreview'); |
| const player = document.getElementById('videoPlayer'); |
| const meta = document.getElementById('videoMeta'); |
| |
| player.src = URL.createObjectURL(file); |
| preview.style.display = 'block'; |
| uploadZone.style.display = 'none'; |
| |
| player.onloadedmetadata = () => { |
| const dur = formatDuration(player.duration); |
| const size = (file.size / 1024 / 1024).toFixed(1); |
| meta.innerHTML = ` |
| <span>🎬 <strong>${file.name}</strong></span> |
| <span>Ⱡمدة: <strong>${dur}</strong></span> |
| <span>💾 Ø§Ù„ØØ¬Ù…: <strong>${size} MB</strong></span> |
| <span>📠${player.videoWidth}×${player.videoHeight}</span> |
| `; |
| }; |
| } |
| |
| |
| function analyzeVideo(file) { |
| const uploadProgress = document.getElementById('uploadProgress'); |
| const progressFill = document.getElementById('progressFill'); |
| const progressPct = document.getElementById('progressPct'); |
| const progressLabel = document.getElementById('progressLabel'); |
| |
| uploadProgress.style.display = 'block'; |
| showTimeline(); |
| addTimelineItem('analyze', 'active', '⚙️ بدء التحليل الكامل', 'ANALYZING', |
| `جاري رفع ${file.name} وتحليله...`); |
| |
| const xhr = new XMLHttpRequest(); |
| const formData = new FormData(); |
| formData.append('file', file); |
| |
| xhr.upload.onprogress = e => { |
| if (e.lengthComputable) { |
| const pct = Math.round(e.loaded / e.total * 100); |
| progressFill.style.width = pct + '%'; |
| progressPct.textContent = pct + '%'; |
| progressLabel.textContent = `جاري الرفع... ${(e.loaded/1024/1024).toFixed(1)}MB / ${(e.total/1024/1024).toFixed(1)}MB`; |
| } |
| }; |
| |
| xhr.onload = () => { |
| uploadProgress.style.display = 'none'; |
| try { |
| const data = JSON.parse(xhr.responseText || '{}'); |
| if (xhr.status !== 200) { |
| updateTimelineItem('analyze', 'error', `❌ فشل التحليل: ${data.detail || xhr.status}`); |
| setButtonsDisabled(false); |
| return; |
| } |
| const elapsed = data.analysis_time || '—'; |
| currentJobId = data.output_job_id || null; |
| updateTimelineItem('analyze', 'done', `✅ اكتمل التحليل في ${elapsed}s`); |
| handleAnalysisResult(data, elapsed); |
| } catch(e) { |
| updateTimelineItem('analyze', 'error', '❌ خطأ في تحليل الاستجابة'); |
| } |
| setButtonsDisabled(false); |
| }; |
| |
| xhr.onerror = () => { |
| uploadProgress.style.display = 'none'; |
| updateTimelineItem('analyze', 'error', '❌ خطأ في الاتصال'); |
| setButtonsDisabled(false); |
| }; |
| |
| xhr.open('POST', `${API_BASE}/analyze-video`); |
| xhr.send(formData); |
| } |
| document.getElementById('btnQuickCheck').addEventListener('click', async () => { |
| if (!currentFile || !apiReady) return; |
| setButtonsDisabled(true); |
| addTimelineItem('quickcheck', 'active', 'ðŸ” ÙØØµ سريع للÙيديو', 'QUICK-CHECK', |
| 'جاري ÙØØµ أول frame من الÙيديو للتØÙ‚Ù‚ السريع من وجود نساء...'); |
| |
| try { |
| |
| const canvas = document.createElement('canvas'); |
| const video = document.getElementById('videoPlayer'); |
| video.currentTime = 1; |
| |
| await new Promise(r => { video.onseeked = r; }); |
| |
| canvas.width = video.videoWidth; |
| canvas.height = video.videoHeight; |
| canvas.getContext('2d').drawImage(video, 0, 0); |
| |
| canvas.toBlob(async (blob) => { |
| const formData = new FormData(); |
| formData.append('file', blob, 'frame.jpg'); |
| |
| const res = await fetch(`${API_BASE}/analyze-file`, { method: 'POST', body: formData }); |
| const data = await res.json(); |
| |
| if (data.decision === 'BLOCK' || data.decision === 'block') { |
| updateTimelineItem('quickcheck', 'done', |
| `🔴 تم اكتشا٠امرأة ÙÙŠ الـ frame الأول — ÙŠÙØØªÙ…Ù„ وجود نساء ÙÙŠ الÙيديو`); |
| addTimelineItem('qc-result', 'active', 'âš ï¸ ØªØØ°ÙŠØ±: يوجد Ù…ØØªÙˆÙ‰ أنثوي', 'DETECTED', |
| 'Ø§Ù„ÙØØµ السريع اكتش٠امرأة. ÙŠÙÙ†ØµØ Ø¨Ø§Ù„Ù…ØªØ§Ø¨Ø¹Ø© إلى "تØÙ„يل وتقطيع" للمعالجة الكاملة.'); |
| } else { |
| updateTimelineItem('quickcheck', 'done', |
| `🟢 الـ frame الأول نظي٠— لا يوجد نساء ÙÙŠ البداية`); |
| addTimelineItem('qc-result', 'done', '✅ Ø§Ù„ÙØØµ الأولي نظيÙ', 'CLEAN', |
| 'لم ÙŠÙÙƒØªØ´Ù Ù…ØØªÙˆÙ‰ أنثوي ÙÙŠ الـ frame الأول. ÙŠÙÙ†ØµØ Ø¨Ø§Ù„Ù…ØªØ§Ø¨Ø¹Ø© لتØÙ„يل كامل الÙيديو.'); |
| } |
| |
| setButtonsDisabled(false); |
| }, 'image/jpeg', 0.9); |
| |
| } catch (e) { |
| updateTimelineItem('quickcheck', 'error', `⌠خطأ: ${e.message}`); |
| setButtonsDisabled(false); |
| } |
| }); |
| |
| |
| document.getElementById('btnAnalyze').addEventListener('click', () => { |
| if (!currentFile || !apiReady) return; |
| setButtonsDisabled(true); |
| analyzeVideo(currentFile); |
| }); |
| |
| |
| function handleAnalysisResult(data, elapsed) { |
| const resultsSection = document.getElementById('resultsSection'); |
| resultsSection.style.display = 'block'; |
| |
| if (!data.has_female) { |
| |
| addTimelineItem('result', 'done', '🟢 الÙيديو نظي٠تماماً', 'CLEAN', |
| 'لم ÙŠÙكتش٠أي Ù…ØØªÙˆÙ‰ أنثوي ÙÙŠ الÙيديو كاملاً. الÙيديو جاهز للنشر.'); |
| |
| resultsSection.innerHTML = ` |
| <div class="result-card"> |
| <div class="result-card-header"> |
| <span>📊</span> |
| <h3>نتيجة التØÙ„يل</h3> |
| </div> |
| <div class="result-card-body"> |
| <div class="verdict clean"> |
| <div class="verdict-icon">✅</div> |
| <div class="verdict-text"> |
| <h4 style="color:var(--accent)">الÙيديو نظيÙ</h4> |
| <p>لا ÙŠØØªÙˆÙŠ Ø¹Ù„Ù‰ أي Ù…ØØªÙˆÙ‰ أنثوي</p> |
| </div> |
| </div> |
| <div class="stats-grid"> |
| <div class="stat-box"> |
| <span class="val">${data.analysis_log ? data.analysis_log.length : '—'}</span> |
| <span class="lbl">frames تم تØÙ„يلها</span> |
| </div> |
| <div class="stat-box"> |
| <span class="val">${data.analysis_time || elapsed || '—'}s</span> |
| <span class="lbl">وقت التØÙ„يل</span> |
| </div> |
| <div class="stat-box"> |
| <span class="val" style="color:var(--accent)">0</span> |
| <span class="lbl">مقاطع Ù…ØØ°ÙˆÙØ©</span> |
| </div> |
| </div> |
| ${renderFrameLog(data.analysis_log)} |
| </div> |
| </div> |
| `; |
| return; |
| } |
| |
| |
| const femaleSegs = data.female_segments || []; |
| const keptSegs = data.kept_segments || []; |
| const totalRemoved = data.total_removed_sec || 0; |
| |
| if (femaleSegs.length > 0) { |
| addTimelineItem('cutting', 'done', `âœ‚ï¸ ØªÙ… تقطيع ${femaleSegs.length} مقطع`, 'CUTTING', |
| `تم ØØ°Ù ${totalRemoved.toFixed(1)} ثانية ØªØØªÙˆÙŠ Ø¹Ù„Ù‰ نساء وبناء الÙيديو النظيÙ.`); |
| } |
| |
| addTimelineItem('result', data.output_available ? 'done' : 'active', |
| data.output_available ? '📦 الÙيديو النظي٠جاهز' : 'âš ï¸ Ø§Ù„Ùيديو كله ÙŠØØªÙˆÙŠ Ù†Ø³Ø§Ø¡', |
| 'RESULT', data.message); |
| |
| |
| const totalDur = femaleSegs.length > 0 && keptSegs.length > 0 |
| ? (keptSegs[keptSegs.length-1][1] || 0) |
| : 0; |
| |
| resultsSection.innerHTML = ` |
| <div class="result-card"> |
| <div class="result-card-header"> |
| <span>📊</span> |
| <h3>نتيجة التØÙ„يل</h3> |
| </div> |
| <div class="result-card-body"> |
| <div class="verdict female"> |
| <div class="verdict-icon">âš ï¸</div> |
| <div class="verdict-text"> |
| <h4 style="color:var(--accent2)">تم Ø§ÙƒØªØ´Ø§Ù Ù…ØØªÙˆÙ‰ أنثوي</h4> |
| <p>${femaleSegs.length} مقطع ÙŠØØªÙˆÙŠ Ø¹Ù„Ù‰ نساء — تم ØØ°Ùها</p> |
| </div> |
| </div> |
| |
| <div class="stats-grid"> |
| <div class="stat-box"> |
| <span class="val" style="color:var(--accent2)">${femaleSegs.length}</span> |
| <span class="lbl">مقاطع Ù…ØØ°ÙˆÙØ©</span> |
| </div> |
| <div class="stat-box"> |
| <span class="val" style="color:var(--accent2)">${totalRemoved.toFixed(1)}s</span> |
| <span class="lbl">مدة Ù…ØØ°ÙˆÙØ©</span> |
| </div> |
| <div class="stat-box"> |
| <span class="val">${data.analysis_log ? data.analysis_log.length : '—'}</span> |
| <span class="lbl">frames تم تØÙ„يلها</span> |
| </div> |
| <div class="stat-box"> |
| <span class="val">${data.analysis_time || elapsed || '—'}s</span> |
| <span class="lbl">وقت التØÙ„يل</span> |
| </div> |
| </div> |
| |
| ${renderVideoTimeline(femaleSegs, keptSegs, totalDur)} |
| ${renderSegmentsTable(femaleSegs, 'female')} |
| ${renderFrameLog(data.analysis_log)} |
| </div> |
| </div> |
| `; |
| |
| if (data.output_available && currentJobId) { |
| const dl = document.getElementById('downloadSection'); |
| dl.style.display = 'block'; |
| document.getElementById('downloadDesc').textContent = |
| `تم ØØ°Ù ${totalRemoved.toFixed(1)} ثانية — الÙيديو النظي٠جاهز للتØÙ…يل`; |
| |
| document.getElementById('btnDownload').onclick = () => { |
| window.location.href = `${API_BASE}/download/${currentJobId}`; |
| }; |
| } |
| } |
| |
| |
| function renderVideoTimeline(femaleSegs, keptSegs, totalDur) { |
| if (!totalDur || totalDur === 0) return ''; |
| const allSegs = [ |
| ...femaleSegs.map(s => ({start: s[0], end: s[1], type: 'female'})), |
| ...keptSegs.map(s => ({start: s[0], end: s[1], type: 'clean'})) |
| ].sort((a,b) => a.start - b.start); |
| |
| const bars = allSegs.map(s => { |
| const left = (s.start / totalDur * 100).toFixed(1); |
| const width = ((s.end - s.start) / totalDur * 100).toFixed(1); |
| return `<div class="segment seg-${s.type}" style="left:${left}%;width:${width}%" title="${s.type}: ${s.start.toFixed(1)}s - ${s.end.toFixed(1)}s"></div>`; |
| }).join(''); |
| |
| return ` |
| <div style="margin:16px 0"> |
| <div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--muted);margin-bottom:6px"> |
| خريطة الÙيديو — <span style="color:var(--accent)">■نظيÙ</span> <span style="color:var(--accent2)">â– Ù…ØØ°ÙˆÙ</span> |
| </div> |
| <div class="video-timeline">${bars}</div> |
| <div class="timeline-label"><span>0s</span><span>${totalDur.toFixed(0)}s</span></div> |
| </div> |
| `; |
| } |
| |
| function renderSegmentsTable(segs, type) { |
| if (!segs || segs.length === 0) return ''; |
| const color = type === 'female' ? 'var(--accent2)' : 'var(--accent)'; |
| const rows = segs.map((s, i) => ` |
| <tr> |
| <td style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:${color};padding:6px 8px">#${i+1}</td> |
| <td style="font-family:'IBM Plex Mono',monospace;font-size:11px;padding:6px 8px">${s[0].toFixed(1)}s</td> |
| <td style="font-family:'IBM Plex Mono',monospace;font-size:11px;padding:6px 8px">${s[1].toFixed(1)}s</td> |
| <td style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--muted);padding:6px 8px">${(s[1]-s[0]).toFixed(1)}s</td> |
| </tr> |
| `).join(''); |
| |
| return ` |
| <div style="margin-top:12px"> |
| <div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--muted);margin-bottom:6px">المقاطع Ø§Ù„Ù…ØØ°ÙˆÙØ©</div> |
| <table style="width:100%;border-collapse:collapse;background:rgba(0,0,0,0.3);border-radius:4px;overflow:hidden"> |
| <tr style="background:rgba(255,255,255,0.03)"> |
| <th style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--muted);padding:6px 8px;text-align:right;font-weight:400">#</th> |
| <th style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--muted);padding:6px 8px;text-align:right;font-weight:400">البداية</th> |
| <th style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--muted);padding:6px 8px;text-align:right;font-weight:400">النهاية</th> |
| <th style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--muted);padding:6px 8px;text-align:right;font-weight:400">المدة</th> |
| </tr> |
| ${rows} |
| </table> |
| </div> |
| `; |
| } |
| |
| function renderFrameLog(log) { |
| if (!log || log.length === 0) return ''; |
| const badges = log.map(f => |
| `<span class="frame-badge ${f.has_female ? 'female' : 'clean'}" title="${f.reason || ''}">${f.second}s</span>` |
| ).join(''); |
| return ` |
| <div style="margin-top:12px"> |
| <div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--muted);margin-bottom:6px"> |
| سجل الـ frames — <span style="color:var(--accent)">■نظيÙ</span> <span style="color:var(--accent2)">â– ÙŠØØªÙˆÙŠ Ù†Ø³Ø§Ø¡</span> |
| </div> |
| <div class="frame-log">${badges}</div> |
| </div> |
| `; |
| } |
| |
| |
| function showTimeline() { |
| document.getElementById('timelineSection').style.display = 'block'; |
| } |
| |
| function addTimelineItem(id, status, title, tag, desc) { |
| const timeline = document.getElementById('timeline'); |
| const existing = document.getElementById(`tl-${id}`); |
| if (existing) { updateTimelineItem(id, status, desc); return; } |
| |
| const item = document.createElement('div'); |
| item.className = `timeline-item status-${status}`; |
| item.id = `tl-${id}`; |
| item.innerHTML = ` |
| <div class="timeline-dot"></div> |
| <div class="timeline-content"> |
| <div class="timeline-title"> |
| <strong>${title}</strong> |
| <span class="tag">${tag}</span> |
| ${status === 'active' ? '<div class="spinner"></div>' : ''} |
| </div> |
| <div class="timeline-desc" id="tl-desc-${id}">${desc}</div> |
| </div> |
| `; |
| timeline.appendChild(item); |
| setTimeout(() => item.classList.add('visible'), 50); |
| } |
| |
| function updateTimelineItem(id, status, desc) { |
| const item = document.getElementById(`tl-${id}`); |
| if (!item) return; |
| item.className = `timeline-item status-${status} visible`; |
| const descEl = document.getElementById(`tl-desc-${id}`); |
| if (descEl && desc) descEl.textContent = desc; |
| |
| const spinner = item.querySelector('.spinner'); |
| if (spinner) spinner.remove(); |
| } |
| |
| |
| function formatDuration(sec) { |
| const m = Math.floor(sec / 60); |
| const s = Math.floor(sec % 60); |
| return `${m}:${s.toString().padStart(2,'0')}`; |
| } |
| |
| function showAlert(msg, type) { |
| const box = document.getElementById('alertBox'); |
| box.textContent = msg; |
| box.className = `alert alert-${type} show`; |
| setTimeout(() => box.classList.remove('show'), 5000); |
| } |
| |
| function setButtonsDisabled(disabled) { |
| document.getElementById('btnQuickCheck').disabled = disabled || !apiReady; |
| document.getElementById('btnAnalyze').disabled = disabled || !apiReady; |
| } |
| |
| function resetAll() { |
| currentFile = null; |
| currentJobId = null; |
| document.getElementById('uploadZone').style.display = 'block'; |
| document.getElementById('videoPreview').style.display = 'none'; |
| document.getElementById('actionsBar').style.display = 'none'; |
| document.getElementById('timelineSection').style.display = 'none'; |
| document.getElementById('resultsSection').style.display = 'none'; |
| document.getElementById('downloadSection').style.display = 'none'; |
| document.getElementById('timeline').innerHTML = ''; |
| document.getElementById('resultsSection').innerHTML = ''; |
| document.getElementById('videoPlayer').src = ''; |
| fileInput.value = ''; |
| } |
| |
| |
| checkApiStatus(); |
| </script> |
| </body> |
| </html> |