count-model / main.js
RehamAAhmed's picture
Upload 2 files
f72a723 verified
Raw
History Blame Contribute Delete
19.5 kB
document.addEventListener('DOMContentLoaded', () => {
// DOM Elements
const uploadForm = document.getElementById('upload-form');
const promptInput = document.getElementById('prompt-input');
const beforeInput = document.getElementById('video-before');
const afterInput = document.getElementById('video-after');
const beforeZone = document.getElementById('upload-before-zone');
const afterZone = document.getElementById('upload-after-zone');
const beforeFileName = document.getElementById('before-file-name');
const afterFileName = document.getElementById('after-file-name');
const speedModeSelect = document.getElementById('speed-mode-select');
const fpsSelect = document.getElementById('fps-select');
const geminiKeyInput = document.getElementById('gemini-key-input');
const uploadBtn = document.getElementById('upload-btn');
const startBtn = document.getElementById('start-btn');
const resetBtn = document.getElementById('reset-btn');
const processingView = document.getElementById('processing-view');
const progressStatus = document.getElementById('progress-status');
const progressFill = document.getElementById('progress-fill');
const progressPct = document.getElementById('progress-pct');
const backendStatusText = document.getElementById('backend-status-text');
const resultsPlaceholder = document.getElementById('results-placeholder');
const resultsView = document.getElementById('results-view');
const outVideoBefore = document.getElementById('out-video-before');
const outVideoAfter = document.getElementById('out-video-after');
const assetCardsContainer = document.getElementById('asset-cards-container');
let pollInterval = null;
// --- Load saved Gemini API Key ---
const savedKey = localStorage.getItem('gemini_api_key');
if (savedKey) {
geminiKeyInput.value = savedKey;
}
// --- File Drag & Drop Listeners ---
setupDragAndDrop(beforeZone, beforeInput, beforeFileName, "Before Renting Video Selected");
setupDragAndDrop(afterZone, afterInput, afterFileName, "After Renting Video Selected");
function setupDragAndDrop(zone, input, display, successMsg) {
zone.addEventListener('click', () => input.click());
input.addEventListener('change', (e) => {
if (input.files.length > 0) {
display.textContent = `${input.files[0].name} (${(input.files[0].size / 1e6).toFixed(1)} MB)`;
zone.style.borderColor = 'var(--success)';
}
});
zone.addEventListener('dragover', (e) => {
e.preventDefault();
zone.classList.add('drag-over');
});
zone.addEventListener('dragleave', () => {
zone.classList.remove('drag-over');
});
zone.addEventListener('drop', (e) => {
e.preventDefault();
zone.classList.remove('drag-over');
if (e.dataTransfer.files.length > 0) {
input.files = e.dataTransfer.files;
display.textContent = `${input.files[0].name} (${(input.files[0].size / 1e6).toFixed(1)} MB)`;
zone.style.borderColor = 'var(--success)';
}
});
}
// --- Upload Videos ---
uploadForm.addEventListener('submit', async (e) => {
e.preventDefault();
if (beforeInput.files.length === 0 || afterInput.files.length === 0) {
alert('Please select both "Before" and "After" renting videos first.');
return;
}
const prompt = promptInput.value.trim();
if (!prompt) {
alert('Please define target inspection items prompt.');
return;
}
const formData = new FormData();
formData.append('video_before', beforeInput.files[0]);
formData.append('video_after', afterInput.files[0]);
formData.append('prompt', prompt);
try {
uploadBtn.disabled = true;
uploadBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Uploading...';
backendStatusText.textContent = "Uploading videos...";
const response = await fetch('/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Upload failed.');
}
uploadBtn.innerHTML = '<i class="fa-solid fa-check"></i> Uploaded!';
uploadBtn.style.background = 'var(--success)';
// Enable Start button
startBtn.removeAttribute('disabled');
startBtn.classList.remove('btn-disabled');
backendStatusText.textContent = "Ready to analyze";
alert('Videos uploaded successfully. You can now start the evaluation.');
} catch (error) {
console.error(error);
alert(`Error: ${error.message}`);
uploadBtn.disabled = false;
uploadBtn.innerHTML = '<i class="fa-solid fa-arrow-up-from-bracket"></i> Upload Videos';
backendStatusText.textContent = "Upload failed";
}
});
// --- Start Tracking / Evaluation ---
startBtn.addEventListener('click', async () => {
const prompt = promptInput.value.trim();
const geminiKey = geminiKeyInput.value.trim();
const speedMode = speedModeSelect.value;
const fps = fpsSelect.value;
// Securely cache Gemini API key in browser
if (geminiKey) {
localStorage.setItem('gemini_api_key', geminiKey);
} else {
localStorage.removeItem('gemini_api_key');
}
try {
startBtn.disabled = true;
startBtn.classList.add('btn-disabled');
processingView.classList.remove('hidden');
backendStatusText.textContent = "Running evaluation...";
const response = await fetch('/process', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
prompt: prompt,
gemini_api_key: geminiKey,
speed_mode: speedMode,
fps: parseFloat(fps)
})
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to start process.');
}
// Start polling status
startStatusPolling();
} catch (error) {
console.error(error);
alert(`Error: ${error.message}`);
processingView.classList.add('hidden');
startBtn.disabled = false;
startBtn.classList.remove('btn-disabled');
backendStatusText.textContent = "Error occurred";
}
});
// --- Reset ---
resetBtn.addEventListener('click', async () => {
if (confirm('Are you sure you want to reset and clear all data?')) {
try {
await fetch('/reset', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ force: true })
});
// Clear intervals
clearInterval(pollInterval);
pollInterval = null;
// Reset inputs and displays
uploadForm.reset();
beforeInput.value = '';
afterInput.value = '';
beforeZone.style.borderColor = 'var(--border-color)';
afterZone.style.borderColor = 'var(--border-color)';
beforeFileName.textContent = 'Drag & drop or click to upload (.mp4)';
afterFileName.textContent = 'Drag & drop or click to upload (.mp4)';
// Retain Gemini Key
const savedKey = localStorage.getItem('gemini_api_key');
if (savedKey) {
geminiKeyInput.value = savedKey;
}
uploadBtn.disabled = false;
uploadBtn.innerHTML = '<i class="fa-solid fa-arrow-up-from-bracket"></i> Upload Videos';
uploadBtn.style.background = 'var(--primary)';
startBtn.disabled = true;
startBtn.classList.add('btn-disabled');
processingView.classList.add('hidden');
resultsView.classList.add('hidden');
resultsPlaceholder.classList.remove('hidden');
outVideoBefore.src = '';
outVideoAfter.src = '';
assetCardsContainer.innerHTML = '';
backendStatusText.textContent = "System Ready";
} catch (error) {
console.error(error);
alert('Reset failed.');
}
}
});
// --- Status Polling ---
function startStatusPolling() {
if (pollInterval) clearInterval(pollInterval);
pollInterval = setInterval(async () => {
try {
const response = await fetch('/status');
const data = await response.json();
if (data.error) {
clearInterval(pollInterval);
alert(`Evaluation Error: ${data.error}`);
processingView.classList.add('hidden');
startBtn.disabled = false;
startBtn.classList.remove('btn-disabled');
backendStatusText.textContent = "Analysis Failed";
return;
}
// Update progress overlay
progressStatus.textContent = data.status;
progressFill.style.width = `${data.percent}%`;
progressPct.textContent = `${data.percent}%`;
if (!data.running && data.percent === 100 && data.result) {
clearInterval(pollInterval);
pollInterval = null;
displayResults(data.result);
}
} catch (error) {
console.error('Polling error:', error);
}
}, 1500);
}
// --- Display Results ---
function displayResults(result) {
// Hide processing and placeholder
processingView.classList.add('hidden');
resultsPlaceholder.classList.add('hidden');
resultsView.classList.remove('hidden');
backendStatusText.textContent = "Evaluation Complete";
// Render Summary Banner
const summaryBanner = document.getElementById('summary-banner');
if (summaryBanner && result.summary) {
const s = result.summary;
// Format elapsed time (e.g. "45.2s" or "1m 15s")
let timeStr = "";
if (s.elapsed_time < 60) {
timeStr = `${s.elapsed_time}s`;
} else {
const mins = Math.floor(s.elapsed_time / 60);
const secs = Math.round(s.elapsed_time % 60);
timeStr = `${mins}m ${secs}s`;
}
// Compensation judgment logic: if any item is Missing or Damaged, tenant must pay compensation
const compensationRequired = (s.statuses.Missing || 0) > 0 || (s.statuses["Potential Damage / Altered"] || 0) > 0;
const verdictTitle = compensationRequired ? "Tenant Must Pay Compensation" : "No Compensation Required";
const verdictColor = compensationRequired ? "var(--danger)" : "var(--success)";
const verdictIcon = compensationRequired ? "fa-solid fa-circle-exclamation" : "fa-solid fa-circle-check";
const verdictDesc = compensationRequired
? "Damage or missing assets detected. Tenant is liable for compensation."
: "All tracked assets are intact. No compensation required.";
// Create unique list of all discovered item labels
const uniqueLabels = Array.from(new Set([...s.before_items, ...s.after_items]));
const labelsHtml = uniqueLabels.length > 0
? uniqueLabels.map(lbl => `<span class="discovered-tag">${lbl}</span>`).join('')
: '<span class="text-secondary">None</span>';
summaryBanner.innerHTML = `
<div class="summary-card highlight" style="grid-column: span 2; border-color: ${verdictColor}; background: linear-gradient(135deg, rgba(20, 24, 33, 0.85), ${compensationRequired ? 'rgba(255, 82, 82, 0.05)' : 'rgba(0, 219, 139, 0.05)'});">
<span class="summary-card-val" style="color: ${verdictColor}; font-size: 1.35rem; display: flex; align-items: center; gap: 8px;">
<i class="${verdictIcon}"></i> ${verdictTitle}
</span>
<span class="summary-card-label" style="margin-top: 5px;">AI Final Verdict</span>
<span style="font-size: 0.82rem; color: var(--text-secondary); margin-top: 4px; line-height: 1.3;">${verdictDesc}</span>
</div>
<div class="summary-card">
<span class="summary-card-val time">${timeStr}</span>
<span class="summary-card-label">Time Taken</span>
</div>
<div class="summary-card">
<span class="summary-card-val before">${s.total_before}</span>
<span class="summary-card-label">Assets (Before)</span>
</div>
<div class="summary-card">
<span class="summary-card-val after">${s.total_after}</span>
<span class="summary-card-label">Assets (After)</span>
</div>
<div class="summary-card">
<span class="summary-card-val missing">${s.statuses.Missing || 0}</span>
<span class="summary-card-label">Missing Items</span>
</div>
<div class="summary-card">
<span class="summary-card-val" style="color: var(--warning);">${s.statuses["Potential Damage / Altered"] || 0}</span>
<span class="summary-card-label">Damaged Items</span>
</div>
<div class="summary-card">
<span class="summary-card-val" style="color: var(--success);">${(s.statuses.Intact || 0) + (s.statuses["Fair / Minor Change"] || 0)}</span>
<span class="summary-card-label">Intact / Fair</span>
</div>
<div class="discovered-labels-list">
<strong><i class="fa-solid fa-list-check"></i> Discovered Items:</strong>
${labelsHtml}
</div>
`;
}
// Set video sources (add timestamp parameter to bypass browser cache)
const t = new Date().getTime();
outVideoBefore.src = `${result.annotated_before}?t=${t}`;
outVideoAfter.src = `${result.annotated_after}?t=${t}`;
outVideoBefore.load();
outVideoAfter.load();
// Render Asset Cards
assetCardsContainer.innerHTML = '';
result.report.forEach(item => {
const card = document.createElement('div');
card.className = 'asset-card';
// Top Row of card: Info, Crops, and Status
const mainDiv = document.createElement('div');
mainDiv.className = 'asset-card-main';
// 1. Info section
const infoDiv = document.createElement('div');
infoDiv.className = 'asset-info';
const labelH3 = document.createElement('h3');
labelH3.className = 'asset-label';
labelH3.textContent = item.label;
const scoreSpan = document.createElement('span');
scoreSpan.className = 'asset-score';
scoreSpan.textContent = item.before_crop && item.after_crop ? `Similarity: ${item.similarity}%` : '';
infoDiv.appendChild(labelH3);
infoDiv.appendChild(scoreSpan);
mainDiv.appendChild(infoDiv);
// 2. Before Crop
const beforeCropDiv = createCropView(item.before_crop, "Before");
mainDiv.appendChild(beforeCropDiv);
// 3. After Crop
const afterCropDiv = createCropView(item.after_crop, "After");
mainDiv.appendChild(afterCropDiv);
// 4. Status Badge
const badgeDiv = document.createElement('div');
const badge = document.createElement('span');
badge.className = `status-badge ${getBadgeClass(item.status)}`;
badge.textContent = item.status;
badgeDiv.appendChild(badge);
mainDiv.appendChild(badgeDiv);
card.appendChild(mainDiv);
// Bottom Row of card: Explanation (AI-generated or fallback)
if (item.explanation) {
const explanationDiv = document.createElement('div');
explanationDiv.className = 'asset-explanation';
const icon = document.createElement('i');
// Use robot icon if Gemini was used, sparkles icon if local CV fallback was used
icon.className = item.ai_evaluated ? 'fa-solid fa-robot explanation-icon' : 'fa-solid fa-wand-magic-sparkles explanation-icon';
const textSpan = document.createElement('span');
textSpan.textContent = item.explanation;
explanationDiv.appendChild(icon);
explanationDiv.appendChild(textSpan);
card.appendChild(explanationDiv);
}
assetCardsContainer.appendChild(card);
});
}
function createCropView(src, label) {
const container = document.createElement('div');
container.className = 'crop-img-container';
const title = document.createElement('span');
title.textContent = label;
container.appendChild(title);
const frame = document.createElement('div');
frame.className = 'crop-frame';
if (src) {
const img = document.createElement('img');
// Add cache-busting timestamp
img.src = `${src}?t=${new Date().getTime()}`;
img.alt = label;
frame.appendChild(img);
} else {
const icon = document.createElement('i');
icon.className = 'fa-solid fa-circle-question crop-missing-icon';
frame.appendChild(icon);
}
container.appendChild(frame);
return container;
}
function getBadgeClass(status) {
switch (status) {
case 'Intact':
return 'badge-intact';
case 'Fair / Minor Change':
return 'badge-fair';
case 'Potential Damage / Altered':
return 'badge-damaged';
case 'Missing':
return 'badge-missing';
case 'New Item':
return 'badge-new';
default:
return '';
}
}
});