Spaces:
Sleeping
Sleeping
update: added gps and location intelligence
Browse files
app.py
CHANGED
|
@@ -58,6 +58,8 @@ def _schedule_cleanup(background_tasks: BackgroundTasks, path: str) -> None:
|
|
| 58 |
class CachedMission:
|
| 59 |
prompt: str
|
| 60 |
detector: Optional[str]
|
|
|
|
|
|
|
| 61 |
plan: MissionPlan
|
| 62 |
created_at: float
|
| 63 |
|
|
@@ -77,12 +79,20 @@ def _prune_mission_cache() -> None:
|
|
| 77 |
MISSION_CACHE.pop(key, None)
|
| 78 |
|
| 79 |
|
| 80 |
-
def _store_mission_plan(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
_prune_mission_cache()
|
| 82 |
mission_id = uuid4().hex
|
| 83 |
MISSION_CACHE[mission_id] = CachedMission(
|
| 84 |
prompt=prompt,
|
| 85 |
detector=detector,
|
|
|
|
|
|
|
| 86 |
plan=plan,
|
| 87 |
created_at=time.time(),
|
| 88 |
)
|
|
@@ -97,28 +107,80 @@ def _get_cached_mission(mission_id: str) -> CachedMission:
|
|
| 97 |
return entry
|
| 98 |
|
| 99 |
|
| 100 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
if mission_id:
|
| 102 |
cached = _get_cached_mission(mission_id)
|
| 103 |
return cached.plan, cached.prompt
|
| 104 |
normalized_prompt = (prompt or "").strip()
|
| 105 |
if not normalized_prompt:
|
| 106 |
raise HTTPException(status_code=400, detail="Mission prompt is required.")
|
|
|
|
| 107 |
plan = get_mission_plan(normalized_prompt)
|
| 108 |
return plan, normalized_prompt
|
| 109 |
|
| 110 |
|
| 111 |
-
def _validate_inputs(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
if video is None:
|
| 113 |
raise HTTPException(status_code=400, detail="Video file is required.")
|
| 114 |
-
if
|
|
|
|
|
|
|
| 115 |
raise HTTPException(status_code=400, detail="Mission prompt is required.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
|
| 118 |
@app.post("/mission_plan")
|
| 119 |
async def mission_plan_endpoint(
|
| 120 |
prompt: str = Form(...),
|
| 121 |
detector: Optional[str] = Form(None),
|
|
|
|
|
|
|
| 122 |
):
|
| 123 |
normalized_prompt = (prompt or "").strip()
|
| 124 |
if not normalized_prompt:
|
|
@@ -129,8 +191,12 @@ async def mission_plan_endpoint(
|
|
| 129 |
logging.exception("Mission planning failed.")
|
| 130 |
raise HTTPException(status_code=500, detail=str(exc))
|
| 131 |
|
| 132 |
-
mission_id = _store_mission_plan(normalized_prompt, detector, plan)
|
| 133 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
|
| 136 |
@app.post("/process_video")
|
|
@@ -140,8 +206,10 @@ async def process_video(
|
|
| 140 |
prompt: Optional[str] = Form(None),
|
| 141 |
mission_id: Optional[str] = Form(None),
|
| 142 |
detector: Optional[str] = Form(None),
|
|
|
|
|
|
|
| 143 |
):
|
| 144 |
-
_validate_inputs(video, prompt, mission_id)
|
| 145 |
|
| 146 |
try:
|
| 147 |
input_path = _save_upload_to_tmp(video)
|
|
@@ -154,7 +222,7 @@ async def process_video(
|
|
| 154 |
fd, output_path = tempfile.mkstemp(prefix="output_", suffix=".mp4", dir="/tmp")
|
| 155 |
os.close(fd)
|
| 156 |
|
| 157 |
-
mission_plan, mission_prompt = _resolve_mission_plan(prompt, mission_id)
|
| 158 |
try:
|
| 159 |
output_path, _, _ = run_inference(
|
| 160 |
input_path,
|
|
@@ -192,9 +260,11 @@ async def mission_summary(
|
|
| 192 |
prompt: Optional[str] = Form(None),
|
| 193 |
mission_id: Optional[str] = Form(None),
|
| 194 |
detector: Optional[str] = Form(None),
|
|
|
|
|
|
|
| 195 |
):
|
| 196 |
-
_validate_inputs(video, prompt, mission_id)
|
| 197 |
-
mission_plan, mission_prompt = _resolve_mission_plan(prompt, mission_id)
|
| 198 |
try:
|
| 199 |
input_path = _save_upload_to_tmp(video)
|
| 200 |
except Exception:
|
|
|
|
| 58 |
class CachedMission:
|
| 59 |
prompt: str
|
| 60 |
detector: Optional[str]
|
| 61 |
+
latitude: float
|
| 62 |
+
longitude: float
|
| 63 |
plan: MissionPlan
|
| 64 |
created_at: float
|
| 65 |
|
|
|
|
| 79 |
MISSION_CACHE.pop(key, None)
|
| 80 |
|
| 81 |
|
| 82 |
+
def _store_mission_plan(
|
| 83 |
+
prompt: str,
|
| 84 |
+
detector: Optional[str],
|
| 85 |
+
plan: MissionPlan,
|
| 86 |
+
latitude: float,
|
| 87 |
+
longitude: float,
|
| 88 |
+
) -> str:
|
| 89 |
_prune_mission_cache()
|
| 90 |
mission_id = uuid4().hex
|
| 91 |
MISSION_CACHE[mission_id] = CachedMission(
|
| 92 |
prompt=prompt,
|
| 93 |
detector=detector,
|
| 94 |
+
latitude=latitude,
|
| 95 |
+
longitude=longitude,
|
| 96 |
plan=plan,
|
| 97 |
created_at=time.time(),
|
| 98 |
)
|
|
|
|
| 107 |
return entry
|
| 108 |
|
| 109 |
|
| 110 |
+
def _require_coordinates(
|
| 111 |
+
latitude: Optional[float],
|
| 112 |
+
longitude: Optional[float],
|
| 113 |
+
) -> tuple[float, float]:
|
| 114 |
+
if latitude is None or longitude is None:
|
| 115 |
+
raise HTTPException(status_code=400, detail="Mission location coordinates are required.")
|
| 116 |
+
return float(latitude), float(longitude)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def _resolve_mission_plan(
|
| 120 |
+
prompt: Optional[str],
|
| 121 |
+
mission_id: Optional[str],
|
| 122 |
+
latitude: Optional[float],
|
| 123 |
+
longitude: Optional[float],
|
| 124 |
+
) -> tuple[MissionPlan, str]:
|
| 125 |
if mission_id:
|
| 126 |
cached = _get_cached_mission(mission_id)
|
| 127 |
return cached.plan, cached.prompt
|
| 128 |
normalized_prompt = (prompt or "").strip()
|
| 129 |
if not normalized_prompt:
|
| 130 |
raise HTTPException(status_code=400, detail="Mission prompt is required.")
|
| 131 |
+
_require_coordinates(latitude, longitude)
|
| 132 |
plan = get_mission_plan(normalized_prompt)
|
| 133 |
return plan, normalized_prompt
|
| 134 |
|
| 135 |
|
| 136 |
+
def _validate_inputs(
|
| 137 |
+
video: UploadFile | None,
|
| 138 |
+
prompt: str | None,
|
| 139 |
+
mission_id: Optional[str],
|
| 140 |
+
latitude: Optional[float],
|
| 141 |
+
longitude: Optional[float],
|
| 142 |
+
) -> None:
|
| 143 |
if video is None:
|
| 144 |
raise HTTPException(status_code=400, detail="Video file is required.")
|
| 145 |
+
if mission_id:
|
| 146 |
+
return
|
| 147 |
+
if not prompt:
|
| 148 |
raise HTTPException(status_code=400, detail="Mission prompt is required.")
|
| 149 |
+
_require_coordinates(latitude, longitude)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def _location_only_prompt(latitude: float, longitude: float) -> str:
|
| 153 |
+
return (
|
| 154 |
+
"Threat reconnaissance mission. "
|
| 155 |
+
f"Identify and prioritize potential hostile or suspicious object classes around latitude {latitude:.4f}, "
|
| 156 |
+
f"longitude {longitude:.4f}. Consider common threats for this environment when selecting classes."
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
@app.post("/location_context")
|
| 161 |
+
async def location_context(
|
| 162 |
+
latitude: float = Form(...),
|
| 163 |
+
longitude: float = Form(...),
|
| 164 |
+
):
|
| 165 |
+
prompt = _location_only_prompt(latitude, longitude)
|
| 166 |
+
try:
|
| 167 |
+
plan = get_mission_plan(prompt)
|
| 168 |
+
except Exception as exc:
|
| 169 |
+
logging.exception("Location-only planning failed.")
|
| 170 |
+
raise HTTPException(status_code=500, detail=str(exc))
|
| 171 |
+
return {
|
| 172 |
+
"mission_plan": plan.to_dict(),
|
| 173 |
+
"location": {"latitude": latitude, "longitude": longitude},
|
| 174 |
+
"prompt_used": prompt,
|
| 175 |
+
}
|
| 176 |
|
| 177 |
|
| 178 |
@app.post("/mission_plan")
|
| 179 |
async def mission_plan_endpoint(
|
| 180 |
prompt: str = Form(...),
|
| 181 |
detector: Optional[str] = Form(None),
|
| 182 |
+
latitude: float = Form(...),
|
| 183 |
+
longitude: float = Form(...),
|
| 184 |
):
|
| 185 |
normalized_prompt = (prompt or "").strip()
|
| 186 |
if not normalized_prompt:
|
|
|
|
| 191 |
logging.exception("Mission planning failed.")
|
| 192 |
raise HTTPException(status_code=500, detail=str(exc))
|
| 193 |
|
| 194 |
+
mission_id = _store_mission_plan(normalized_prompt, detector, plan, latitude, longitude)
|
| 195 |
+
return {
|
| 196 |
+
"mission_id": mission_id,
|
| 197 |
+
"mission_plan": plan.to_dict(),
|
| 198 |
+
"location": {"latitude": latitude, "longitude": longitude},
|
| 199 |
+
}
|
| 200 |
|
| 201 |
|
| 202 |
@app.post("/process_video")
|
|
|
|
| 206 |
prompt: Optional[str] = Form(None),
|
| 207 |
mission_id: Optional[str] = Form(None),
|
| 208 |
detector: Optional[str] = Form(None),
|
| 209 |
+
latitude: Optional[float] = Form(None),
|
| 210 |
+
longitude: Optional[float] = Form(None),
|
| 211 |
):
|
| 212 |
+
_validate_inputs(video, prompt, mission_id, latitude, longitude)
|
| 213 |
|
| 214 |
try:
|
| 215 |
input_path = _save_upload_to_tmp(video)
|
|
|
|
| 222 |
fd, output_path = tempfile.mkstemp(prefix="output_", suffix=".mp4", dir="/tmp")
|
| 223 |
os.close(fd)
|
| 224 |
|
| 225 |
+
mission_plan, mission_prompt = _resolve_mission_plan(prompt, mission_id, latitude, longitude)
|
| 226 |
try:
|
| 227 |
output_path, _, _ = run_inference(
|
| 228 |
input_path,
|
|
|
|
| 260 |
prompt: Optional[str] = Form(None),
|
| 261 |
mission_id: Optional[str] = Form(None),
|
| 262 |
detector: Optional[str] = Form(None),
|
| 263 |
+
latitude: Optional[float] = Form(None),
|
| 264 |
+
longitude: Optional[float] = Form(None),
|
| 265 |
):
|
| 266 |
+
_validate_inputs(video, prompt, mission_id, latitude, longitude)
|
| 267 |
+
mission_plan, mission_prompt = _resolve_mission_plan(prompt, mission_id, latitude, longitude)
|
| 268 |
try:
|
| 269 |
input_path = _save_upload_to_tmp(video)
|
| 270 |
except Exception:
|
demo.html
CHANGED
|
@@ -4,6 +4,7 @@
|
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<title>Mission Console</title>
|
| 6 |
|
|
|
|
| 7 |
<style>
|
| 8 |
body {
|
| 9 |
margin: 0;
|
|
@@ -107,6 +108,11 @@ button:disabled {
|
|
| 107 |
background-color: #3a3a3a;
|
| 108 |
}
|
| 109 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
.prompt-actions {
|
| 111 |
display: flex;
|
| 112 |
flex-wrap: wrap;
|
|
@@ -114,6 +120,40 @@ button:disabled {
|
|
| 114 |
margin-top: 8px;
|
| 115 |
}
|
| 116 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
.output-section {
|
| 118 |
margin-top: 35px;
|
| 119 |
padding: 20px;
|
|
@@ -167,6 +207,11 @@ button:disabled {
|
|
| 167 |
font-size: 13px;
|
| 168 |
min-height: 18px;
|
| 169 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
</style>
|
| 171 |
|
| 172 |
</head>
|
|
@@ -190,6 +235,20 @@ button:disabled {
|
|
| 190 |
</div>
|
| 191 |
</div>
|
| 192 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
<label class="label">OBJECT DETECTOR</label>
|
| 194 |
<select id="detectorSelect">
|
| 195 |
<option value="owlv2" selected>OWL-V2</option>
|
|
@@ -217,11 +276,13 @@ button:disabled {
|
|
| 217 |
|
| 218 |
</div>
|
| 219 |
|
|
|
|
| 220 |
<script>
|
| 221 |
const API_BASE_URL = "https://biaslab2025-demo-2025.hf.space";
|
| 222 |
const PROCESS_VIDEO_URL = `${API_BASE_URL}/process_video`;
|
| 223 |
const SUMMARY_URL = `${API_BASE_URL}/mission_summary`;
|
| 224 |
const MISSION_PLAN_URL = `${API_BASE_URL}/mission_plan`;
|
|
|
|
| 225 |
|
| 226 |
const missionInputEl = document.getElementById("missionPrompt");
|
| 227 |
const detectorSelectEl = document.getElementById("detectorSelect");
|
|
@@ -231,12 +292,21 @@ const summaryEl = document.getElementById("summary");
|
|
| 231 |
const statusEl = document.getElementById("status");
|
| 232 |
const setMissionButton = document.getElementById("setMissionButton");
|
| 233 |
const executeButton = document.getElementById("executeButton");
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
|
| 235 |
let originalVideoUrl = null;
|
| 236 |
let currentMissionId = null;
|
| 237 |
let missionReady = false;
|
| 238 |
let missionRequestPending = false;
|
| 239 |
let videoProcessing = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
|
| 241 |
missionInputEl.addEventListener("input", handleMissionPromptEdit);
|
| 242 |
|
|
@@ -252,6 +322,84 @@ videoInputEl.addEventListener("change", () => {
|
|
| 252 |
}
|
| 253 |
});
|
| 254 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
function handleMissionPromptEdit() {
|
| 256 |
if (!missionReady && !currentMissionId) {
|
| 257 |
return;
|
|
@@ -266,7 +414,7 @@ function updateControlState() {
|
|
| 266 |
const disableVideoActions = !missionReady || missionRequestPending || videoProcessing;
|
| 267 |
videoInputEl.disabled = disableVideoActions;
|
| 268 |
executeButton.disabled = disableVideoActions;
|
| 269 |
-
setMissionButton.disabled = missionRequestPending;
|
| 270 |
}
|
| 271 |
|
| 272 |
function setStatus(message, tone = "info") {
|
|
@@ -280,12 +428,70 @@ function setStatus(message, tone = "info") {
|
|
| 280 |
}
|
| 281 |
}
|
| 282 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
async function stageMissionPrompt() {
|
| 284 |
const mission = missionInputEl.value.trim();
|
| 285 |
if (!mission) {
|
| 286 |
alert("Enter a mission prompt before setting it.");
|
| 287 |
return;
|
| 288 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
missionRequestPending = true;
|
| 290 |
missionReady = false;
|
| 291 |
currentMissionId = null;
|
|
@@ -295,6 +501,8 @@ async function stageMissionPrompt() {
|
|
| 295 |
const form = new FormData();
|
| 296 |
form.append("prompt", mission);
|
| 297 |
form.append("detector", detectorSelectEl.value);
|
|
|
|
|
|
|
| 298 |
const response = await fetch(MISSION_PLAN_URL, {
|
| 299 |
method: "POST",
|
| 300 |
body: form,
|
|
@@ -347,6 +555,8 @@ async function executeMission() {
|
|
| 347 |
videoForm.append("video", videoFile);
|
| 348 |
videoForm.append("prompt", mission);
|
| 349 |
videoForm.append("mission_id", currentMissionId);
|
|
|
|
|
|
|
| 350 |
videoForm.append("detector", detector);
|
| 351 |
|
| 352 |
console.log("[process_video] submitting request", { detector, missionLength: mission.length, fileSize: videoFile.size });
|
|
@@ -395,6 +605,8 @@ async function executeMission() {
|
|
| 395 |
summaryForm.append("video", videoFile);
|
| 396 |
summaryForm.append("prompt", mission);
|
| 397 |
summaryForm.append("mission_id", currentMissionId);
|
|
|
|
|
|
|
| 398 |
summaryForm.append("detector", detector);
|
| 399 |
|
| 400 |
console.log("[mission_summary] submitting request", { detector, missionLength: mission.length, fileSize: videoFile.size });
|
|
@@ -439,6 +651,9 @@ function setOriginalPreview(file) {
|
|
| 439 |
originalVideoEl.src = originalVideoUrl;
|
| 440 |
originalVideoEl.load();
|
| 441 |
}
|
|
|
|
|
|
|
|
|
|
| 442 |
</script>
|
| 443 |
|
| 444 |
</body>
|
|
|
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<title>Mission Console</title>
|
| 6 |
|
| 7 |
+
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css">
|
| 8 |
<style>
|
| 9 |
body {
|
| 10 |
margin: 0;
|
|
|
|
| 108 |
background-color: #3a3a3a;
|
| 109 |
}
|
| 110 |
|
| 111 |
+
.secondary-button.small {
|
| 112 |
+
padding: 8px 14px;
|
| 113 |
+
font-size: 14px;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
.prompt-actions {
|
| 117 |
display: flex;
|
| 118 |
flex-wrap: wrap;
|
|
|
|
| 120 |
margin-top: 8px;
|
| 121 |
}
|
| 122 |
|
| 123 |
+
.location-section {
|
| 124 |
+
margin-top: 20px;
|
| 125 |
+
padding: 16px;
|
| 126 |
+
border: 1px solid #2c2c2c;
|
| 127 |
+
border-radius: 10px;
|
| 128 |
+
background: #141414;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.location-hint {
|
| 132 |
+
font-size: 13px;
|
| 133 |
+
color: #bdbdbd;
|
| 134 |
+
margin-top: 6px;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
.location-map-container {
|
| 138 |
+
margin-top: 12px;
|
| 139 |
+
height: 260px;
|
| 140 |
+
border: 1px solid #2c2c2c;
|
| 141 |
+
border-radius: 8px;
|
| 142 |
+
overflow: hidden;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
#missionMap {
|
| 146 |
+
height: 100%;
|
| 147 |
+
width: 100%;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
.location-readout {
|
| 151 |
+
display: flex;
|
| 152 |
+
gap: 20px;
|
| 153 |
+
margin-top: 12px;
|
| 154 |
+
font-size: 14px;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
.output-section {
|
| 158 |
margin-top: 35px;
|
| 159 |
padding: 20px;
|
|
|
|
| 207 |
font-size: 13px;
|
| 208 |
min-height: 18px;
|
| 209 |
}
|
| 210 |
+
|
| 211 |
+
.status-message.small {
|
| 212 |
+
font-size: 12px;
|
| 213 |
+
margin-top: 10px;
|
| 214 |
+
}
|
| 215 |
</style>
|
| 216 |
|
| 217 |
</head>
|
|
|
|
| 235 |
</div>
|
| 236 |
</div>
|
| 237 |
|
| 238 |
+
<div class="location-section">
|
| 239 |
+
<label class="label">MISSION LOCATION</label>
|
| 240 |
+
<div class="location-hint">We will auto-detect your GPS (if allowed). Otherwise, click anywhere on the map to select the mission location.</div>
|
| 241 |
+
<div id="missionMapContainer" class="location-map-container">
|
| 242 |
+
<div id="missionMap"></div>
|
| 243 |
+
</div>
|
| 244 |
+
<div class="location-readout">
|
| 245 |
+
<div>Latitude: <span id="latitudeDisplay">--</span></div>
|
| 246 |
+
<div>Longitude: <span id="longitudeDisplay">--</span></div>
|
| 247 |
+
</div>
|
| 248 |
+
<div id="locationStatus" class="status-message small"></div>
|
| 249 |
+
<div id="locationIntel" class="status-message small" style="margin-top:6px;"></div>
|
| 250 |
+
</div>
|
| 251 |
+
|
| 252 |
<label class="label">OBJECT DETECTOR</label>
|
| 253 |
<select id="detectorSelect">
|
| 254 |
<option value="owlv2" selected>OWL-V2</option>
|
|
|
|
| 276 |
|
| 277 |
</div>
|
| 278 |
|
| 279 |
+
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
| 280 |
<script>
|
| 281 |
const API_BASE_URL = "https://biaslab2025-demo-2025.hf.space";
|
| 282 |
const PROCESS_VIDEO_URL = `${API_BASE_URL}/process_video`;
|
| 283 |
const SUMMARY_URL = `${API_BASE_URL}/mission_summary`;
|
| 284 |
const MISSION_PLAN_URL = `${API_BASE_URL}/mission_plan`;
|
| 285 |
+
const LOCATION_INTEL_URL = `${API_BASE_URL}/location_context`;
|
| 286 |
|
| 287 |
const missionInputEl = document.getElementById("missionPrompt");
|
| 288 |
const detectorSelectEl = document.getElementById("detectorSelect");
|
|
|
|
| 292 |
const statusEl = document.getElementById("status");
|
| 293 |
const setMissionButton = document.getElementById("setMissionButton");
|
| 294 |
const executeButton = document.getElementById("executeButton");
|
| 295 |
+
const latitudeDisplayEl = document.getElementById("latitudeDisplay");
|
| 296 |
+
const longitudeDisplayEl = document.getElementById("longitudeDisplay");
|
| 297 |
+
const locationStatusEl = document.getElementById("locationStatus");
|
| 298 |
+
const locationIntelEl = document.getElementById("locationIntel");
|
| 299 |
|
| 300 |
let originalVideoUrl = null;
|
| 301 |
let currentMissionId = null;
|
| 302 |
let missionReady = false;
|
| 303 |
let missionRequestPending = false;
|
| 304 |
let videoProcessing = false;
|
| 305 |
+
let locationReady = false;
|
| 306 |
+
let selectedLat = null;
|
| 307 |
+
let selectedLon = null;
|
| 308 |
+
let missionMap = null;
|
| 309 |
+
let missionMapMarker = null;
|
| 310 |
|
| 311 |
missionInputEl.addEventListener("input", handleMissionPromptEdit);
|
| 312 |
|
|
|
|
| 322 |
}
|
| 323 |
});
|
| 324 |
|
| 325 |
+
initializeMissionMap();
|
| 326 |
+
attemptAutoGps();
|
| 327 |
+
|
| 328 |
+
function initializeMissionMap() {
|
| 329 |
+
if (missionMap) {
|
| 330 |
+
return;
|
| 331 |
+
}
|
| 332 |
+
missionMap = L.map("missionMap").setView([20, 0], 2);
|
| 333 |
+
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
| 334 |
+
maxZoom: 19,
|
| 335 |
+
attribution: "© OpenStreetMap contributors",
|
| 336 |
+
}).addTo(missionMap);
|
| 337 |
+
missionMap.on("click", (event) => {
|
| 338 |
+
setSelectedLocation(event.latlng.lat, event.latlng.lng, "map");
|
| 339 |
+
});
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
function attemptAutoGps() {
|
| 343 |
+
if (!navigator.geolocation) {
|
| 344 |
+
setLocationStatus("GPS unavailable. Click on the map to choose a location.", "error");
|
| 345 |
+
return;
|
| 346 |
+
}
|
| 347 |
+
setLocationStatus("Detecting GPS location...");
|
| 348 |
+
navigator.geolocation.getCurrentPosition(
|
| 349 |
+
(position) => {
|
| 350 |
+
setSelectedLocation(position.coords.latitude, position.coords.longitude, "gps");
|
| 351 |
+
},
|
| 352 |
+
(error) => {
|
| 353 |
+
setLocationStatus(`GPS failed (${error.message}). Click on the map to select a location.`, "error");
|
| 354 |
+
},
|
| 355 |
+
{ enableHighAccuracy: true, timeout: 12000, maximumAge: 0 }
|
| 356 |
+
);
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
function setSelectedLocation(lat, lon, source) {
|
| 360 |
+
selectedLat = lat;
|
| 361 |
+
selectedLon = lon;
|
| 362 |
+
locationReady = true;
|
| 363 |
+
placeMapMarker(lat, lon);
|
| 364 |
+
updateLocationDisplay();
|
| 365 |
+
setLocationStatus(
|
| 366 |
+
`Location set via ${source.toUpperCase()}: ${lat.toFixed(4)}, ${lon.toFixed(4)}`,
|
| 367 |
+
"success"
|
| 368 |
+
);
|
| 369 |
+
if (currentMissionId) {
|
| 370 |
+
missionReady = false;
|
| 371 |
+
currentMissionId = null;
|
| 372 |
+
setStatus("Mission location changed. Set mission prompt again.");
|
| 373 |
+
}
|
| 374 |
+
updateControlState();
|
| 375 |
+
sendLocationIntel(lat, lon);
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
function updateLocationDisplay() {
|
| 379 |
+
latitudeDisplayEl.textContent = selectedLat !== null ? selectedLat.toFixed(4) : "--";
|
| 380 |
+
longitudeDisplayEl.textContent = selectedLon !== null ? selectedLon.toFixed(4) : "--";
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
function placeMapMarker(lat, lon) {
|
| 384 |
+
if (!missionMap) {
|
| 385 |
+
return;
|
| 386 |
+
}
|
| 387 |
+
const point = [lat, lon];
|
| 388 |
+
if (!missionMapMarker) {
|
| 389 |
+
missionMapMarker = L.circleMarker(point, {
|
| 390 |
+
radius: 8,
|
| 391 |
+
color: "#ff4d4d",
|
| 392 |
+
fillColor: "#ff4d4d",
|
| 393 |
+
fillOpacity: 0.95,
|
| 394 |
+
weight: 2,
|
| 395 |
+
}).addTo(missionMap);
|
| 396 |
+
} else {
|
| 397 |
+
missionMapMarker.setLatLng(point);
|
| 398 |
+
}
|
| 399 |
+
const targetZoom = missionMap.getZoom() < 8 ? 8 : missionMap.getZoom();
|
| 400 |
+
missionMap.setView(point, targetZoom);
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
function handleMissionPromptEdit() {
|
| 404 |
if (!missionReady && !currentMissionId) {
|
| 405 |
return;
|
|
|
|
| 414 |
const disableVideoActions = !missionReady || missionRequestPending || videoProcessing;
|
| 415 |
videoInputEl.disabled = disableVideoActions;
|
| 416 |
executeButton.disabled = disableVideoActions;
|
| 417 |
+
setMissionButton.disabled = missionRequestPending || !locationReady;
|
| 418 |
}
|
| 419 |
|
| 420 |
function setStatus(message, tone = "info") {
|
|
|
|
| 428 |
}
|
| 429 |
}
|
| 430 |
|
| 431 |
+
function setLocationStatus(message, tone = "info") {
|
| 432 |
+
locationStatusEl.textContent = message || "";
|
| 433 |
+
if (tone === "error") {
|
| 434 |
+
locationStatusEl.style.color = "#ff7b7b";
|
| 435 |
+
} else if (tone === "success") {
|
| 436 |
+
locationStatusEl.style.color = "#7bffb3";
|
| 437 |
+
} else {
|
| 438 |
+
locationStatusEl.style.color = "#bdbdbd";
|
| 439 |
+
}
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
let locationIntelAbortController = null;
|
| 443 |
+
|
| 444 |
+
async function sendLocationIntel(lat, lon) {
|
| 445 |
+
if (locationIntelAbortController) {
|
| 446 |
+
locationIntelAbortController.abort();
|
| 447 |
+
}
|
| 448 |
+
locationIntelAbortController = new AbortController();
|
| 449 |
+
locationIntelEl.textContent = "Analyzing location-based threats...";
|
| 450 |
+
locationIntelEl.style.color = "#ffa95c";
|
| 451 |
+
try {
|
| 452 |
+
const form = new FormData();
|
| 453 |
+
form.append("latitude", lat.toString());
|
| 454 |
+
form.append("longitude", lon.toString());
|
| 455 |
+
const response = await fetch(LOCATION_INTEL_URL, {
|
| 456 |
+
method: "POST",
|
| 457 |
+
body: form,
|
| 458 |
+
signal: locationIntelAbortController.signal,
|
| 459 |
+
});
|
| 460 |
+
if (!response.ok) {
|
| 461 |
+
throw new Error(`Location intelligence failed (${response.status})`);
|
| 462 |
+
}
|
| 463 |
+
const payload = await response.json();
|
| 464 |
+
const classes = (payload.mission_plan?.classes || []).slice(0, 4);
|
| 465 |
+
if (!classes.length) {
|
| 466 |
+
locationIntelEl.textContent = "Location intel: no threat classes suggested.";
|
| 467 |
+
locationIntelEl.style.color = "#bdbdbd";
|
| 468 |
+
return;
|
| 469 |
+
}
|
| 470 |
+
const classSummary = classes
|
| 471 |
+
.map((entry) => entry.name)
|
| 472 |
+
.join(", ");
|
| 473 |
+
locationIntelEl.textContent = `Likely threats near this location: ${classSummary}`;
|
| 474 |
+
locationIntelEl.style.color = "#7bffb3";
|
| 475 |
+
} catch (error) {
|
| 476 |
+
if (error.name === "AbortError") {
|
| 477 |
+
return;
|
| 478 |
+
}
|
| 479 |
+
console.error("[location_intel] error", error);
|
| 480 |
+
locationIntelEl.textContent = error.message || "Failed to analyze location.";
|
| 481 |
+
locationIntelEl.style.color = "#ff7b7b";
|
| 482 |
+
}
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
async function stageMissionPrompt() {
|
| 486 |
const mission = missionInputEl.value.trim();
|
| 487 |
if (!mission) {
|
| 488 |
alert("Enter a mission prompt before setting it.");
|
| 489 |
return;
|
| 490 |
}
|
| 491 |
+
if (!locationReady || selectedLat === null || selectedLon === null) {
|
| 492 |
+
alert("Set mission location via GPS or the map before proceeding.");
|
| 493 |
+
return;
|
| 494 |
+
}
|
| 495 |
missionRequestPending = true;
|
| 496 |
missionReady = false;
|
| 497 |
currentMissionId = null;
|
|
|
|
| 501 |
const form = new FormData();
|
| 502 |
form.append("prompt", mission);
|
| 503 |
form.append("detector", detectorSelectEl.value);
|
| 504 |
+
form.append("latitude", selectedLat.toString());
|
| 505 |
+
form.append("longitude", selectedLon.toString());
|
| 506 |
const response = await fetch(MISSION_PLAN_URL, {
|
| 507 |
method: "POST",
|
| 508 |
body: form,
|
|
|
|
| 555 |
videoForm.append("video", videoFile);
|
| 556 |
videoForm.append("prompt", mission);
|
| 557 |
videoForm.append("mission_id", currentMissionId);
|
| 558 |
+
videoForm.append("latitude", selectedLat?.toString() || "");
|
| 559 |
+
videoForm.append("longitude", selectedLon?.toString() || "");
|
| 560 |
videoForm.append("detector", detector);
|
| 561 |
|
| 562 |
console.log("[process_video] submitting request", { detector, missionLength: mission.length, fileSize: videoFile.size });
|
|
|
|
| 605 |
summaryForm.append("video", videoFile);
|
| 606 |
summaryForm.append("prompt", mission);
|
| 607 |
summaryForm.append("mission_id", currentMissionId);
|
| 608 |
+
summaryForm.append("latitude", selectedLat?.toString() || "");
|
| 609 |
+
summaryForm.append("longitude", selectedLon?.toString() || "");
|
| 610 |
summaryForm.append("detector", detector);
|
| 611 |
|
| 612 |
console.log("[mission_summary] submitting request", { detector, missionLength: mission.length, fileSize: videoFile.size });
|
|
|
|
| 651 |
originalVideoEl.src = originalVideoUrl;
|
| 652 |
originalVideoEl.load();
|
| 653 |
}
|
| 654 |
+
|
| 655 |
+
setLocationStatus("Awaiting GPS permission or map selection...");
|
| 656 |
+
updateControlState();
|
| 657 |
</script>
|
| 658 |
|
| 659 |
</body>
|