zye0616 commited on
Commit
5e3ba22
·
1 Parent(s): 4cb2d06

update: two stages processing

Browse files
Files changed (3) hide show
  1. app.py +91 -9
  2. demo.html +223 -28
  3. inference.py +9 -5
app.py CHANGED
@@ -1,8 +1,11 @@
1
  import logging
2
  import os
3
  import tempfile
 
 
4
  from pathlib import Path
5
  from typing import Optional
 
6
 
7
  from fastapi import BackgroundTasks, FastAPI, File, Form, HTTPException, UploadFile
8
  from fastapi.middleware.cors import CORSMiddleware
@@ -10,6 +13,7 @@ from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
10
  import uvicorn
11
 
12
  from inference import run_inference
 
13
 
14
  logging.basicConfig(level=logging.INFO)
15
 
@@ -50,21 +54,94 @@ def _schedule_cleanup(background_tasks: BackgroundTasks, path: str) -> None:
50
  background_tasks.add_task(_cleanup)
51
 
52
 
53
- def _validate_inputs(video: UploadFile | None, prompt: str | None) -> None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  if video is None:
55
  raise HTTPException(status_code=400, detail="Video file is required.")
56
- if not prompt:
 
 
 
 
 
 
 
 
 
 
57
  raise HTTPException(status_code=400, detail="Prompt is required.")
 
 
 
 
 
 
 
 
58
 
59
 
60
  @app.post("/process_video")
61
  async def process_video(
62
  background_tasks: BackgroundTasks,
63
  video: UploadFile = File(...),
64
- prompt: str = Form(...),
 
65
  detector: Optional[str] = Form(None),
66
  ):
67
- _validate_inputs(video, prompt)
68
 
69
  try:
70
  input_path = _save_upload_to_tmp(video)
@@ -77,13 +154,15 @@ async def process_video(
77
  fd, output_path = tempfile.mkstemp(prefix="output_", suffix=".mp4", dir="/tmp")
78
  os.close(fd)
79
 
 
80
  try:
81
  output_path, _, _ = run_inference(
82
  input_path,
83
  output_path,
84
- prompt,
85
  detector_name=detector,
86
  generate_summary=False,
 
87
  )
88
  except ValueError as exc:
89
  logging.exception("Video decoding failed.")
@@ -110,10 +189,12 @@ async def process_video(
110
  @app.post("/mission_summary")
111
  async def mission_summary(
112
  video: UploadFile = File(...),
113
- prompt: str = Form(...),
 
114
  detector: Optional[str] = Form(None),
115
  ):
116
- _validate_inputs(video, prompt)
 
117
  try:
118
  input_path = _save_upload_to_tmp(video)
119
  except Exception:
@@ -123,13 +204,14 @@ async def mission_summary(
123
  await video.close()
124
 
125
  try:
126
- _, mission_plan, mission_summary = run_inference(
127
  input_path,
128
  output_video_path=None,
129
- mission_prompt=prompt,
130
  detector_name=detector,
131
  write_output_video=False,
132
  generate_summary=True,
 
133
  )
134
  except ValueError as exc:
135
  logging.exception("Video decoding failed.")
 
1
  import logging
2
  import os
3
  import tempfile
4
+ import time
5
+ from dataclasses import dataclass
6
  from pathlib import Path
7
  from typing import Optional
8
+ from uuid import uuid4
9
 
10
  from fastapi import BackgroundTasks, FastAPI, File, Form, HTTPException, UploadFile
11
  from fastapi.middleware.cors import CORSMiddleware
 
13
  import uvicorn
14
 
15
  from inference import run_inference
16
+ from mission_planner import MissionPlan, get_mission_plan
17
 
18
  logging.basicConfig(level=logging.INFO)
19
 
 
54
  background_tasks.add_task(_cleanup)
55
 
56
 
57
+ @dataclass
58
+ class CachedMission:
59
+ prompt: str
60
+ detector: Optional[str]
61
+ plan: MissionPlan
62
+ created_at: float
63
+
64
+
65
+ MISSION_CACHE: dict[str, CachedMission] = {}
66
+ MISSION_CACHE_TTL_SECONDS = 3600.0
67
+
68
+
69
+ def _prune_mission_cache() -> None:
70
+ now = time.time()
71
+ expired = [
72
+ key
73
+ for key, entry in MISSION_CACHE.items()
74
+ if now - entry.created_at > MISSION_CACHE_TTL_SECONDS
75
+ ]
76
+ for key in expired:
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
+ )
89
+ return mission_id
90
+
91
+
92
+ def _get_cached_mission(mission_id: str) -> CachedMission:
93
+ _prune_mission_cache()
94
+ entry = MISSION_CACHE.get(mission_id)
95
+ if entry is None:
96
+ raise HTTPException(status_code=404, detail="Mission prompt not found. Please set it again.")
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:
125
  raise HTTPException(status_code=400, detail="Prompt is required.")
126
+ try:
127
+ plan = get_mission_plan(normalized_prompt)
128
+ except Exception as exc:
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")
137
  async def process_video(
138
  background_tasks: BackgroundTasks,
139
  video: UploadFile = File(...),
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
  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,
161
  output_path,
162
+ mission_prompt,
163
  detector_name=detector,
164
  generate_summary=False,
165
+ mission_plan=mission_plan,
166
  )
167
  except ValueError as exc:
168
  logging.exception("Video decoding failed.")
 
189
  @app.post("/mission_summary")
190
  async def mission_summary(
191
  video: UploadFile = File(...),
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:
 
204
  await video.close()
205
 
206
  try:
207
+ _, _, mission_summary = run_inference(
208
  input_path,
209
  output_video_path=None,
210
+ mission_prompt=mission_prompt,
211
  detector_name=detector,
212
  write_output_video=False,
213
  generate_summary=True,
214
+ mission_plan=mission_plan,
215
  )
216
  except ValueError as exc:
217
  logging.exception("Video decoding failed.")
demo.html CHANGED
@@ -22,8 +22,8 @@ h1 {
22
  }
23
 
24
  .container {
25
- width: 80%;
26
- max-width: 900px;
27
  margin: auto;
28
  background: #1e1e1e;
29
  padding: 25px;
@@ -31,28 +31,39 @@ h1 {
31
  box-shadow: 0 15px 40px rgba(0, 0, 0, 0.25);
32
  }
33
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  .label {
35
  font-size: 15px;
36
  font-weight: 600;
37
- margin-top: 18px;
38
  display: block;
39
  color: #bdbdbd;
40
  }
41
 
42
  input[type="text"] {
43
- width: 100%;
44
  padding: 12px;
45
- margin-top: 5px;
46
  border: 1px solid #2c2c2c;
47
  background-color: #161616;
48
  color: #fff;
49
  font-size: 15px;
50
  border-radius: 8px;
 
 
51
  }
52
 
53
  input[type="file"],
54
  select {
55
- margin-top: 10px;
56
  padding: 10px;
57
  background-color: #161616;
58
  border: 1px solid #2c2c2c;
@@ -79,6 +90,30 @@ button:hover {
79
  background-color: #4f4f4f;
80
  }
81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  .output-section {
83
  margin-top: 35px;
84
  padding: 20px;
@@ -99,13 +134,39 @@ button:hover {
99
  border-radius: 8px;
100
  }
101
 
102
- #processedVideo {
103
  margin-top: 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  width: 100%;
 
 
105
  border: 1px solid #2a2a2a;
106
  border-radius: 8px;
107
  background-color: #000;
108
  }
 
 
 
 
 
 
 
109
  </style>
110
 
111
  </head>
@@ -115,11 +176,19 @@ button:hover {
115
 
116
  <div class="container">
117
 
118
- <label class="label">MISSION PROMPT</label>
119
- <input id="missionPrompt" type="text" placeholder="e.g., Track hostile drone movement...">
120
-
121
- <label class="label">UPLOAD VIDEO (.mp4)</label>
122
- <input id="videoInput" type="file" accept="video/mp4">
 
 
 
 
 
 
 
 
123
 
124
  <label class="label">OBJECT DETECTOR</label>
125
  <select id="detectorSelect">
@@ -128,14 +197,22 @@ button:hover {
128
  <option value="hf_yolov8_defence">YOLOv8m Defence</option>
129
  </select>
130
 
131
- <button onclick="executeMission()">EXECUTE MISSION</button>
132
 
133
  <div class="output-section">
134
- <h2 style="color:#00ffea; margin-bottom:10px;">MISSION SUMMARY</h2>
 
 
 
 
 
 
 
 
 
 
135
  <div id="summary">(Awaiting mission results...)</div>
136
- <h2 style="color:#00ffea; margin-top:25px;">PROCESSED VIDEO FEED</h2>
137
- <video id="processedVideo" controls></video>
138
- <div id="status" style="margin-top:15px;color:#ffa95c;font-size:13px;"></div>
139
  </div>
140
 
141
  </div>
@@ -144,27 +221,132 @@ button:hover {
144
  const API_BASE_URL = "https://biaslab2025-demo-2025.hf.space";
145
  const PROCESS_VIDEO_URL = `${API_BASE_URL}/process_video`;
146
  const SUMMARY_URL = `${API_BASE_URL}/mission_summary`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
  async function executeMission() {
149
 
150
- const videoFile = document.getElementById("videoInput").files[0];
151
- const mission = document.getElementById("missionPrompt").value;
152
- const detector = document.getElementById("detectorSelect").value;
153
- const summaryEl = document.getElementById("summary");
154
- const statusEl = document.getElementById("status");
155
 
156
- if (!videoFile || !mission) {
157
- alert("Mission invalid: Upload video and enter mission parameters.");
 
 
 
 
158
  return;
159
  }
160
 
161
- statusEl.textContent = "Processing video...";
 
162
  summaryEl.textContent = "(Awaiting summary...)";
 
 
 
163
 
164
  try {
165
  const videoForm = new FormData();
166
  videoForm.append("video", videoFile);
167
  videoForm.append("prompt", mission);
 
168
  videoForm.append("detector", detector);
169
 
170
  console.log("[process_video] submitting request", { detector, missionLength: mission.length, fileSize: videoFile.size });
@@ -207,11 +389,12 @@ async function executeMission() {
207
  console.error("[process_video] immediate video error", videoEl.error);
208
  }
209
 
210
- statusEl.textContent = "Generating summary...";
211
 
212
  const summaryForm = new FormData();
213
  summaryForm.append("video", videoFile);
214
  summaryForm.append("prompt", mission);
 
215
  summaryForm.append("detector", detector);
216
 
217
  console.log("[mission_summary] submitting request", { detector, missionLength: mission.length, fileSize: videoFile.size });
@@ -237,12 +420,24 @@ async function executeMission() {
237
  const summaryJson = await summaryResponse.json();
238
  const summaryText = summaryJson.mission_summary || "No summary returned.";
239
  summaryEl.textContent = summaryText;
240
- statusEl.textContent = "Mission complete.";
241
  } catch (err) {
242
  console.error(err);
243
  summaryEl.textContent = "Mission failed.";
244
- statusEl.textContent = `Error: ${err.message}`;
 
 
 
 
 
 
 
 
 
245
  }
 
 
 
246
  }
247
  </script>
248
 
 
22
  }
23
 
24
  .container {
25
+ width: 90%;
26
+ max-width: 1100px;
27
  margin: auto;
28
  background: #1e1e1e;
29
  padding: 25px;
 
31
  box-shadow: 0 15px 40px rgba(0, 0, 0, 0.25);
32
  }
33
 
34
+ .form-grid {
35
+ display: flex;
36
+ flex-wrap: wrap;
37
+ gap: 20px;
38
+ margin-top: 10px;
39
+ }
40
+
41
+ .form-control {
42
+ flex: 1;
43
+ min-width: 260px;
44
+ }
45
+
46
  .label {
47
  font-size: 15px;
48
  font-weight: 600;
 
49
  display: block;
50
  color: #bdbdbd;
51
  }
52
 
53
  input[type="text"] {
 
54
  padding: 12px;
 
55
  border: 1px solid #2c2c2c;
56
  background-color: #161616;
57
  color: #fff;
58
  font-size: 15px;
59
  border-radius: 8px;
60
+ width: 100%;
61
+ margin-top: 5px;
62
  }
63
 
64
  input[type="file"],
65
  select {
66
+ margin-top: 5px;
67
  padding: 10px;
68
  background-color: #161616;
69
  border: 1px solid #2c2c2c;
 
90
  background-color: #4f4f4f;
91
  }
92
 
93
+ button:disabled {
94
+ background-color: #1f1f1f;
95
+ color: #6e6e6e;
96
+ cursor: not-allowed;
97
+ }
98
+
99
+ .secondary-button {
100
+ width: auto;
101
+ padding: 10px 18px;
102
+ margin-top: 10px;
103
+ background-color: #262626;
104
+ }
105
+
106
+ .secondary-button:hover {
107
+ background-color: #3a3a3a;
108
+ }
109
+
110
+ .prompt-actions {
111
+ display: flex;
112
+ flex-wrap: wrap;
113
+ gap: 10px;
114
+ margin-top: 8px;
115
+ }
116
+
117
  .output-section {
118
  margin-top: 35px;
119
  padding: 20px;
 
134
  border-radius: 8px;
135
  }
136
 
137
+ .video-section {
138
  margin-top: 20px;
139
+ display: flex;
140
+ flex-wrap: wrap;
141
+ gap: 25px;
142
+ }
143
+
144
+ .video-panel {
145
+ flex: 1;
146
+ min-width: 320px;
147
+ }
148
+
149
+ .video-panel h2 {
150
+ color: #00ffea;
151
+ margin-bottom: 10px;
152
+ font-size: 18px;
153
+ }
154
+
155
+ .mission-video {
156
  width: 100%;
157
+ max-height: 420px;
158
+ object-fit: contain;
159
  border: 1px solid #2a2a2a;
160
  border-radius: 8px;
161
  background-color: #000;
162
  }
163
+
164
+ .status-message {
165
+ margin-top: 15px;
166
+ color: #ffa95c;
167
+ font-size: 13px;
168
+ min-height: 18px;
169
+ }
170
  </style>
171
 
172
  </head>
 
176
 
177
  <div class="container">
178
 
179
+ <div class="form-grid">
180
+ <div class="form-control">
181
+ <label class="label">MISSION PROMPT</label>
182
+ <input id="missionPrompt" type="text" placeholder="e.g., Track hostile drone movement...">
183
+ <div class="prompt-actions">
184
+ <button type="button" id="setMissionButton" class="secondary-button" onclick="stageMissionPrompt()">Set Mission Prompt</button>
185
+ </div>
186
+ </div>
187
+ <div class="form-control">
188
+ <label class="label">UPLOAD VIDEO (.mp4)</label>
189
+ <input id="videoInput" type="file" accept="video/mp4" disabled>
190
+ </div>
191
+ </div>
192
 
193
  <label class="label">OBJECT DETECTOR</label>
194
  <select id="detectorSelect">
 
197
  <option value="hf_yolov8_defence">YOLOv8m Defence</option>
198
  </select>
199
 
200
+ <button type="button" id="executeButton" onclick="executeMission()" disabled>EXECUTE MISSION</button>
201
 
202
  <div class="output-section">
203
+ <div class="video-section">
204
+ <div class="video-panel">
205
+ <h2>ORIGINAL VIDEO</h2>
206
+ <video id="originalVideo" class="mission-video" controls></video>
207
+ </div>
208
+ <div class="video-panel">
209
+ <h2>PROCESSED VIDEO FEED</h2>
210
+ <video id="processedVideo" class="mission-video" controls></video>
211
+ </div>
212
+ </div>
213
+ <h2 style="color:#00ffea; margin:25px 0 10px;">MISSION SUMMARY</h2>
214
  <div id="summary">(Awaiting mission results...)</div>
215
+ <div id="status" class="status-message"></div>
 
 
216
  </div>
217
 
218
  </div>
 
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");
228
+ const originalVideoEl = document.getElementById("originalVideo");
229
+ const videoInputEl = document.getElementById("videoInput");
230
+ 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
+
243
+ videoInputEl.addEventListener("change", () => {
244
+ const uploaded = videoInputEl.files[0];
245
+ if (uploaded) {
246
+ setOriginalPreview(uploaded);
247
+ } else if (originalVideoUrl) {
248
+ URL.revokeObjectURL(originalVideoUrl);
249
+ originalVideoEl.removeAttribute("src");
250
+ originalVideoEl.load();
251
+ originalVideoUrl = null;
252
+ }
253
+ });
254
+
255
+ function handleMissionPromptEdit() {
256
+ if (!missionReady && !currentMissionId) {
257
+ return;
258
+ }
259
+ missionReady = false;
260
+ currentMissionId = null;
261
+ setStatus("Mission prompt changed. Set it again to process videos.");
262
+ updateControlState();
263
+ }
264
+
265
+ 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") {
273
+ statusEl.textContent = message || "";
274
+ if (tone === "error") {
275
+ statusEl.style.color = "#ff7b7b";
276
+ } else if (tone === "success") {
277
+ statusEl.style.color = "#7bffb3";
278
+ } else {
279
+ statusEl.style.color = "#ffa95c";
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;
292
+ updateControlState();
293
+ setStatus("Preparing mission prompt...");
294
+ try {
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,
301
+ });
302
+ if (!response.ok) {
303
+ let errorDetail = `Failed to set mission prompt (${response.status})`;
304
+ try {
305
+ const errJson = await response.json();
306
+ errorDetail = errJson.detail || errJson.error || errorDetail;
307
+ } catch (_) {}
308
+ throw new Error(errorDetail);
309
+ }
310
+ const payload = await response.json();
311
+ currentMissionId = payload.mission_id;
312
+ missionReady = true;
313
+ setStatus("Mission prompt ready. Upload videos to process.", "success");
314
+ } catch (error) {
315
+ console.error("[mission_plan] error", error);
316
+ setStatus(error.message || "Failed to set mission prompt.", "error");
317
+ } finally {
318
+ missionRequestPending = false;
319
+ updateControlState();
320
+ }
321
+ }
322
 
323
  async function executeMission() {
324
 
325
+ const videoFile = videoInputEl.files[0];
326
+ const mission = missionInputEl.value.trim();
327
+ const detector = detectorSelectEl.value;
 
 
328
 
329
+ if (!currentMissionId || !missionReady) {
330
+ alert("Set the mission prompt before processing videos.");
331
+ return;
332
+ }
333
+ if (!videoFile) {
334
+ alert("Mission invalid: Upload a video file.");
335
  return;
336
  }
337
 
338
+ setOriginalPreview(videoFile);
339
+
340
  summaryEl.textContent = "(Awaiting summary...)";
341
+ videoProcessing = true;
342
+ updateControlState();
343
+ setStatus("Processing video...");
344
 
345
  try {
346
  const videoForm = new FormData();
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 });
 
389
  console.error("[process_video] immediate video error", videoEl.error);
390
  }
391
 
392
+ setStatus("Generating summary...");
393
 
394
  const summaryForm = new FormData();
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 });
 
420
  const summaryJson = await summaryResponse.json();
421
  const summaryText = summaryJson.mission_summary || "No summary returned.";
422
  summaryEl.textContent = summaryText;
423
+ setStatus("Mission complete.", "success");
424
  } catch (err) {
425
  console.error(err);
426
  summaryEl.textContent = "Mission failed.";
427
+ setStatus(`Error: ${err.message}`, "error");
428
+ } finally {
429
+ videoProcessing = false;
430
+ updateControlState();
431
+ }
432
+ }
433
+
434
+ function setOriginalPreview(file) {
435
+ if (originalVideoUrl) {
436
+ URL.revokeObjectURL(originalVideoUrl);
437
  }
438
+ originalVideoUrl = URL.createObjectURL(file);
439
+ originalVideoEl.src = originalVideoUrl;
440
+ originalVideoEl.load();
441
  }
442
  </script>
443
 
inference.py CHANGED
@@ -72,6 +72,7 @@ def run_inference(
72
  detector_name: Optional[str] = None,
73
  write_output_video: bool = True,
74
  generate_summary: bool = True,
 
75
  ) -> Tuple[Optional[str], MissionPlan, Optional[str]]:
76
  try:
77
  frames, fps, width, height = extract_frames(input_video_path)
@@ -79,9 +80,12 @@ def run_inference(
79
  logging.exception("Failed to decode video at %s", input_video_path)
80
  raise
81
 
82
- mission_plan = get_mission_plan(mission_prompt)
83
- logging.info("Mission plan: %s", mission_plan.to_json())
84
- queries = mission_plan.queries()
 
 
 
85
 
86
  processed_frames: List[np.ndarray] = []
87
  detection_log: List[Dict[str, Any]] = []
@@ -101,6 +105,6 @@ def run_inference(
101
  else:
102
  video_path_result = None
103
  mission_summary = (
104
- summarize_results(mission_prompt, mission_plan, detection_log) if generate_summary else None
105
  )
106
- return video_path_result, mission_plan, mission_summary
 
72
  detector_name: Optional[str] = None,
73
  write_output_video: bool = True,
74
  generate_summary: bool = True,
75
+ mission_plan: Optional[MissionPlan] = None,
76
  ) -> Tuple[Optional[str], MissionPlan, Optional[str]]:
77
  try:
78
  frames, fps, width, height = extract_frames(input_video_path)
 
80
  logging.exception("Failed to decode video at %s", input_video_path)
81
  raise
82
 
83
+ mission_prompt_clean = (mission_prompt or "").strip()
84
+ if not mission_prompt_clean:
85
+ raise ValueError("Mission prompt is required.")
86
+ resolved_plan = mission_plan or get_mission_plan(mission_prompt_clean)
87
+ logging.info("Mission plan: %s", resolved_plan.to_json())
88
+ queries = resolved_plan.queries()
89
 
90
  processed_frames: List[np.ndarray] = []
91
  detection_log: List[Dict[str, Any]] = []
 
105
  else:
106
  video_path_result = None
107
  mission_summary = (
108
+ summarize_results(mission_prompt_clean, resolved_plan, detection_log) if generate_summary else None
109
  )
110
+ return video_path_result, resolved_plan, mission_summary