zye0616 commited on
Commit
710ec96
·
1 Parent(s): 5e3ba22

update: added gps and location intelligence

Browse files
Files changed (2) hide show
  1. app.py +80 -10
  2. demo.html +216 -1
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(prompt: str, detector: Optional[str], plan: MissionPlan) -> str:
 
 
 
 
 
 
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 _resolve_mission_plan(prompt: Optional[str], mission_id: Optional[str]) -> tuple[MissionPlan, str]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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(video: UploadFile | None, prompt: str | None, mission_id: str | None) -> None:
 
 
 
 
 
 
112
  if video is None:
113
  raise HTTPException(status_code=400, detail="Video file is required.")
114
- if not prompt and not mission_id:
 
 
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 {"mission_id": mission_id, "mission_plan": plan.to_dict()}
 
 
 
 
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: "&copy; 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>