Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>Mission Console</title> | |
| <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css"> | |
| <style> | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| font-family: "Inter", Arial, sans-serif; | |
| background-color: #121212; | |
| color: #e0e0e0; | |
| } | |
| h1 { | |
| text-align: center; | |
| padding: 20px; | |
| font-size: 26px; | |
| color: #f5f5f5; | |
| margin-bottom: 10px; | |
| } | |
| .container { | |
| width: 90%; | |
| max-width: 1100px; | |
| margin: auto; | |
| background: #1e1e1e; | |
| padding: 25px; | |
| border-radius: 12px; | |
| box-shadow: 0 15px 40px rgba(0, 0, 0, 0.25); | |
| } | |
| .form-grid { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 20px; | |
| margin-top: 10px; | |
| } | |
| .form-control { | |
| flex: 1; | |
| min-width: 260px; | |
| } | |
| .label { | |
| font-size: 15px; | |
| font-weight: 600; | |
| display: block; | |
| color: #bdbdbd; | |
| } | |
| input[type="text"] { | |
| padding: 12px; | |
| border: 1px solid #2c2c2c; | |
| background-color: #161616; | |
| color: #fff; | |
| font-size: 15px; | |
| border-radius: 8px; | |
| width: 100%; | |
| margin-top: 5px; | |
| } | |
| input[type="file"], | |
| select { | |
| margin-top: 5px; | |
| padding: 10px; | |
| background-color: #161616; | |
| border: 1px solid #2c2c2c; | |
| border-radius: 8px; | |
| color: #f5f5f5; | |
| width: 100%; | |
| } | |
| button { | |
| margin-top: 25px; | |
| width: 100%; | |
| padding: 14px; | |
| background-color: #333333; | |
| border: none; | |
| color: #f5f5f5; | |
| font-size: 16px; | |
| font-weight: 600; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: 0.2s; | |
| } | |
| button:hover { | |
| background-color: #4f4f4f; | |
| } | |
| button:disabled { | |
| background-color: #1f1f1f; | |
| color: #6e6e6e; | |
| cursor: not-allowed; | |
| } | |
| .secondary-button { | |
| width: auto; | |
| padding: 10px 18px; | |
| margin-top: 10px; | |
| background-color: #262626; | |
| } | |
| .secondary-button:hover { | |
| background-color: #3a3a3a; | |
| } | |
| .secondary-button.small { | |
| padding: 8px 14px; | |
| font-size: 14px; | |
| } | |
| .prompt-actions { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 10px; | |
| margin-top: 8px; | |
| } | |
| .location-section { | |
| margin-top: 20px; | |
| padding: 16px; | |
| border: 1px solid #2c2c2c; | |
| border-radius: 10px; | |
| background: #141414; | |
| } | |
| .location-hint { | |
| font-size: 13px; | |
| color: #bdbdbd; | |
| margin-top: 6px; | |
| } | |
| .location-map-container { | |
| margin-top: 12px; | |
| height: 260px; | |
| border: 1px solid #2c2c2c; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| } | |
| #missionMap { | |
| height: 100%; | |
| width: 100%; | |
| } | |
| .location-readout { | |
| display: flex; | |
| gap: 20px; | |
| margin-top: 12px; | |
| font-size: 14px; | |
| } | |
| .output-section { | |
| margin-top: 35px; | |
| padding: 20px; | |
| border-radius: 12px; | |
| background-color: #171717; | |
| border: 1px solid #2a2a2a; | |
| } | |
| #summary { | |
| margin-top: 10px; | |
| padding: 14px; | |
| border: 1px solid #2a2a2a; | |
| background-color: #111; | |
| color: #d1d1d1; | |
| font-size: 15px; | |
| min-height: 70px; | |
| white-space: pre-wrap; | |
| border-radius: 8px; | |
| } | |
| .video-section { | |
| margin-top: 20px; | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 25px; | |
| } | |
| .video-panel { | |
| flex: 1; | |
| min-width: 320px; | |
| } | |
| .video-panel h2 { | |
| color: #00ffea; | |
| margin-bottom: 10px; | |
| font-size: 18px; | |
| } | |
| .mission-video { | |
| width: 100%; | |
| max-height: 420px; | |
| object-fit: contain; | |
| border: 1px solid #2a2a2a; | |
| border-radius: 8px; | |
| background-color: #000; | |
| } | |
| .status-message { | |
| margin-top: 15px; | |
| color: #ffa95c; | |
| font-size: 13px; | |
| min-height: 18px; | |
| } | |
| .status-message.small { | |
| font-size: 12px; | |
| margin-top: 10px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Mission Console</h1> | |
| <div class="container"> | |
| <div class="form-grid"> | |
| <div class="form-control"> | |
| <label class="label">MISSION PROMPT</label> | |
| <input id="missionPrompt" type="text" placeholder="e.g., Track hostile drone movement..."> | |
| <div class="prompt-actions"> | |
| <button type="button" id="setMissionButton" class="secondary-button" onclick="stageMissionPrompt()">Set Mission Prompt</button> | |
| </div> | |
| </div> | |
| <div class="form-control"> | |
| <label class="label">UPLOAD VIDEO (.mp4)</label> | |
| <input id="videoInput" type="file" accept="video/mp4" disabled> | |
| </div> | |
| </div> | |
| <div class="location-section"> | |
| <label class="label">MISSION LOCATION</label> | |
| <div class="location-hint">We will auto-detect your GPS (if allowed). Otherwise, click anywhere on the map to select the mission location.</div> | |
| <div id="missionMapContainer" class="location-map-container"> | |
| <div id="missionMap"></div> | |
| </div> | |
| <div class="location-readout"> | |
| <div>Latitude: <span id="latitudeDisplay">--</span></div> | |
| <div>Longitude: <span id="longitudeDisplay">--</span></div> | |
| </div> | |
| <div id="locationStatus" class="status-message small"></div> | |
| <div id="locationIntel" class="status-message small" style="margin-top:6px;"></div> | |
| </div> | |
| <label class="label">OBJECT DETECTOR</label> | |
| <select id="detectorSelect"> | |
| <option value="owlv2" selected>OWL-V2</option> | |
| <option value="hf_yolov8">YOLOv8</option> | |
| <option value="hf_yolov8_defence">YOLOv8m Defence</option> | |
| <option value="hf_yolov12_bot_sort">YOLOv12 BoT-SORT + ReID</option> | |
| </select> | |
| <button type="button" id="executeButton" onclick="executeMission()" disabled>EXECUTE MISSION</button> | |
| <div class="output-section"> | |
| <div class="video-section"> | |
| <div class="video-panel"> | |
| <h2>ORIGINAL VIDEO</h2> | |
| <video id="originalVideo" class="mission-video" controls></video> | |
| </div> | |
| <div class="video-panel"> | |
| <h2>PROCESSED VIDEO FEED</h2> | |
| <video id="processedVideo" class="mission-video" controls></video> | |
| </div> | |
| </div> | |
| <h2 style="color:#00ffea; margin:25px 0 10px;">MISSION SUMMARY</h2> | |
| <div id="summary">(Awaiting mission results...)</div> | |
| <div id="status" class="status-message"></div> | |
| </div> | |
| </div> | |
| <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script> | |
| <script> | |
| const API_BASE_URL = "https://biaslab2025-demo-2025.hf.space"; | |
| const PROCESS_VIDEO_URL = `${API_BASE_URL}/process_video`; | |
| const SUMMARY_URL = `${API_BASE_URL}/mission_summary`; | |
| const MISSION_PLAN_URL = `${API_BASE_URL}/mission_plan`; | |
| const LOCATION_INTEL_URL = `${API_BASE_URL}/location_context`; | |
| const missionInputEl = document.getElementById("missionPrompt"); | |
| const detectorSelectEl = document.getElementById("detectorSelect"); | |
| const originalVideoEl = document.getElementById("originalVideo"); | |
| const videoInputEl = document.getElementById("videoInput"); | |
| const summaryEl = document.getElementById("summary"); | |
| const statusEl = document.getElementById("status"); | |
| const setMissionButton = document.getElementById("setMissionButton"); | |
| const executeButton = document.getElementById("executeButton"); | |
| const latitudeDisplayEl = document.getElementById("latitudeDisplay"); | |
| const longitudeDisplayEl = document.getElementById("longitudeDisplay"); | |
| const locationStatusEl = document.getElementById("locationStatus"); | |
| const locationIntelEl = document.getElementById("locationIntel"); | |
| let originalVideoUrl = null; | |
| let currentMissionId = null; | |
| let missionReady = false; | |
| let missionRequestPending = false; | |
| let videoProcessing = false; | |
| let locationReady = false; | |
| let selectedLat = null; | |
| let selectedLon = null; | |
| let missionMap = null; | |
| let missionMapMarker = null; | |
| missionInputEl.addEventListener("input", handleMissionPromptEdit); | |
| videoInputEl.addEventListener("change", () => { | |
| const uploaded = videoInputEl.files[0]; | |
| if (uploaded) { | |
| setOriginalPreview(uploaded); | |
| } else if (originalVideoUrl) { | |
| URL.revokeObjectURL(originalVideoUrl); | |
| originalVideoEl.removeAttribute("src"); | |
| originalVideoEl.load(); | |
| originalVideoUrl = null; | |
| } | |
| }); | |
| initializeMissionMap(); | |
| attemptAutoGps(); | |
| function initializeMissionMap() { | |
| if (missionMap) { | |
| return; | |
| } | |
| missionMap = L.map("missionMap").setView([20, 0], 2); | |
| L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { | |
| maxZoom: 19, | |
| attribution: "© OpenStreetMap contributors", | |
| }).addTo(missionMap); | |
| missionMap.on("click", (event) => { | |
| setSelectedLocation(event.latlng.lat, event.latlng.lng, "map"); | |
| }); | |
| } | |
| function attemptAutoGps() { | |
| if (!navigator.geolocation) { | |
| setLocationStatus("GPS unavailable. Click on the map to choose a location.", "error"); | |
| return; | |
| } | |
| setLocationStatus("Detecting GPS location..."); | |
| navigator.geolocation.getCurrentPosition( | |
| (position) => { | |
| setSelectedLocation(position.coords.latitude, position.coords.longitude, "gps"); | |
| }, | |
| (error) => { | |
| setLocationStatus(`GPS failed (${error.message}). Click on the map to select a location.`, "error"); | |
| }, | |
| { enableHighAccuracy: true, timeout: 12000, maximumAge: 0 } | |
| ); | |
| } | |
| function setSelectedLocation(lat, lon, source) { | |
| selectedLat = lat; | |
| selectedLon = lon; | |
| locationReady = true; | |
| placeMapMarker(lat, lon); | |
| updateLocationDisplay(); | |
| setLocationStatus( | |
| `Location set via ${source.toUpperCase()}: ${lat.toFixed(4)}, ${lon.toFixed(4)}`, | |
| "success" | |
| ); | |
| if (currentMissionId) { | |
| missionReady = false; | |
| currentMissionId = null; | |
| setStatus("Mission location changed. Set mission prompt again."); | |
| } | |
| updateControlState(); | |
| sendLocationIntel(lat, lon); | |
| } | |
| function updateLocationDisplay() { | |
| latitudeDisplayEl.textContent = selectedLat !== null ? selectedLat.toFixed(4) : "--"; | |
| longitudeDisplayEl.textContent = selectedLon !== null ? selectedLon.toFixed(4) : "--"; | |
| } | |
| function placeMapMarker(lat, lon) { | |
| if (!missionMap) { | |
| return; | |
| } | |
| const point = [lat, lon]; | |
| if (!missionMapMarker) { | |
| missionMapMarker = L.circleMarker(point, { | |
| radius: 8, | |
| color: "#ff4d4d", | |
| fillColor: "#ff4d4d", | |
| fillOpacity: 0.95, | |
| weight: 2, | |
| }).addTo(missionMap); | |
| } else { | |
| missionMapMarker.setLatLng(point); | |
| } | |
| const targetZoom = missionMap.getZoom() < 8 ? 8 : missionMap.getZoom(); | |
| missionMap.setView(point, targetZoom); | |
| } | |
| function handleMissionPromptEdit() { | |
| if (!missionReady && !currentMissionId) { | |
| return; | |
| } | |
| missionReady = false; | |
| currentMissionId = null; | |
| setStatus("Mission prompt changed. Set it again to process videos."); | |
| updateControlState(); | |
| } | |
| function updateControlState() { | |
| const disableVideoActions = !missionReady || missionRequestPending || videoProcessing; | |
| videoInputEl.disabled = disableVideoActions; | |
| executeButton.disabled = disableVideoActions; | |
| setMissionButton.disabled = missionRequestPending || !locationReady; | |
| } | |
| function setStatus(message, tone = "info") { | |
| statusEl.textContent = message || ""; | |
| if (tone === "error") { | |
| statusEl.style.color = "#ff7b7b"; | |
| } else if (tone === "success") { | |
| statusEl.style.color = "#7bffb3"; | |
| } else { | |
| statusEl.style.color = "#ffa95c"; | |
| } | |
| } | |
| function setLocationStatus(message, tone = "info") { | |
| locationStatusEl.textContent = message || ""; | |
| if (tone === "error") { | |
| locationStatusEl.style.color = "#ff7b7b"; | |
| } else if (tone === "success") { | |
| locationStatusEl.style.color = "#7bffb3"; | |
| } else { | |
| locationStatusEl.style.color = "#bdbdbd"; | |
| } | |
| } | |
| let locationIntelAbortController = null; | |
| async function sendLocationIntel(lat, lon) { | |
| if (locationIntelAbortController) { | |
| locationIntelAbortController.abort(); | |
| } | |
| locationIntelAbortController = new AbortController(); | |
| locationIntelEl.textContent = "Analyzing location-based threats..."; | |
| locationIntelEl.style.color = "#ffa95c"; | |
| try { | |
| const form = new FormData(); | |
| form.append("latitude", lat.toString()); | |
| form.append("longitude", lon.toString()); | |
| const response = await fetch(LOCATION_INTEL_URL, { | |
| method: "POST", | |
| body: form, | |
| signal: locationIntelAbortController.signal, | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`Location intelligence failed (${response.status})`); | |
| } | |
| const payload = await response.json(); | |
| const classes = (payload.mission_plan?.classes || []).slice(0, 4); | |
| if (!classes.length) { | |
| locationIntelEl.textContent = "Location intel: no threat classes suggested."; | |
| locationIntelEl.style.color = "#bdbdbd"; | |
| return; | |
| } | |
| const classSummary = classes | |
| .map((entry) => entry.name) | |
| .join(", "); | |
| locationIntelEl.textContent = `Likely threats near this location: ${classSummary}`; | |
| locationIntelEl.style.color = "#7bffb3"; | |
| } catch (error) { | |
| if (error.name === "AbortError") { | |
| return; | |
| } | |
| console.error("[location_intel] error", error); | |
| locationIntelEl.textContent = error.message || "Failed to analyze location."; | |
| locationIntelEl.style.color = "#ff7b7b"; | |
| } | |
| } | |
| async function stageMissionPrompt() { | |
| const mission = missionInputEl.value.trim(); | |
| if (!mission) { | |
| alert("Enter a mission prompt before setting it."); | |
| return; | |
| } | |
| if (!locationReady || selectedLat === null || selectedLon === null) { | |
| alert("Set mission location via GPS or the map before proceeding."); | |
| return; | |
| } | |
| missionRequestPending = true; | |
| missionReady = false; | |
| currentMissionId = null; | |
| updateControlState(); | |
| setStatus("Preparing mission prompt..."); | |
| try { | |
| const form = new FormData(); | |
| form.append("prompt", mission); | |
| form.append("detector", detectorSelectEl.value); | |
| form.append("latitude", selectedLat.toString()); | |
| form.append("longitude", selectedLon.toString()); | |
| const response = await fetch(MISSION_PLAN_URL, { | |
| method: "POST", | |
| body: form, | |
| }); | |
| if (!response.ok) { | |
| let errorDetail = `Failed to set mission prompt (${response.status})`; | |
| try { | |
| const errJson = await response.json(); | |
| errorDetail = errJson.detail || errJson.error || errorDetail; | |
| } catch (_) {} | |
| throw new Error(errorDetail); | |
| } | |
| const payload = await response.json(); | |
| currentMissionId = payload.mission_id; | |
| missionReady = true; | |
| setStatus("Mission prompt ready. Upload videos to process.", "success"); | |
| } catch (error) { | |
| console.error("[mission_plan] error", error); | |
| setStatus(error.message || "Failed to set mission prompt.", "error"); | |
| } finally { | |
| missionRequestPending = false; | |
| updateControlState(); | |
| } | |
| } | |
| async function executeMission() { | |
| const videoFile = videoInputEl.files[0]; | |
| const mission = missionInputEl.value.trim(); | |
| const detector = detectorSelectEl.value; | |
| if (!currentMissionId || !missionReady) { | |
| alert("Set the mission prompt before processing videos."); | |
| return; | |
| } | |
| if (!videoFile) { | |
| alert("Mission invalid: Upload a video file."); | |
| return; | |
| } | |
| setOriginalPreview(videoFile); | |
| summaryEl.textContent = "(Awaiting summary...)"; | |
| videoProcessing = true; | |
| updateControlState(); | |
| setStatus("Processing video..."); | |
| try { | |
| const videoForm = new FormData(); | |
| videoForm.append("video", videoFile); | |
| videoForm.append("prompt", mission); | |
| videoForm.append("mission_id", currentMissionId); | |
| videoForm.append("latitude", selectedLat?.toString() || ""); | |
| videoForm.append("longitude", selectedLon?.toString() || ""); | |
| videoForm.append("detector", detector); | |
| console.log("[process_video] submitting request", { detector, missionLength: mission.length, fileSize: videoFile.size }); | |
| const response = await fetch(PROCESS_VIDEO_URL, { | |
| method: "POST", | |
| body: videoForm | |
| }); | |
| console.log( | |
| "[process_video] response meta", | |
| response.status, | |
| response.statusText, | |
| { contentLength: response.headers.get("content-length"), contentType: response.headers.get("content-type") } | |
| ); | |
| if (!response.ok) { | |
| let errorDetail = `Request failed (${response.status})`; | |
| try { | |
| const errJson = await response.json(); | |
| errorDetail = errJson.error || errorDetail; | |
| } catch (_) { | |
| // ignore | |
| } | |
| throw new Error(errorDetail); | |
| } | |
| const videoBlob = await response.blob(); | |
| console.log("[process_video] blob size", videoBlob.size, videoBlob.type); | |
| const videoUrl = URL.createObjectURL(videoBlob); | |
| const videoEl = document.getElementById("processedVideo"); | |
| videoEl.addEventListener("loadeddata", () => { | |
| console.log("[process_video] video loadeddata event", videoEl.readyState); | |
| }, { once: true }); | |
| videoEl.addEventListener("error", (event) => { | |
| console.error("[process_video] video error", videoEl.error, event); | |
| }, { once: true }); | |
| videoEl.src = videoUrl; | |
| videoEl.load(); | |
| console.log("[process_video] video element readyState after load()", videoEl.readyState); | |
| if (videoEl.error) { | |
| console.error("[process_video] immediate video error", videoEl.error); | |
| } | |
| setStatus("Generating summary..."); | |
| const summaryForm = new FormData(); | |
| summaryForm.append("video", videoFile); | |
| summaryForm.append("prompt", mission); | |
| summaryForm.append("mission_id", currentMissionId); | |
| summaryForm.append("latitude", selectedLat?.toString() || ""); | |
| summaryForm.append("longitude", selectedLon?.toString() || ""); | |
| summaryForm.append("detector", detector); | |
| console.log("[mission_summary] submitting request", { detector, missionLength: mission.length, fileSize: videoFile.size }); | |
| const summaryResponse = await fetch(SUMMARY_URL, { | |
| method: "POST", | |
| body: summaryForm | |
| }); | |
| if (!summaryResponse.ok) { | |
| let errorDetail = `Summary failed (${summaryResponse.status})`; | |
| try { | |
| const errJson = await summaryResponse.json(); | |
| errorDetail = errJson.error || errorDetail; | |
| } catch (_) {} | |
| throw new Error(errorDetail); | |
| } | |
| console.log( | |
| "[mission_summary] response meta", | |
| summaryResponse.status, | |
| summaryResponse.statusText, | |
| { contentLength: summaryResponse.headers.get("content-length"), contentType: summaryResponse.headers.get("content-type") } | |
| ); | |
| const summaryJson = await summaryResponse.json(); | |
| const summaryText = summaryJson.mission_summary || "No summary returned."; | |
| summaryEl.textContent = summaryText; | |
| setStatus("Mission complete.", "success"); | |
| } catch (err) { | |
| console.error(err); | |
| summaryEl.textContent = "Mission failed."; | |
| setStatus(`Error: ${err.message}`, "error"); | |
| } finally { | |
| videoProcessing = false; | |
| updateControlState(); | |
| } | |
| } | |
| function setOriginalPreview(file) { | |
| if (originalVideoUrl) { | |
| URL.revokeObjectURL(originalVideoUrl); | |
| } | |
| originalVideoUrl = URL.createObjectURL(file); | |
| originalVideoEl.src = originalVideoUrl; | |
| originalVideoEl.load(); | |
| } | |
| setLocationStatus("Awaiting GPS permission or map selection..."); | |
| updateControlState(); | |
| </script> | |
| </body> | |
| </html> | |