Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <!-- SPA Bootstrap: load dashboard without changing URL --> | |
| <script> | |
| if (sessionStorage.getItem('funky_run')) { | |
| document.documentElement.style.display = 'none'; | |
| fetch('/vehicles.html') | |
| .then(function (r) { return r.text(); }) | |
| .then(function (html) { | |
| document.open(); | |
| document.write(html); | |
| document.close(); | |
| }); | |
| } | |
| </script> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>UrbanFlow</title> | |
| <link rel="icon" type="image/svg+xml" href="rf.png"> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <link | |
| href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Montserrat:wght@400;500;600;700;800;900&display=swap" | |
| rel="stylesheet"> | |
| <style> | |
| :root { | |
| --cocoa: #8b5e3c; | |
| --cocoa-l: #c89a6c; | |
| --cocoa-xl: #d4b08a; | |
| --t1: #f0ece6; | |
| --t2: #a89f97; | |
| --border: #2a2a2a; | |
| } | |
| body { | |
| font-family: 'Montserrat', sans-serif; | |
| background-color: #000000; | |
| color: var(--t1); | |
| } | |
| .fade-in { | |
| animation: fadeIn 0.4s ease-in-out forwards; | |
| } | |
| @keyframes fadeIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(10px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| /* Executive Overrides */ | |
| .traffic-dynamics-card { | |
| background-color: #0a0a0a ; | |
| border: 2px solid var(--cocoa) ; | |
| } | |
| .traffic-dynamics-card:hover { | |
| border-color: var(--cocoa-l) ; | |
| } | |
| #dropzone { | |
| transition: all 0.2s ease; | |
| border-color: #2a2a2a; | |
| } | |
| #dropzone:hover { | |
| border-color: var(--cocoa-l) ; | |
| background-color: #0a0a0a ; | |
| } | |
| .core-badge { | |
| background-color: var(--cocoa) ; | |
| color: var(--t1) ; | |
| } | |
| /* Onboarding */ | |
| .onboard-overlay { | |
| position: fixed; inset: 0; z-index: 9999; | |
| background: rgba(0,0,0,0.92); | |
| display: flex; align-items: center; justify-content: center; | |
| } | |
| .onboard-card { | |
| background: #0a0a0a; border: 1px solid #2a2a2a; | |
| border-radius: 16px; max-width: 440px; width: 90%; | |
| padding: 40px 32px; text-align: center; | |
| } | |
| .onboard-step { display: none; } | |
| .onboard-step.active { display: block; } | |
| .onboard-dots { display: flex; gap: 6px; justify-content: center; margin-top: 20px; } | |
| .onboard-dot { | |
| width: 8px; height: 8px; border-radius: 50%; | |
| background: #333; transition: background 0.2s; | |
| } | |
| .onboard-dot.active { background: var(--cocoa-l); } | |
| /* Mobile responsive */ | |
| @media (max-width: 768px) { | |
| main { grid-template-columns: 1fr ; padding: 16px ; } | |
| h1 { font-size: 2.2rem ; } | |
| } | |
| </style> | |
| </head> | |
| <body | |
| class="bg-black text-white min-h-screen w-full flex flex-col items-center selection:bg-white selection:text-black"> | |
| <header class="mt-16 flex flex-col items-center flex-shrink-0 w-full z-10"> | |
| <img src="uf_rf.png" alt="UrbanFlow Logo" class="h-44 md:h-52 w-auto object-contain mb-3"> | |
| </header> | |
| <main | |
| class="flex-1 w-full max-w-[90rem] mx-auto grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-20 px-10 py-6 items-center z-10"> | |
| <div class="lg:col-span-7 flex flex-col justify-center xl:pl-10 pb-10 lg:pb-0"> | |
| <h1 class="text-5xl xl:text-[4.5rem] font-extrabold mb-4 leading-[1.1] tracking-tight" style="background:linear-gradient(110deg,#f0ece6 0%,#f0ece6 35%,#c89a6c 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text"> | |
| Automated <br>Vision Intelligence | |
| </h1> | |
| <p class="font-bold mb-8 text-sm uppercase tracking-[0.2em] flex items-center" style="color:#a89f97"> | |
| <span class="core-badge px-3 py-1 rounded-full text-[10px] mr-3">DEMO</span> | |
| Cloud-Native Traffic Intelligence | |
| </p> | |
| <ul class="space-y-4 xl:space-y-5 text-base xl:text-lg font-medium" style="color:#a89f97"> | |
| <li class="flex items-center"><i class="fa-solid fa-check mr-5 text-xl" style="color:#c89a6c"></i> No new hardware — works with your existing cameras</li> | |
| <li class="flex items-center"><i class="fa-solid fa-check mr-5 text-xl" style="color:#c89a6c"></i> Granular vehicle counts across 14 Indian road classes</li> | |
| <li class="flex items-center"><i class="fa-solid fa-check mr-5 text-xl" style="color:#c89a6c"></i> Directional flow & congestion insights in minutes</li> | |
| <li class="flex items-center"><i class="fa-solid fa-check mr-5 text-xl" style="color:#c89a6c"></i> Downloadable reports ready for planning & compliance</li> | |
| <li class="flex items-center"><i class="fa-solid fa-check mr-5 text-xl" style="color:#c89a6c"></i> Built for Indian roads — tested on real field conditions</li> | |
| </ul> | |
| </div> | |
| <div | |
| class="lg:col-span-5 flex flex-col justify-center w-full max-w-[32rem] mx-auto min-h-[450px] mb-12 lg:mb-0"> | |
| <!-- STEP: Modules --> | |
| <div id="step-modules" class="w-full flex flex-col fade-in"> | |
| <h2 class="text-3xl font-bold mb-2 text-center" style="color:#f0ece6">UrbanFlow</h2> | |
| <p class="text-[13px] font-medium mb-8 text-center" style="color:#a89f97">Select an analytical module to continue.</p> | |
| <div class="flex justify-center w-full"> | |
| <div onclick="showStep('upload')" | |
| class="group relative border-2 rounded-[2rem] p-8 cursor-pointer hover:-translate-y-1 transition-all duration-300 text-center max-w-sm w-full traffic-dynamics-card"> | |
| <div | |
| class="absolute top-4 right-6 text-[9px] font-bold px-2.5 py-1 rounded-full uppercase tracking-wider" | |
| style="background:#c89a6c;color:#000"> | |
| DEMO</div> | |
| <i class="fa-solid fa-car-side text-4xl mb-4 block mx-auto" style="color:#c89a6c"></i> | |
| <h3 class="font-bold text-lg mb-2 leading-tight" style="color:#f0ece6">Traffic <br>Dynamics</h3> | |
| <p class="text-[10px] font-medium leading-relaxed" style="color:#a89f97">Vehicle counting, classification, and flow analysis for Indian roads.</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- STEP: Upload --> | |
| <div id="step-upload" class="hidden w-full flex flex-col fade-in"> | |
| <button onclick="showStep('modules')" | |
| class="text-neutral-500 hover:text-white transition flex items-center text-xs font-bold uppercase tracking-widest mb-6 w-fit"> | |
| <i class="fa-solid fa-arrow-left mr-2"></i> Back | |
| </button> | |
| <h2 class="text-3xl font-bold mb-2 text-center" style="color:#f0ece6">Source Media</h2> | |
| <p class="text-[13px] font-medium mb-8 text-center" style="color:#a89f97">Submit camera footage to begin traffic analysis.</p> | |
| <input id="file-input" type="file" accept="video/*" class="hidden"> | |
| <div id="dropzone" onclick="document.getElementById('file-input').click()" | |
| class="border border-dashed border-neutral-700 rounded-[2rem] p-12 flex flex-col items-center justify-center cursor-pointer transition-all duration-300 group"> | |
| <i | |
| class="fa-solid fa-arrow-up-from-bracket text-4xl mb-5 block mx-auto transition" style="color:#a89f97"></i> | |
| <span class="font-semibold text-lg mb-2 text-center block" style="color:#f0ece6">Drop or select a video file</span> | |
| <span class="text-[10px] font-bold uppercase tracking-widest text-center block" style="color:#a89f97">Any standard video format accepted</span> | |
| </div> | |
| <div id="upload-progress-container" class="hidden mt-10 w-full"> | |
| <div class="flex justify-between text-[11px] font-bold uppercase tracking-widest mb-3" style="color:#f0ece6"> | |
| <span id="upload-text">Uploading...</span> | |
| <span id="upload-percentage">0%</span> | |
| </div> | |
| <div class="w-full h-1 bg-neutral-900 rounded-full overflow-hidden"> | |
| <div id="upload-bar" | |
| class="h-full w-0 transition-all duration-75 ease-linear rounded-full" style="background:#c89a6c"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- STEP: Draw --> | |
| <div id="step-draw" class="hidden w-full flex flex-col fade-in"> | |
| <h2 class="text-3xl font-bold mb-2 text-center" style="color:#f0ece6">Spatial Boundary</h2> | |
| <p class="text-[11px] font-bold uppercase tracking-widest mb-6 text-center" style="color:#a89f97">Mark two points to define the vehicle counting threshold</p> | |
| <div | |
| class="relative w-full aspect-video bg-neutral-950 rounded-3xl overflow-hidden cursor-crosshair mb-6"> | |
| <img id="frame-img" class="absolute inset-0 w-full h-full object-contain" style="display:none;"> | |
| <div id="frame-placeholder" | |
| class="absolute inset-0 flex flex-col items-center justify-center text-neutral-800 pointer-events-none"> | |
| <i class="fa-solid fa-video text-4xl mb-3 opacity-30"></i> | |
| <span class="font-bold text-[10px] uppercase tracking-widest opacity-50">Media Frame | |
| Preview</span> | |
| </div> | |
| <canvas id="drawing-canvas" class="absolute inset-0 w-full h-full"></canvas> | |
| </div> | |
| <div class="flex flex-col items-center gap-3"> | |
| <button id="btn-proceed" onclick="startRun()" | |
| class="w-fit px-16 py-3.5 rounded-full font-bold transition-all text-center text-sm shadow-lg hover:scale-105 active:scale-95" style="background:#c89a6c;color:#000"> | |
| Continue → | |
| </button> | |
| <button onclick="resetCanvas()" | |
| class="text-[10px] font-bold uppercase tracking-widest text-slate-500 hover:text-white transition" style="background:none;border:none;">Reset Boundary</button> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <script> | |
| let videoId = null; | |
| let runConfig = {}; | |
| function showStep(name) { | |
| ['modules', 'upload', 'draw'].forEach(s => { | |
| const el = document.getElementById('step-' + s); | |
| if (el) el.classList.add('hidden'); | |
| }); | |
| const target = document.getElementById('step-' + name); | |
| if (target) target.classList.remove('hidden'); | |
| if (name === 'upload') { | |
| document.getElementById('upload-progress-container').classList.add('hidden'); | |
| document.getElementById('dropzone').classList.remove('hidden'); | |
| // Reset Progress Bar state for new uploads | |
| document.getElementById('upload-bar').style.width = '0%'; | |
| document.getElementById('upload-percentage').innerText = '0%'; | |
| document.getElementById('upload-text').innerText = 'Uploading...'; | |
| document.getElementById('upload-text').classList.remove('text-red-500'); | |
| } | |
| if (name === 'draw') loadFirstFrame(); | |
| } | |
| const dropzone = document.getElementById('dropzone'); | |
| const fileInput = document.getElementById('file-input'); | |
| if (fileInput) { | |
| fileInput.addEventListener('change', () => { | |
| if (fileInput.files.length) uploadFile(fileInput.files[0]); | |
| }); | |
| } | |
| if (dropzone) { | |
| dropzone.addEventListener('dragover', e => { e.preventDefault(); dropzone.classList.add('border-white', 'bg-neutral-950'); }); | |
| dropzone.addEventListener('dragleave', () => dropzone.classList.remove('border-white', 'bg-neutral-950')); | |
| dropzone.addEventListener('drop', e => { | |
| e.preventDefault(); | |
| dropzone.classList.remove('border-white', 'bg-neutral-950'); | |
| if (e.dataTransfer.files.length) uploadFile(e.dataTransfer.files[0]); | |
| }); | |
| } | |
| let currentXHR = null; | |
| function uploadFile(file) { | |
| // Abort previous upload if it exists to prevent jitter/multiple requests | |
| if (currentXHR) currentXHR.abort(); | |
| const dropzoneEl = document.getElementById('dropzone'); | |
| const prog = document.getElementById('upload-progress-container'); | |
| const bar = document.getElementById('upload-bar'); | |
| const pct = document.getElementById('upload-percentage'); | |
| const txt = document.getElementById('upload-text'); | |
| if (dropzoneEl) dropzoneEl.classList.add('hidden'); | |
| if (prog) prog.classList.remove('hidden'); | |
| const form = new FormData(); | |
| form.append('file', file); | |
| const xhr = new XMLHttpRequest(); | |
| currentXHR = xhr; | |
| xhr.open('POST', '/upload'); | |
| xhr.upload.onprogress = e => { | |
| if (e.lengthComputable) { | |
| const p = Math.round(e.loaded / e.total * 100); | |
| bar.style.width = p + '%'; | |
| pct.innerText = p + '%'; | |
| } | |
| }; | |
| xhr.onerror = () => { | |
| txt.innerText = 'Error: Network failure'; | |
| txt.classList.add('text-red-500'); | |
| fileInput.value = ''; | |
| }; | |
| xhr.onload = () => { | |
| if (xhr.status !== 200) { | |
| txt.innerText = 'Error: ' + xhr.status; | |
| txt.classList.add('text-red-500'); | |
| fileInput.value = ''; | |
| return; | |
| } | |
| const res = JSON.parse(xhr.responseText); | |
| videoId = res.video_id; | |
| txt.innerText = 'Extracting Metadata...'; | |
| bar.style.width = '100%'; | |
| pct.innerText = '100%'; | |
| fetch('/config/' + videoId) | |
| .then(r => r.json()) | |
| .then(cfg => { | |
| runConfig = cfg; | |
| runConfig.conf = 0.12; | |
| runConfig.iou = 0.60; | |
| txt.innerText = 'Initialization Complete'; | |
| fileInput.value = ''; | |
| setTimeout(() => showStep('draw'), 800); | |
| }) | |
| .catch(e => { | |
| txt.innerText = 'Metadata Failed'; | |
| txt.classList.add('text-red-500'); | |
| fileInput.value = ''; | |
| }); | |
| }; | |
| xhr.send(form); | |
| } | |
| // Draw Canvas Logic | |
| const canvas = document.getElementById('drawing-canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| let points = []; | |
| let imgNatW = 0, imgNatH = 0; | |
| function loadFirstFrame() { | |
| const img = document.getElementById('frame-img'); | |
| img.src = '/first-frame/' + videoId; | |
| img.onload = () => { | |
| imgNatW = img.naturalWidth; | |
| imgNatH = img.naturalHeight; | |
| img.style.display = 'block'; | |
| document.getElementById('frame-placeholder').style.display = 'none'; | |
| initCanvas(); | |
| }; | |
| } | |
| function initCanvas() { | |
| if (canvas) { | |
| canvas.width = canvas.offsetWidth; | |
| canvas.height = canvas.offsetHeight; | |
| } | |
| } | |
| window.addEventListener('resize', initCanvas); | |
| if (canvas) { | |
| canvas.addEventListener('mousedown', e => { | |
| if (points.length >= 2) return; | |
| const rect = canvas.getBoundingClientRect(); | |
| const cx = e.clientX - rect.left; | |
| const cy = e.clientY - rect.top; | |
| const rx = (cx / canvas.width) * imgNatW; | |
| const ry = (cy / canvas.height) * imgNatH; | |
| points.push({ cx, cy, rx: Math.round(rx), ry: Math.round(ry) }); | |
| drawDot(cx, cy); | |
| if (points.length === 2) drawLine(); | |
| }); | |
| } | |
| function drawDot(x, y) { | |
| ctx.beginPath(); | |
| ctx.arc(x, y, 5, 0, Math.PI * 2); | |
| ctx.fillStyle = '#c89a6c'; | |
| ctx.fill(); | |
| ctx.strokeStyle = '#f0ece6'; | |
| ctx.lineWidth = 2; | |
| ctx.stroke(); | |
| } | |
| function drawLine() { | |
| ctx.beginPath(); | |
| ctx.moveTo(points[0].cx, points[0].cy); | |
| ctx.lineTo(points[1].cx, points[1].cy); | |
| ctx.strokeStyle = '#c89a6c'; | |
| ctx.lineWidth = 3; | |
| ctx.stroke(); | |
| } | |
| function resetCanvas() { | |
| points = []; | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| } | |
| function startRun() { | |
| if (points.length < 2) return; | |
| const line = [[points[0].rx, points[0].ry], [points[1].rx, points[1].ry]]; | |
| sessionStorage.setItem('funky_run', JSON.stringify({ | |
| video_id: videoId, | |
| line: line, | |
| config: runConfig | |
| })); | |
| window.location.href = '/'; | |
| } | |
| </script> | |
| <!-- Onboarding Walkthrough --> | |
| <div id="onboard-overlay" class="onboard-overlay" style="display:none"> | |
| <div class="onboard-card"> | |
| <div class="onboard-step active" data-step="0"> | |
| <i class="fa-solid fa-cloud-arrow-up text-4xl mb-4" style="color:var(--cocoa-l)"></i> | |
| <h3 class="text-lg font-bold mb-2" style="color:#f0ece6">Upload a Traffic Video</h3> | |
| <p class="text-xs" style="color:#777;line-height:1.7">Drag & drop or select a video file recorded from any traffic camera. MP4, MOV, AVI formats supported.</p> | |
| </div> | |
| <div class="onboard-step" data-step="1"> | |
| <i class="fa-solid fa-draw-polygon text-4xl mb-4" style="color:var(--cocoa-l)"></i> | |
| <h3 class="text-lg font-bold mb-2" style="color:#f0ece6">Draw a Counting Boundary</h3> | |
| <p class="text-xs" style="color:#777;line-height:1.7">Click two points on the first frame to define a spatial boundary. Vehicles crossing this line will be counted and classified.</p> | |
| </div> | |
| <div class="onboard-step" data-step="2"> | |
| <i class="fa-solid fa-chart-line text-4xl mb-4" style="color:var(--cocoa-l)"></i> | |
| <h3 class="text-lg font-bold mb-2" style="color:#f0ece6">Review Analytics & Export</h3> | |
| <p class="text-xs" style="color:#777;line-height:1.7">Watch real-time charts populate as inference runs. Download annotated video, reports, and structured JSON when complete.</p> | |
| </div> | |
| <div class="onboard-dots"> | |
| <span class="onboard-dot active"></span> | |
| <span class="onboard-dot"></span> | |
| <span class="onboard-dot"></span> | |
| </div> | |
| <div class="flex gap-3 justify-center mt-6"> | |
| <button onclick="closeOnboarding()" class="text-[10px] font-bold uppercase tracking-widest px-4 py-2 rounded-full" style="color:#555;border:1px solid #222">Skip</button> | |
| <button id="onboard-next" onclick="nextOnboardStep()" class="text-[10px] font-bold uppercase tracking-widest px-6 py-2 rounded-full" style="background:var(--cocoa);color:#f0ece6">Next</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let _obStep = 0; | |
| function nextOnboardStep() { | |
| _obStep++; | |
| if (_obStep >= 3) { closeOnboarding(); return; } | |
| document.querySelectorAll('.onboard-step').forEach((s, i) => s.classList.toggle('active', i === _obStep)); | |
| document.querySelectorAll('.onboard-dot').forEach((d, i) => d.classList.toggle('active', i === _obStep)); | |
| if (_obStep === 2) document.getElementById('onboard-next').innerText = 'Get Started'; | |
| } | |
| function closeOnboarding() { | |
| document.getElementById('onboard-overlay').style.display = 'none'; | |
| } | |
| // Show onboarding on every page load | |
| document.getElementById('onboard-overlay').style.display = 'flex'; | |
| </script> | |
| </body> | |
| </html> |