aibuild / index.html
webolavo's picture
Update index.html
52fd7ac verified
<!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;
}
/* ─── Grid Background ─── */
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 ─── */
.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 ─── */
.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 ─── */
.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 ─── */
.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 ─── */
.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);
}
/* ─── Action Buttons ─── */
.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 ─── */
.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 ─── */
.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 ─── */
.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 Bar ─── */
.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 ─── */
.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 ─── */
.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 ─── */
.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">
<!-- Header -->
<div class="header">
<div class="header-badge">AI VIDEO FILTER</div>
<h1>تنقية <span>الفيديو</span> الإعلاني</h1>
<p>إزالة مقاطع النساء تلقائياً باستخدام BLIP + Florence-2</p>
</div>
<!-- API Status -->
<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>
<!-- Alert -->
<div class="alert alert-warn" id="alertBox"></div>
<!-- Upload Zone -->
<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>
<!-- Video Preview -->
<div class="video-preview" id="videoPreview">
<video id="videoPlayer" controls></video>
<div class="video-meta" id="videoMeta"></div>
</div>
<!-- Upload Progress -->
<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>
<!-- Action Buttons -->
<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>
<!-- Timeline -->
<div class="timeline-section" id="timelineSection">
<div class="timeline-header">
<h2>⬡ سجل العمليات</h2>
</div>
<div class="timeline" id="timeline"></div>
</div>
<!-- Results -->
<div class="results-section" id="resultsSection"></div>
<!-- Download -->
<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;
// ─── API Status Check ──────────────────────────────────────────
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);
}
}
// ─── Upload Zone ───────────────────────────────────────────────
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>
`;
};
}
// ─── Upload with Progress ──────────────────────────────────────
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 {
// نستخرج أول frame من الفيديو
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);
}
});
// ─── Full Analysis ─────────────────────────────────────────────
document.getElementById('btnAnalyze').addEventListener('click', () => {
if (!currentFile || !apiReady) return;
setButtonsDisabled(true);
analyzeVideo(currentFile);
});
// ─── Handle Analysis Result ───────────────────────────────────
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}`;
};
}
}
// ─── Render Helpers ────────────────────────────────────────────
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>
`;
}
// ─── Timeline Helpers ──────────────────────────────────────────
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;
// إزالة spinner
const spinner = item.querySelector('.spinner');
if (spinner) spinner.remove();
}
// ─── Utils ─────────────────────────────────────────────────────
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 = '';
}
// ─── Init ──────────────────────────────────────────────────────
checkApiStatus();
</script>
</body>
</html>