lucid-hf commited on
Commit
7042293
·
verified ·
1 Parent(s): 0b0897f

CI: deploy Docker/PDM Space

Browse files
demo media/Bushland Beacon 1.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8aadc5547f57edf2bfcf17c4d89095d62ae4646f9aaa40675068d0a3c585c49e
3
+ size 34102597
services/app_service/app.py CHANGED
@@ -29,7 +29,7 @@ def main() -> None:
29
  st.page_link("pages/task_drone.py", label="Task Drone")
30
 
31
 
32
- bg_image = local_image_to_data_url("resources/images/rescue.jpg")
33
  hide_default_css = """
34
  <style>
35
  #MainMenu {visibility: hidden;}
@@ -129,12 +129,12 @@ def main() -> None:
129
  <section class="hero">
130
  <div class="content">
131
  <h1>SAR-X<sup>ai</h1>
132
- <h2>NATSAR Detection Hub</h2>
133
  <div class="subtitle">
134
  AI-powered person and vessel recognition
135
  </div>
136
  <div class="disclaimer">
137
- Note: this is a mock-up of a potential app
138
  </divd>
139
  </div>
140
  </section>
 
29
  st.page_link("pages/task_drone.py", label="Task Drone")
30
 
31
 
32
+ bg_image = local_image_to_data_url("resources/images/rescue3.jpg")
33
  hide_default_css = """
34
  <style>
35
  #MainMenu {visibility: hidden;}
 
129
  <section class="hero">
130
  <div class="content">
131
  <h1>SAR-X<sup>ai</h1>
132
+ <h2>Detection Hub</h2>
133
  <div class="subtitle">
134
  AI-powered person and vessel recognition
135
  </div>
136
  <div class="disclaimer">
137
+ (application mock-up)
138
  </divd>
139
  </div>
140
  </section>
services/app_service/pages/bushland_beacon.py CHANGED
@@ -1,26 +1,60 @@
1
  # pages/bushland_beacon.py
 
 
2
  import io
3
- import tempfile
4
  import time
 
 
 
5
  from pathlib import Path
 
6
 
7
  import cv2
8
  import numpy as np
9
  import streamlit as st
10
  from PIL import Image
 
 
 
 
 
 
 
11
  from utils.model_manager import get_model_manager, load_model
12
 
13
- # =============== Page setup ===============
14
- st.set_page_config(
15
- page_title="Bushland Beacon ", layout="wide", initial_sidebar_state="expanded"
16
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  st.markdown(
19
  "<h2 style='text-align:center;margin-top:0'>SAR-X<sup>ai</sup></h2>"
20
  "<h2 style='text-align:center;margin-top:0'>Bushland Beacon 🚨 </h2>",
21
  unsafe_allow_html=True,
22
  )
23
- # =============== Sidebar: custom menu + cards ===============
 
24
  with st.sidebar:
25
  st.page_link("app.py", label="Home")
26
  st.page_link("pages/bushland_beacon.py", label="Bushland Beacon")
@@ -29,55 +63,278 @@ with st.sidebar:
29
  st.markdown("---")
30
  st.page_link("pages/task_satellite.py", label="Task Satellite")
31
  st.page_link("pages/task_drone.py", label="Task Drone")
32
-
33
  st.markdown("---")
34
 
35
- # Simple "card" styling in the sidebar
36
- st.markdown(
37
- """
38
- <style>
39
- .sb-card {border:1px solid rgba(255,255,255,0.15); padding:14px; border-radius:8px; margin-bottom:16px;}
40
- .sb-card h4 {margin:0 0 10px 0; font-weight:700;}
41
- </style>
42
- """,
43
- unsafe_allow_html=True,
44
- )
45
-
46
- # Image Detection card
47
  st.sidebar.header("Image Detection")
48
- img_file = st.file_uploader(
49
- "Upload an image", type=["jpg", "jpeg", "png"], key="img_up"
50
- )
51
  run_img = st.button("🔍 Run Image Detection", use_container_width=True)
52
 
53
- # Video Detection card
54
- st.sidebar.header("Video Detection")
55
- vid_file = st.file_uploader(
56
- "Upload a video", type=["mp4", "mov", "avi", "mkv"], key="vid_up"
57
- )
58
- run_vid = st.button("🎥 Run Video Detection", use_container_width=True)
 
 
 
 
59
 
60
  st.sidebar.markdown("---")
61
- # Parameters card (shared)
62
  st.sidebar.header("Parameters")
63
- conf_thr = st.slider("Minimum confidence threshold", 0.05, 0.95, 0.50, 0.01)
64
 
65
- st.sidebar.markdown("---")
66
 
67
- # Get model manager instance
68
- model_manager = get_model_manager()
 
 
 
 
69
 
70
- # Render model selection UI
71
- model_label, model_key = model_manager.render_model_selection(
72
- key_prefix="bushland_beacon"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  )
74
- st.sidebar.markdown("---")
75
 
76
- # Render device information
 
 
 
77
  model_manager.render_device_info()
78
 
79
 
80
- # =============== Detection helpers ===============
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  def run_image_detection(uploaded_file, conf_thr: float = 0.5, model_key: str = "deim"):
82
  try:
83
  data = uploaded_file.getvalue()
@@ -89,95 +346,225 @@ def run_image_detection(uploaded_file, conf_thr: float = 0.5, model_key: str = "
89
 
90
  try:
91
  model = load_model(model_key)
92
- with st.spinner("Running detection..."):
93
- annotated = model.predict_image(img, min_confidence=conf_thr)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
  st.subheader("🎯 Detection Results")
96
- st.image(annotated, caption="Detections", width="stretch")
 
 
 
 
97
  except Exception as e:
98
  st.error(f"Error during detection: {e}")
99
 
100
 
101
- def run_video_detection(vid_bytes, conf_thr: float = 0.5, model_key: str = "deim"):
102
- tmp_in = Path(tempfile.gettempdir()) / f"in_{int(time.time())}.mp4"
 
 
 
 
 
 
 
 
 
 
103
  with open(tmp_in, "wb") as f:
104
  f.write(vid_bytes)
105
 
 
106
  model = load_model(model_key)
107
-
108
- # Set up video capture for preview
109
- cap = cv2.VideoCapture(str(tmp_in))
 
 
 
 
 
 
110
  if not cap.isOpened():
111
  st.error("Failed to open the uploaded video.")
112
  return
113
 
114
- fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
 
 
 
 
 
115
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
 
 
116
 
117
- # Set up preview and progress
118
  frame_ph = st.empty()
119
- prog = st.progress(0.0, text="Processing…")
120
-
121
- # Set up video writer for output
122
- W = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
123
- H = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
124
- tmp_out = Path(tempfile.gettempdir()) / f"out_{int(time.time())}.mp4"
125
- writer = cv2.VideoWriter(str(tmp_out), cv2.VideoWriter_fourcc(*"mp4v"), fps, (W, H))
126
-
127
- frame_count = 0
128
- last_preview_update = 0
129
- preview_update_interval = 5 # Update preview every 5 frames
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
  try:
132
- with st.spinner("Processing video with live preview…"):
133
  while True:
134
- ok, frame = cap.read()
135
- if not ok:
136
  break
137
 
138
- # Process frame with model
139
- if model_key == "deim":
140
- frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
141
- annotated_pil = model.predict_image(
142
- Image.fromarray(frame_rgb), min_confidence=conf_thr
143
- )
144
- vis = cv2.cvtColor(np.array(annotated_pil), cv2.COLOR_RGB2BGR)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  else:
146
- _, vis = model.predict_and_visualize(
147
- frame, min_confidence=conf_thr, show_score=True
148
- )
149
-
150
- # Update progress
151
- progress = frame_count / total_frames if total_frames > 0 else 0
152
- prog.progress(
153
- progress,
154
- text=f"Processing frame {frame_count + 1}/{total_frames}...",
155
- )
156
-
157
- # Update preview (throttled to prevent freezing)
158
- if (frame_count - last_preview_update) >= preview_update_interval:
 
 
 
 
 
 
 
 
 
159
  frame_ph.image(
160
- cv2.cvtColor(vis, cv2.COLOR_BGR2RGB),
161
  use_container_width=True,
162
  output_format="JPEG",
163
  channels="RGB",
164
  )
165
- last_preview_update = frame_count
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
- # Write frame to output video
168
- writer.write(vis)
169
- frame_count += 1
170
 
171
  except Exception as exc:
172
  st.error(f"Video detection failed: {exc}")
173
  return
174
  finally:
175
- cap.release()
176
- writer.release()
 
 
 
 
 
 
 
177
 
178
  st.success("Done!")
179
 
180
- # Check if output file exists before trying to display it
181
  if tmp_out.exists():
182
  st.video(str(tmp_out))
183
  with open(tmp_out, "rb") as f:
@@ -191,17 +578,38 @@ def run_video_detection(vid_bytes, conf_thr: float = 0.5, model_key: str = "deim
191
  st.error("Video processing completed but output file was not created.")
192
 
193
 
194
- # =============== Main: hook up actions ===============
195
  if run_img:
196
  if img_file is None:
197
  st.warning("Please upload an image first.")
198
  else:
199
  run_image_detection(img_file, conf_thr=conf_thr, model_key=model_key)
200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  if run_vid:
202
  if vid_file is None:
203
  st.warning("Please upload a video first.")
204
  else:
 
205
  run_video_detection(
206
- vid_bytes=vid_file.read(), conf_thr=conf_thr, model_key=model_key
 
 
 
 
 
 
207
  )
 
1
  # pages/bushland_beacon.py
2
+
3
+
4
  import io
 
5
  import time
6
+ import queue
7
+ import threading
8
+ import tempfile
9
  from pathlib import Path
10
+ from contextlib import contextmanager
11
 
12
  import cv2
13
  import numpy as np
14
  import streamlit as st
15
  from PIL import Image
16
+
17
+ # Torch (optional)
18
+ try:
19
+ import torch
20
+ except Exception:
21
+ torch = None
22
+
23
  from utils.model_manager import get_model_manager, load_model
24
 
25
+
26
+ # ================== CONFIG ==================
27
+ # --- User-tunable parameters ---
28
+ DEFAULT_CONF_THRESHOLD = 0.30 # Detection confidence
29
+ DEFAULT_TARGET_SHORT_SIDE = 960 # Resize short edge (px)
30
+ DEFAULT_MAX_PREVIEW_FPS = 30 # Limit UI update frequency
31
+ DEFAULT_DROP_IF_BEHIND = False # Drop frames if lagging
32
+ DEFAULT_PROCESS_STRIDE = 1 # Process every Nth frame (1=all)
33
+ DEFAULT_QUEUE_SIZE = 24 # Frame queue length
34
+ DEFAULT_WRITER_CODEC = "mp4v" # Codec to avoid OpenH264 issue
35
+ DEFAULT_TMP_EXT = ".mp4" # Temp file extension
36
+ DEFAULT_MAX_SLIDER_SHORT_SIDE = 1080 # Max short side slider
37
+ DEFAULT_MIN_SLIDER_SHORT_SIDE = 256 # Min short side slider
38
+ DEFAULT_MIN_FPS_SLIDER = 1 # Min preview FPS slider
39
+ DEFAULT_MAX_FPS_SLIDER = 30 # Max preview FPS slider
40
+ # ============================================
41
+
42
+
43
+ # ============== Session state (stop flag) ==============
44
+ if "stop_video" not in st.session_state:
45
+ st.session_state["stop_video"] = False
46
+
47
+
48
+ # ================== Page setup ==================
49
+ st.set_page_config(page_title="Bushland Beacon", layout="wide", initial_sidebar_state="expanded")
50
 
51
  st.markdown(
52
  "<h2 style='text-align:center;margin-top:0'>SAR-X<sup>ai</sup></h2>"
53
  "<h2 style='text-align:center;margin-top:0'>Bushland Beacon 🚨 </h2>",
54
  unsafe_allow_html=True,
55
  )
56
+
57
+ # ================== Sidebar ==================
58
  with st.sidebar:
59
  st.page_link("app.py", label="Home")
60
  st.page_link("pages/bushland_beacon.py", label="Bushland Beacon")
 
63
  st.markdown("---")
64
  st.page_link("pages/task_satellite.py", label="Task Satellite")
65
  st.page_link("pages/task_drone.py", label="Task Drone")
 
66
  st.markdown("---")
67
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  st.sidebar.header("Image Detection")
69
+ img_file = st.file_uploader("Upload an image", type=["jpg", "jpeg", "png"], key="img_up")
 
 
70
  run_img = st.button("🔍 Run Image Detection", use_container_width=True)
71
 
72
+ st.sidebar.header("Video")
73
+ vid_file = st.file_uploader("Upload a video", type=["mp4", "mov", "avi", "mkv"], key="vid_up")
74
+
75
+ # New buttons
76
+ run_vid_plain = st.button("Play Video", use_container_width=True)
77
+ run_vid = st.button("🎥 Run Detection", use_container_width=True)
78
+ stop_vid = st.button("Stop", use_container_width=True)
79
+
80
+ if stop_vid:
81
+ st.session_state["stop_video"] = True
82
 
83
  st.sidebar.markdown("---")
 
84
  st.sidebar.header("Parameters")
 
85
 
86
+ conf_thr = st.slider("Minimum confidence threshold", 0.05, 0.95, DEFAULT_CONF_THRESHOLD, 0.01)
87
 
88
+ target_short_side = st.select_slider(
89
+ "Target short-side (downscale)",
90
+ options=[256, 320, 384, 448, 512, 640, 720, 800, 864, 960, 1080],
91
+ value=DEFAULT_TARGET_SHORT_SIDE,
92
+ help="Resize so the shorter edge equals this value. Smaller = faster."
93
+ )
94
 
95
+ max_preview_fps = st.slider(
96
+ "Max preview FPS",
97
+ min_value=DEFAULT_MIN_FPS_SLIDER,
98
+ max_value=DEFAULT_MAX_FPS_SLIDER,
99
+ value=DEFAULT_MAX_PREVIEW_FPS,
100
+ help="Throttles UI updates for smoother preview."
101
+ )
102
+
103
+ drop_if_behind = st.toggle(
104
+ "Drop frames if behind",
105
+ value=DEFAULT_DROP_IF_BEHIND,
106
+ help="Drop frames to maintain smooth preview."
107
+ )
108
+
109
+ process_stride = st.slider(
110
+ "Process every Nth frame",
111
+ min_value=1,
112
+ max_value=5,
113
+ value=DEFAULT_PROCESS_STRIDE,
114
+ help="1 = every frame; higher values reuse last result."
115
  )
 
116
 
117
+ st.sidebar.markdown("---")
118
+ model_manager = get_model_manager()
119
+ model_label, model_key = model_manager.render_model_selection(key_prefix="bushland_beacon")
120
+ st.sidebar.markdown("---")
121
  model_manager.render_device_info()
122
 
123
 
124
+ # ================== Perf knobs for OpenCV ==================
125
+ try:
126
+ cv2.setNumThreads(1)
127
+ except Exception:
128
+ pass
129
+ try:
130
+ cv2.ocl.setUseOpenCL(False)
131
+ except Exception:
132
+ pass
133
+
134
+
135
+ # ================== Helper functions ==================
136
+ def _resize_keep_aspect(img_bgr: np.ndarray, short_side: int) -> np.ndarray:
137
+ h, w = img_bgr.shape[:2]
138
+ if min(h, w) == short_side:
139
+ return img_bgr
140
+ if h < w:
141
+ new_h = short_side
142
+ new_w = int(round(w * (short_side / h)))
143
+ else:
144
+ new_w = short_side
145
+ new_h = int(round(h * (short_side / w)))
146
+ return cv2.resize(img_bgr, (new_w, new_h), interpolation=cv2.INTER_AREA)
147
+
148
+
149
+ def _should_force_cpu_for_model(model_key: str) -> bool:
150
+ return (model_key or "").lower() == "deim"
151
+
152
+
153
+ def _choose_device(model_key: str) -> str:
154
+ if _should_force_cpu_for_model(model_key):
155
+ return "cpu"
156
+ if torch is not None and torch.cuda.is_available():
157
+ return "cuda"
158
+ return "cpu"
159
+
160
+
161
+ def _warmup_model(model, model_key: str, shape=(720, 1280, 3), conf: float = 0.25):
162
+ dummy = np.zeros(shape, dtype=np.uint8)
163
+ try:
164
+ if (model_key or "").lower() == "deim":
165
+ pil = Image.fromarray(cv2.cvtColor(dummy, cv2.COLOR_BGR2RGB))
166
+ model.predict_image(pil, min_confidence=conf)
167
+ else:
168
+ model.predict_and_visualize(dummy, min_confidence=conf, show_score=False)
169
+ except Exception:
170
+ pass
171
+
172
+
173
+ @contextmanager
174
+ def maybe_autocast(enabled: bool):
175
+ if enabled and torch is not None and torch.cuda.is_available():
176
+ with torch.cuda.amp.autocast():
177
+ yield
178
+ else:
179
+ yield
180
+
181
+
182
+ def _device_hint() -> str:
183
+ if torch is None:
184
+ return "cpu"
185
+ return "cuda" if torch.cuda.is_available() else "cpu"
186
+
187
+
188
+ # ================== Passthrough (no model, no boxes) ==================
189
+ def run_video_passthrough(
190
+ vid_bytes: bytes,
191
+ target_short_side: int = DEFAULT_TARGET_SHORT_SIDE,
192
+ max_preview_fps: int = DEFAULT_MAX_PREVIEW_FPS,
193
+ drop_if_behind: bool = DEFAULT_DROP_IF_BEHIND,
194
+ ):
195
+ """Play the uploaded video with scaling & pacing only (no inference, no overlays)."""
196
+ ts = int(time.time() * 1000)
197
+ tmp_in = Path(tempfile.gettempdir()) / f"in_{ts}{DEFAULT_TMP_EXT}"
198
+ with open(tmp_in, "wb") as f:
199
+ f.write(vid_bytes)
200
+
201
+ cap = cv2.VideoCapture(str(tmp_in), cv2.CAP_FFMPEG)
202
+ if not cap.isOpened():
203
+ st.error("Failed to open the uploaded video.")
204
+ return
205
+
206
+ try:
207
+ cap.set(cv2.CAP_PROP_BUFFERSIZE, 2)
208
+ except Exception:
209
+ pass
210
+
211
+ src_fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
212
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
213
+
214
+ # UI placeholders
215
+ frame_ph = st.empty()
216
+ info_ph = st.empty()
217
+ prog = st.progress(0.0, text="Preparing…")
218
+
219
+ # Reader thread -> queue
220
+ q: "queue.Queue[tuple[int, np.ndarray] | None]" = queue.Queue(maxsize=DEFAULT_QUEUE_SIZE)
221
+
222
+ def reader():
223
+ idx = 0
224
+ while True:
225
+ if st.session_state.get("stop_video", False):
226
+ break
227
+ ok, frm = cap.read()
228
+ if not ok:
229
+ break
230
+ if drop_if_behind and q.full():
231
+ try:
232
+ q.get_nowait()
233
+ except queue.Empty:
234
+ pass
235
+ try:
236
+ q.put((idx, frm), timeout=0.05)
237
+ except queue.Full:
238
+ pass
239
+ idx += 1
240
+ q.put(None)
241
+
242
+ reader_th = threading.Thread(target=reader, daemon=True)
243
+ reader_th.start()
244
+
245
+ # Writer (optional export)
246
+ tmp_out = Path(tempfile.gettempdir()) / f"out_{ts}{DEFAULT_TMP_EXT}"
247
+ writer = None
248
+
249
+ # Pacing and preview throttle
250
+ min_preview_interval = 1.0 / float(max_preview_fps)
251
+ last_preview_ts = 0.0
252
+ frame_interval = 1.0 / float(src_fps if src_fps > 0 else 25.0)
253
+ next_write_ts = time.perf_counter() + frame_interval
254
+
255
+ frames_done = 0
256
+ t0 = time.perf_counter()
257
+
258
+ try:
259
+ with st.spinner("Playing video…"):
260
+ while True:
261
+ if st.session_state.get("stop_video", False):
262
+ break
263
+
264
+ item = q.get()
265
+ if item is None:
266
+ break
267
+ idx, frame_bgr = item
268
+
269
+ # Downscale for speed/preview
270
+ vis_bgr = _resize_keep_aspect(frame_bgr, short_side=target_short_side)
271
+
272
+ # Init writer lazily
273
+ if writer is None:
274
+ H, W = vis_bgr.shape[:2]
275
+ fourcc = cv2.VideoWriter_fourcc(*DEFAULT_WRITER_CODEC)
276
+ writer = cv2.VideoWriter(str(tmp_out), fourcc, src_fps, (W, H))
277
+
278
+ # Pace writing to match source
279
+ now = time.perf_counter()
280
+ if now < next_write_ts:
281
+ time.sleep(max(0.0, next_write_ts - now))
282
+ writer.write(vis_bgr)
283
+ next_write_ts += frame_interval
284
+
285
+ frames_done += 1
286
+
287
+ # UI updates (throttled)
288
+ now = time.perf_counter()
289
+ if (now - last_preview_ts) >= min_preview_interval:
290
+ frame_ph.image(
291
+ cv2.cvtColor(vis_bgr, cv2.COLOR_BGR2RGB),
292
+ use_container_width=True,
293
+ output_format="JPEG",
294
+ channels="RGB",
295
+ )
296
+ elapsed = now - t0
297
+ fps_est = frames_done / max(elapsed, 1e-6)
298
+ info_ph.info(
299
+ f"Frames: {frames_done}/{total_frames or '?'} • "
300
+ f"Throughput: {fps_est:.1f} FPS • Source FPS: {src_fps:.1f} • "
301
+ f"Mode: Passthrough"
302
+ )
303
+ last_preview_ts = now
304
+
305
+ # Progress
306
+ progress = ((idx + 1) / total_frames) if total_frames > 0 else min(frames_done / (frames_done + 30), 0.99)
307
+ prog.progress(progress, text=f"Playing frame {idx + 1}{'/' + str(total_frames) if total_frames>0 else ''}…")
308
+
309
+ except Exception as exc:
310
+ st.error(f"Video playback failed: {exc}")
311
+ return
312
+ finally:
313
+ try:
314
+ cap.release()
315
+ if writer is not None:
316
+ writer.release()
317
+ except Exception:
318
+ pass
319
+
320
+ # Reset stop flag after finishing
321
+ st.session_state["stop_video"] = False
322
+
323
+ st.success("Done!")
324
+ if tmp_out.exists():
325
+ st.video(str(tmp_out))
326
+ with open(tmp_out, "rb") as f:
327
+ st.download_button(
328
+ "Download video",
329
+ data=f.read(),
330
+ file_name=tmp_out.name,
331
+ mime="video/mp4",
332
+ )
333
+ else:
334
+ st.error("Playback completed but output file was not created.")
335
+
336
+
337
+ # ================== Detection routines ==================
338
  def run_image_detection(uploaded_file, conf_thr: float = 0.5, model_key: str = "deim"):
339
  try:
340
  data = uploaded_file.getvalue()
 
346
 
347
  try:
348
  model = load_model(model_key)
349
+ device = _choose_device(model_key)
350
+ if torch is not None:
351
+ try:
352
+ model.to(device)
353
+ except Exception:
354
+ pass
355
+
356
+ _warmup_model(model, model_key=model_key, shape=(img.height, img.width, 3), conf=conf_thr)
357
+ use_amp = (device == "cuda") and not _should_force_cpu_for_model(model_key)
358
+
359
+ with st.spinner(f"Running detection on {device.upper()}…"):
360
+ with maybe_autocast(use_amp):
361
+ if (model_key or "").lower() == "deim":
362
+ annotated = model.predict_image(img, min_confidence=conf_thr)
363
+ else:
364
+ try:
365
+ annotated = model.predict_image(img, min_confidence=conf_thr)
366
+ except Exception:
367
+ np_bgr = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
368
+ _, vis = model.predict_and_visualize(np_bgr, min_confidence=conf_thr, show_score=True)
369
+ annotated = Image.fromarray(cv2.cvtColor(vis, cv2.COLOR_BGR2RGB))
370
 
371
  st.subheader("🎯 Detection Results")
372
+ st.image(annotated, caption="Detections", use_container_width=True)
373
+
374
+ if _should_force_cpu_for_model(model_key):
375
+ st.info("DEIM runs on CPU to avoid TorchScript device mismatch.")
376
+
377
  except Exception as e:
378
  st.error(f"Error during detection: {e}")
379
 
380
 
381
+ def run_video_detection(
382
+ vid_bytes: bytes,
383
+ conf_thr: float = 0.5,
384
+ model_key: str = "deim",
385
+ target_short_side: int = DEFAULT_TARGET_SHORT_SIDE,
386
+ max_preview_fps: int = DEFAULT_MAX_PREVIEW_FPS,
387
+ drop_if_behind: bool = DEFAULT_DROP_IF_BEHIND,
388
+ process_stride: int = DEFAULT_PROCESS_STRIDE,
389
+ ):
390
+ # Save upload to a temp file
391
+ ts = int(time.time() * 1000)
392
+ tmp_in = Path(tempfile.gettempdir()) / f"in_{ts}{DEFAULT_TMP_EXT}"
393
  with open(tmp_in, "wb") as f:
394
  f.write(vid_bytes)
395
 
396
+ # Load model & choose device
397
  model = load_model(model_key)
398
+ device = _choose_device(model_key)
399
+ if torch is not None:
400
+ try:
401
+ model.to(device)
402
+ except Exception:
403
+ pass
404
+
405
+ # Capture
406
+ cap = cv2.VideoCapture(str(tmp_in), cv2.CAP_FFMPEG)
407
  if not cap.isOpened():
408
  st.error("Failed to open the uploaded video.")
409
  return
410
 
411
+ try:
412
+ cap.set(cv2.CAP_PROP_BUFFERSIZE, 2)
413
+ except Exception:
414
+ pass
415
+
416
+ src_fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
417
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
418
+ src_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
419
+ src_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
420
 
 
421
  frame_ph = st.empty()
422
+ info_ph = st.empty()
423
+ prog = st.progress(0.0, text="Preparing…")
424
+
425
+ _warmup_model(model, model_key=model_key, shape=(min(src_h, src_w), max(src_h, src_w), 3), conf=conf_thr)
426
+
427
+ # Reader thread -> bounded queue
428
+ q: "queue.Queue[tuple[int, np.ndarray] | None]" = queue.Queue(maxsize=DEFAULT_QUEUE_SIZE)
429
+
430
+ def reader():
431
+ idx = 0
432
+ while True:
433
+ if st.session_state.get("stop_video", False):
434
+ break
435
+ ok, frm = cap.read()
436
+ if not ok:
437
+ break
438
+ if drop_if_behind and q.full():
439
+ # drop the oldest frame to keep things moving
440
+ try:
441
+ q.get_nowait()
442
+ except queue.Empty:
443
+ pass
444
+ try:
445
+ q.put((idx, frm), timeout=0.05)
446
+ except queue.Full:
447
+ pass
448
+ idx += 1
449
+ q.put(None)
450
+
451
+ reader_th = threading.Thread(target=reader, daemon=True)
452
+ reader_th.start()
453
+
454
+ tmp_out = Path(tempfile.gettempdir()) / f"out_{ts}{DEFAULT_TMP_EXT}"
455
+ writer = None
456
+
457
+ # Preview throttle
458
+ min_preview_interval = 1.0 / float(max_preview_fps)
459
+ last_preview_ts = 0.0
460
+
461
+ # Source pacing
462
+ frame_interval = 1.0 / float(src_fps if src_fps > 0 else 25.0)
463
+ next_write_ts = time.perf_counter() + frame_interval
464
+
465
+ frames_done = 0
466
+ t0 = time.perf_counter()
467
+ use_amp = (device == "cuda") and not _should_force_cpu_for_model(model_key)
468
+
469
+ last_vis_bgr = None # for stride reuse
470
 
471
  try:
472
+ with st.spinner(f"Processing video on {device.upper()} with live preview…"):
473
  while True:
474
+ if st.session_state.get("stop_video", False):
 
475
  break
476
 
477
+ item = q.get()
478
+ if item is None:
479
+ break
480
+ idx, frame_bgr = item
481
+
482
+ # Downscale for speed
483
+ proc_bgr = _resize_keep_aspect(frame_bgr, short_side=target_short_side)
484
+
485
+ run_infer = (process_stride <= 1) or ((idx % process_stride) == 0)
486
+
487
+ if run_infer:
488
+ # Run model
489
+ if (model_key or "").lower() == "deim":
490
+ img_rgb = cv2.cvtColor(proc_bgr, cv2.COLOR_BGR2RGB)
491
+ pil_img = Image.fromarray(img_rgb)
492
+ annotated_pil = model.predict_image(pil_img, min_confidence=conf_thr)
493
+ vis_bgr = cv2.cvtColor(np.array(annotated_pil), cv2.COLOR_RGB2BGR)
494
+ else:
495
+ with maybe_autocast(use_amp):
496
+ try:
497
+ _, vis_bgr = model.predict_and_visualize(
498
+ proc_bgr, min_confidence=conf_thr, show_score=True
499
+ )
500
+ except Exception:
501
+ pil = Image.fromarray(cv2.cvtColor(proc_bgr, cv2.COLOR_BGR2RGB))
502
+ annotated = model.predict_image(pil, min_confidence=conf_thr)
503
+ vis_bgr = cv2.cvtColor(np.array(annotated), cv2.COLOR_RGB2BGR)
504
+ last_vis_bgr = vis_bgr
505
  else:
506
+ # Reuse last visualised frame to avoid visible “skips”
507
+ vis_bgr = last_vis_bgr if last_vis_bgr is not None else proc_bgr
508
+
509
+ # Init writer when first output frame is ready
510
+ if writer is None:
511
+ H, W = vis_bgr.shape[:2]
512
+ fourcc = cv2.VideoWriter_fourcc(*DEFAULT_WRITER_CODEC) # avoids OpenH264 issues
513
+ out_fps = src_fps # preserve source FPS in output
514
+ writer = cv2.VideoWriter(str(tmp_out), fourcc, out_fps, (W, H))
515
+
516
+ # Pace writing to match the source timeline
517
+ now = time.perf_counter()
518
+ if now < next_write_ts:
519
+ time.sleep(max(0.0, next_write_ts - now))
520
+ writer.write(vis_bgr)
521
+ next_write_ts += frame_interval
522
+
523
+ frames_done += 1
524
+
525
+ # UI updates (throttled)
526
+ now = time.perf_counter()
527
+ if (now - last_preview_ts) >= min_preview_interval:
528
  frame_ph.image(
529
+ cv2.cvtColor(vis_bgr, cv2.COLOR_BGR2RGB),
530
  use_container_width=True,
531
  output_format="JPEG",
532
  channels="RGB",
533
  )
534
+ elapsed = now - t0
535
+ fps_est = frames_done / max(elapsed, 1e-6)
536
+ device_msg = f"{device.upper()}" if device != "cuda" else f"{device.upper()} ({_device_hint().upper()})"
537
+ info_text = (
538
+ f"Processed: {frames_done} / {total_frames if total_frames>0 else '?'} • "
539
+ f"Throughput: {fps_est:.1f} FPS • "
540
+ f"Source FPS: {src_fps:.1f} • Device: {device_msg} • "
541
+ f"Stride: {process_stride}x"
542
+ )
543
+ if _should_force_cpu_for_model(model_key):
544
+ info_text += " • Note: DEIM forced to CPU."
545
+ info_ph.info(info_text)
546
+ last_preview_ts = now
547
 
548
+ # Progress bar
549
+ progress = ((idx + 1) / total_frames) if total_frames > 0 else min(frames_done / (frames_done + 30), 0.99)
550
+ prog.progress(progress, text=f"Processing frame {idx + 1}{'/' + str(total_frames) if total_frames>0 else ''}…")
551
 
552
  except Exception as exc:
553
  st.error(f"Video detection failed: {exc}")
554
  return
555
  finally:
556
+ try:
557
+ cap.release()
558
+ if writer is not None:
559
+ writer.release()
560
+ except Exception:
561
+ pass
562
+
563
+ # Reset stop flag after finishing
564
+ st.session_state["stop_video"] = False
565
 
566
  st.success("Done!")
567
 
 
568
  if tmp_out.exists():
569
  st.video(str(tmp_out))
570
  with open(tmp_out, "rb") as f:
 
578
  st.error("Video processing completed but output file was not created.")
579
 
580
 
581
+ # ================== Main Actions ==================
582
  if run_img:
583
  if img_file is None:
584
  st.warning("Please upload an image first.")
585
  else:
586
  run_image_detection(img_file, conf_thr=conf_thr, model_key=model_key)
587
 
588
+ # New: Passthrough mode
589
+ if run_vid_plain:
590
+ if vid_file is None:
591
+ st.warning("Please upload a video first.")
592
+ else:
593
+ st.session_state["stop_video"] = False
594
+ run_video_passthrough(
595
+ vid_bytes=vid_file.read(),
596
+ target_short_side=target_short_side,
597
+ max_preview_fps=max_preview_fps,
598
+ drop_if_behind=drop_if_behind,
599
+ )
600
+
601
+ # Original: Detection mode
602
  if run_vid:
603
  if vid_file is None:
604
  st.warning("Please upload a video first.")
605
  else:
606
+ st.session_state["stop_video"] = False
607
  run_video_detection(
608
+ vid_bytes=vid_file.read(),
609
+ conf_thr=conf_thr,
610
+ model_key=model_key,
611
+ target_short_side=target_short_side,
612
+ max_preview_fps=max_preview_fps,
613
+ drop_if_behind=drop_if_behind,
614
+ process_stride=process_stride,
615
  )
services/app_service/pages/bushland_beacon_KY.py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # pages/bushland_beacon.py
2
+ import io
3
+ import tempfile
4
+ import time
5
+ from pathlib import Path
6
+
7
+ import cv2
8
+ import numpy as np
9
+ import streamlit as st
10
+ from PIL import Image
11
+ from utils.model_manager import get_model_manager, load_model
12
+
13
+ # =============== Page setup ===============
14
+ st.set_page_config(
15
+ page_title="Bushland Beacon ", layout="wide", initial_sidebar_state="expanded"
16
+ )
17
+
18
+ st.markdown(
19
+ "<h2 style='text-align:center;margin-top:0'>SAR-X<sup>ai</sup></h2>"
20
+ "<h2 style='text-align:center;margin-top:0'>Bushland Beacon 🚨 </h2>",
21
+ unsafe_allow_html=True,
22
+ )
23
+ # =============== Sidebar: custom menu + cards ===============
24
+ with st.sidebar:
25
+ st.page_link("app.py", label="Home")
26
+ st.page_link("pages/bushland_beacon.py", label="Bushland Beacon")
27
+ st.page_link("pages/lost_at_sea.py", label="Lost at Sea")
28
+ st.page_link("pages/signal_watch.py", label="Signal Watch")
29
+ st.markdown("---")
30
+ st.page_link("pages/task_satellite.py", label="Task Satellite")
31
+ st.page_link("pages/task_drone.py", label="Task Drone")
32
+
33
+ st.markdown("---")
34
+
35
+ # Simple "card" styling in the sidebar
36
+ st.markdown(
37
+ """
38
+ <style>
39
+ .sb-card {border:1px solid rgba(255,255,255,0.15); padding:14px; border-radius:8px; margin-bottom:16px;}
40
+ .sb-card h4 {margin:0 0 10px 0; font-weight:700;}
41
+ </style>
42
+ """,
43
+ unsafe_allow_html=True,
44
+ )
45
+
46
+ # Image Detection card
47
+ st.sidebar.header("Image Detection")
48
+ img_file = st.file_uploader(
49
+ "Upload an image", type=["jpg", "jpeg", "png"], key="img_up"
50
+ )
51
+ run_img = st.button("🔍 Run Image Detection", use_container_width=True)
52
+
53
+ # Video Detection card
54
+ st.sidebar.header("Video Detection")
55
+ vid_file = st.file_uploader(
56
+ "Upload a video", type=["mp4", "mov", "avi", "mkv"], key="vid_up"
57
+ )
58
+ run_vid = st.button("🎥 Run Video Detection", use_container_width=True)
59
+
60
+ st.sidebar.markdown("---")
61
+ # Parameters card (shared)
62
+ st.sidebar.header("Parameters")
63
+ conf_thr = st.slider("Minimum confidence threshold", 0.05, 0.95, 0.50, 0.01)
64
+
65
+ st.sidebar.markdown("---")
66
+
67
+ # Get model manager instance
68
+ model_manager = get_model_manager()
69
+
70
+ # Render model selection UI
71
+ model_label, model_key = model_manager.render_model_selection(
72
+ key_prefix="bushland_beacon"
73
+ )
74
+ st.sidebar.markdown("---")
75
+
76
+ # Render device information
77
+ model_manager.render_device_info()
78
+
79
+
80
+ # =============== Detection helpers ===============
81
+ def run_image_detection(uploaded_file, conf_thr: float = 0.5, model_key: str = "deim"):
82
+ try:
83
+ data = uploaded_file.getvalue()
84
+ img = Image.open(io.BytesIO(data)).convert("RGB")
85
+ st.image(img, caption="Uploaded Image", use_container_width=True)
86
+ except Exception as e:
87
+ st.error(f"Error loading image: {e}")
88
+ return
89
+
90
+ try:
91
+ model = load_model(model_key)
92
+ with st.spinner("Running detection..."):
93
+ annotated = model.predict_image(img, min_confidence=conf_thr)
94
+
95
+ st.subheader("🎯 Detection Results")
96
+ st.image(annotated, caption="Detections", width="stretch")
97
+ except Exception as e:
98
+ st.error(f"Error during detection: {e}")
99
+
100
+
101
+ def run_video_detection(vid_bytes, conf_thr: float = 0.5, model_key: str = "deim"):
102
+ tmp_in = Path(tempfile.gettempdir()) / f"in_{int(time.time())}.mp4"
103
+ with open(tmp_in, "wb") as f:
104
+ f.write(vid_bytes)
105
+
106
+ model = load_model(model_key)
107
+
108
+ # Set up video capture for preview
109
+ cap = cv2.VideoCapture(str(tmp_in))
110
+ if not cap.isOpened():
111
+ st.error("Failed to open the uploaded video.")
112
+ return
113
+
114
+ fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
115
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
116
+
117
+ # Set up preview and progress
118
+ frame_ph = st.empty()
119
+ prog = st.progress(0.0, text="Processing…")
120
+
121
+ # Set up video writer for output
122
+ W = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
123
+ H = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
124
+ tmp_out = Path(tempfile.gettempdir()) / f"out_{int(time.time())}.mp4"
125
+ writer = cv2.VideoWriter(str(tmp_out), cv2.VideoWriter_fourcc(*"mp4v"), fps, (W, H))
126
+
127
+ frame_count = 0
128
+ last_preview_update = 0
129
+ preview_update_interval = 1 # Update preview every 5 frames
130
+
131
+ try:
132
+ with st.spinner("Processing video with live preview…"):
133
+ while True:
134
+ ok, frame = cap.read()
135
+ if not ok:
136
+ break
137
+
138
+ # Process frame with model
139
+ if model_key == "deim":
140
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
141
+ annotated_pil = model.predict_image(
142
+ Image.fromarray(frame_rgb), min_confidence=conf_thr
143
+ )
144
+ vis = cv2.cvtColor(np.array(annotated_pil), cv2.COLOR_RGB2BGR)
145
+ else:
146
+ _, vis = model.predict_and_visualize(
147
+ frame, min_confidence=conf_thr, show_score=True
148
+ )
149
+
150
+ # Update progress
151
+ progress = frame_count / total_frames if total_frames > 0 else 0
152
+ prog.progress(
153
+ progress,
154
+ text=f"Processing frame {frame_count + 1}/{total_frames}...",
155
+ )
156
+
157
+ # Update preview (throttled to prevent freezing)
158
+ if (frame_count - last_preview_update) >= preview_update_interval:
159
+ frame_ph.image(
160
+ cv2.cvtColor(vis, cv2.COLOR_BGR2RGB),
161
+ use_container_width=True,
162
+ output_format="JPEG",
163
+ channels="RGB",
164
+ )
165
+ last_preview_update = frame_count
166
+
167
+ # Write frame to output video
168
+ writer.write(vis)
169
+ frame_count += 1
170
+
171
+ except Exception as exc:
172
+ st.error(f"Video detection failed: {exc}")
173
+ return
174
+ finally:
175
+ cap.release()
176
+ writer.release()
177
+
178
+ st.success("Done!")
179
+
180
+ # Check if output file exists before trying to display it
181
+ if tmp_out.exists():
182
+ st.video(str(tmp_out))
183
+ with open(tmp_out, "rb") as f:
184
+ st.download_button(
185
+ "Download processed video",
186
+ data=f.read(),
187
+ file_name=tmp_out.name,
188
+ mime="video/mp4",
189
+ )
190
+ else:
191
+ st.error("Video processing completed but output file was not created.")
192
+
193
+
194
+ # =============== Main: hook up actions ===============
195
+ if run_img:
196
+ if img_file is None:
197
+ st.warning("Please upload an image first.")
198
+ else:
199
+ run_image_detection(img_file, conf_thr=conf_thr, model_key=model_key)
200
+
201
+ if run_vid:
202
+ if vid_file is None:
203
+ st.warning("Please upload a video first.")
204
+ else:
205
+ run_video_detection(
206
+ vid_bytes=vid_file.read(), conf_thr=conf_thr, model_key=model_key
207
+ )
services/app_service/pages/lost_at_sea.py CHANGED
@@ -1,83 +1,340 @@
1
  # pages/lost_at_sea.py
 
2
  import io
3
- import tempfile
4
  import time
 
 
 
5
  from pathlib import Path
 
6
 
7
  import cv2
8
  import numpy as np
9
  import streamlit as st
10
  from PIL import Image
 
 
 
 
 
 
 
11
  from utils.model_manager import get_model_manager, load_model
12
 
13
- # =============== Page setup ===============
14
- st.set_page_config(
15
- page_title="Lost at Sea", layout="wide", initial_sidebar_state="expanded"
16
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  st.markdown(
19
- "<h2 style='text-align:center;margin-top:0'>SAR-X<sup>ai</h2>"
20
- "<h2 style='text-align:center;margin-top:0'>Lost at Sea 🌊</h2>",
21
  unsafe_allow_html=True,
22
  )
23
 
24
- # =============== Sidebar: custom menu + cards ===============
25
  with st.sidebar:
26
  st.page_link("app.py", label="Home")
27
  st.page_link("pages/bushland_beacon.py", label="Bushland Beacon")
28
- st.page_link("pages/lost_at_sea.py", label="Lost at Sea")
29
  st.page_link("pages/signal_watch.py", label="Signal Watch")
30
  st.markdown("---")
31
  st.page_link("pages/task_satellite.py", label="Task Satellite")
32
  st.page_link("pages/task_drone.py", label="Task Drone")
 
33
 
34
- # Simple "card" styling in the sidebar
35
- st.markdown(
36
- """
37
- <style>
38
- .sb-card {border:1px solid rgba(255,255,255,0.15); padding:14px; border-radius:8px; margin-bottom:16px;}
39
- .sb-card h4 {margin:0 0 10px 0; font-weight:700;}
40
- </style>
41
- """,
42
- unsafe_allow_html=True,
43
- )
44
-
45
- # Image Detection card
46
  st.sidebar.header("Image Detection")
47
- img_file = st.file_uploader(
48
- "Upload an image", type=["jpg", "jpeg", "png"], key="img_up"
49
- )
50
  run_img = st.button("🔍 Run Image Detection", use_container_width=True)
51
 
52
- # Video Detection card
53
- st.sidebar.header("Video Detection")
54
- vid_file = st.file_uploader(
55
- "Upload a video", type=["mp4", "mov", "avi", "mkv"], key="vid_up"
56
- )
57
- run_vid = st.button("🎥 Run Video Detection", use_container_width=True)
 
 
 
 
58
 
59
  st.sidebar.markdown("---")
60
- # Parameters card (shared)
61
  st.sidebar.header("Parameters")
62
- conf_thr = st.slider("Minimum confidence threshold", 0.05, 0.95, 0.50, 0.01)
63
 
64
- st.sidebar.markdown("---")
65
 
66
- # Get model manager instance
67
- model_manager = get_model_manager()
 
 
 
 
68
 
69
- # Render model selection UI
70
- model_label, model_key = model_manager.render_model_selection(
71
- key_prefix="lost_at_sea"
 
 
 
 
 
 
 
 
 
72
  )
73
- st.sidebar.markdown("---")
74
 
75
- # Render device information
 
 
 
 
 
 
 
 
 
 
 
76
  model_manager.render_device_info()
77
 
78
 
79
- # =============== Detection helpers ===============
80
- def run_image_detection(uploaded_file, conf_thr: float = 0.5):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  try:
82
  data = uploaded_file.getvalue()
83
  img = Image.open(io.BytesIO(data)).convert("RGB")
@@ -88,95 +345,225 @@ def run_image_detection(uploaded_file, conf_thr: float = 0.5):
88
 
89
  try:
90
  model = load_model(model_key)
91
- with st.spinner("Running detection..."):
92
- annotated = model.predict_image(img, min_confidence=conf_thr)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
  st.subheader("🎯 Detection Results")
95
- st.image(annotated, caption="Detections", width="stretch")
 
 
 
 
96
  except Exception as e:
97
  st.error(f"Error during detection: {e}")
98
 
99
 
100
- def run_video_detection(vid_bytes, conf_thr: float = 0.5, model_key: str = "deim"):
101
- tmp_in = Path(tempfile.gettempdir()) / f"in_{int(time.time())}.mp4"
 
 
 
 
 
 
 
 
 
 
102
  with open(tmp_in, "wb") as f:
103
  f.write(vid_bytes)
104
 
 
105
  model = load_model(model_key)
106
-
107
- # Set up video capture for preview
108
- cap = cv2.VideoCapture(str(tmp_in))
 
 
 
 
 
 
109
  if not cap.isOpened():
110
  st.error("Failed to open the uploaded video.")
111
  return
112
 
113
- fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
 
 
 
 
 
114
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
 
 
115
 
116
- # Set up preview and progress
117
  frame_ph = st.empty()
118
- prog = st.progress(0.0, text="Processing…")
119
-
120
- # Set up video writer for output
121
- W = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
122
- H = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
123
- tmp_out = Path(tempfile.gettempdir()) / f"out_{int(time.time())}.mp4"
124
- writer = cv2.VideoWriter(str(tmp_out), cv2.VideoWriter_fourcc(*"mp4v"), fps, (W, H))
125
-
126
- frame_count = 0
127
- last_preview_update = 0
128
- preview_update_interval = 5 # Update preview every 5 frames
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
  try:
131
- with st.spinner("Processing video with live preview…"):
132
  while True:
133
- ok, frame = cap.read()
134
- if not ok:
135
  break
136
 
137
- # Process frame with model
138
- if model_key == "deim":
139
- frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
140
- annotated_pil = model.predict_image(
141
- Image.fromarray(frame_rgb), min_confidence=conf_thr
142
- )
143
- vis = cv2.cvtColor(np.array(annotated_pil), cv2.COLOR_RGB2BGR)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  else:
145
- _, vis = model.predict_and_visualize(
146
- frame, min_confidence=conf_thr, show_score=True
147
- )
148
-
149
- # Update progress
150
- progress = frame_count / total_frames if total_frames > 0 else 0
151
- prog.progress(
152
- progress,
153
- text=f"Processing frame {frame_count + 1}/{total_frames}...",
154
- )
155
-
156
- # Update preview (throttled to prevent freezing)
157
- if (frame_count - last_preview_update) >= preview_update_interval:
 
 
 
 
 
 
 
 
 
158
  frame_ph.image(
159
- cv2.cvtColor(vis, cv2.COLOR_BGR2RGB),
160
  use_container_width=True,
161
  output_format="JPEG",
162
  channels="RGB",
163
  )
164
- last_preview_update = frame_count
 
 
 
 
 
 
 
 
 
 
 
 
165
 
166
- # Write frame to output video
167
- writer.write(vis)
168
- frame_count += 1
169
 
170
  except Exception as exc:
171
  st.error(f"Video detection failed: {exc}")
172
  return
173
  finally:
174
- cap.release()
175
- writer.release()
 
 
 
 
 
 
 
176
 
177
  st.success("Done!")
178
 
179
- # Check if output file exists before trying to display it
180
  if tmp_out.exists():
181
  st.video(str(tmp_out))
182
  with open(tmp_out, "rb") as f:
@@ -190,17 +577,38 @@ def run_video_detection(vid_bytes, conf_thr: float = 0.5, model_key: str = "deim
190
  st.error("Video processing completed but output file was not created.")
191
 
192
 
193
- # =============== Main: hook up actions ===============
194
  if run_img:
195
  if img_file is None:
196
  st.warning("Please upload an image first.")
197
  else:
198
- run_image_detection(img_file, conf_thr=conf_thr)
 
 
 
 
 
 
 
 
 
 
 
 
 
199
 
 
200
  if run_vid:
201
  if vid_file is None:
202
  st.warning("Please upload a video first.")
203
  else:
 
204
  run_video_detection(
205
- vid_bytes=vid_file.read(), conf_thr=conf_thr, model_key=model_key
 
 
 
 
 
 
206
  )
 
1
  # pages/lost_at_sea.py
2
+
3
  import io
 
4
  import time
5
+ import queue
6
+ import threading
7
+ import tempfile
8
  from pathlib import Path
9
+ from contextlib import contextmanager
10
 
11
  import cv2
12
  import numpy as np
13
  import streamlit as st
14
  from PIL import Image
15
+
16
+ # Torch (optional)
17
+ try:
18
+ import torch
19
+ except Exception:
20
+ torch = None
21
+
22
  from utils.model_manager import get_model_manager, load_model
23
 
24
+
25
+ # ================== CONFIG ==================
26
+ # --- User-tunable parameters ---
27
+ DEFAULT_CONF_THRESHOLD = 0.30 # Detection confidence
28
+ DEFAULT_TARGET_SHORT_SIDE = 960 # Resize short edge (px)
29
+ DEFAULT_MAX_PREVIEW_FPS = 30 # Limit UI update frequency
30
+ DEFAULT_DROP_IF_BEHIND = False # Drop frames if lagging
31
+ DEFAULT_PROCESS_STRIDE = 1 # Process every Nth frame (1=all)
32
+ DEFAULT_QUEUE_SIZE = 24 # Frame queue length
33
+ DEFAULT_WRITER_CODEC = "mp4v" # Codec to avoid OpenH264 issue
34
+ DEFAULT_TMP_EXT = ".mp4" # Temp file extension
35
+ DEFAULT_MAX_SLIDER_SHORT_SIDE = 1080 # Max short side slider
36
+ DEFAULT_MIN_SLIDER_SHORT_SIDE = 256 # Min short side slider
37
+ DEFAULT_MIN_FPS_SLIDER = 1 # Min preview FPS slider
38
+ DEFAULT_MAX_FPS_SLIDER = 30 # Max preview FPS slider
39
+ # ============================================
40
+
41
+
42
+ # ============== Session state (stop flag) ==============
43
+ if "stop_video" not in st.session_state:
44
+ st.session_state["stop_video"] = False
45
+
46
+
47
+ # ================== Page setup ==================
48
+ st.set_page_config(page_title="Lost At Sea", layout="wide", initial_sidebar_state="expanded")
49
 
50
  st.markdown(
51
+ "<h2 style='text-align:center;margin-top:0'>SAR-X<sup>ai</sup></h2>"
52
+ "<h2 style='text-align:center;margin-top:0'>Lost At Sea 🌊</h2>",
53
  unsafe_allow_html=True,
54
  )
55
 
56
+ # ================== Sidebar ==================
57
  with st.sidebar:
58
  st.page_link("app.py", label="Home")
59
  st.page_link("pages/bushland_beacon.py", label="Bushland Beacon")
60
+ st.page_link("pages/lost_at_sea.py", label="Lost At Sea")
61
  st.page_link("pages/signal_watch.py", label="Signal Watch")
62
  st.markdown("---")
63
  st.page_link("pages/task_satellite.py", label="Task Satellite")
64
  st.page_link("pages/task_drone.py", label="Task Drone")
65
+ st.markdown("---")
66
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  st.sidebar.header("Image Detection")
68
+ img_file = st.file_uploader("Upload an image", type=["jpg", "jpeg", "png"], key="img_up")
 
 
69
  run_img = st.button("🔍 Run Image Detection", use_container_width=True)
70
 
71
+ st.sidebar.header("Video")
72
+ vid_file = st.file_uploader("Upload a video", type=["mp4", "mov", "avi", "mkv"], key="vid_up")
73
+
74
+ # New buttons
75
+ run_vid_plain = st.button("Play Video", use_container_width=True)
76
+ run_vid = st.button("🎥 Run Detection", use_container_width=True)
77
+ stop_vid = st.button("Stop", use_container_width=True)
78
+
79
+ if stop_vid:
80
+ st.session_state["stop_video"] = True
81
 
82
  st.sidebar.markdown("---")
 
83
  st.sidebar.header("Parameters")
 
84
 
85
+ conf_thr = st.slider("Minimum confidence threshold", 0.05, 0.95, DEFAULT_CONF_THRESHOLD, 0.01)
86
 
87
+ target_short_side = st.select_slider(
88
+ "Target short-side (downscale)",
89
+ options=[256, 320, 384, 448, 512, 640, 720, 800, 864, 960, 1080],
90
+ value=DEFAULT_TARGET_SHORT_SIDE,
91
+ help="Resize so the shorter edge equals this value. Smaller = faster."
92
+ )
93
 
94
+ max_preview_fps = st.slider(
95
+ "Max preview FPS",
96
+ min_value=DEFAULT_MIN_FPS_SLIDER,
97
+ max_value=DEFAULT_MAX_FPS_SLIDER,
98
+ value=DEFAULT_MAX_PREVIEW_FPS,
99
+ help="Throttles UI updates for smoother preview."
100
+ )
101
+
102
+ drop_if_behind = st.toggle(
103
+ "Drop frames if behind",
104
+ value=DEFAULT_DROP_IF_BEHIND,
105
+ help="Drop frames to maintain smooth preview."
106
  )
 
107
 
108
+ process_stride = st.slider(
109
+ "Process every Nth frame",
110
+ min_value=1,
111
+ max_value=5,
112
+ value=DEFAULT_PROCESS_STRIDE,
113
+ help="1 = every frame; higher values reuse last result."
114
+ )
115
+
116
+ st.sidebar.markdown("---")
117
+ model_manager = get_model_manager()
118
+ model_label, model_key = model_manager.render_model_selection(key_prefix="lost_at_sea")
119
+ st.sidebar.markdown("---")
120
  model_manager.render_device_info()
121
 
122
 
123
+ # ================== Perf knobs for OpenCV ==================
124
+ try:
125
+ cv2.setNumThreads(1)
126
+ except Exception:
127
+ pass
128
+ try:
129
+ cv2.ocl.setUseOpenCL(False)
130
+ except Exception:
131
+ pass
132
+
133
+
134
+ # ================== Helper functions ==================
135
+ def _resize_keep_aspect(img_bgr: np.ndarray, short_side: int) -> np.ndarray:
136
+ h, w = img_bgr.shape[:2]
137
+ if min(h, w) == short_side:
138
+ return img_bgr
139
+ if h < w:
140
+ new_h = short_side
141
+ new_w = int(round(w * (short_side / h)))
142
+ else:
143
+ new_w = short_side
144
+ new_h = int(round(h * (short_side / w)))
145
+ return cv2.resize(img_bgr, (new_w, new_h), interpolation=cv2.INTER_AREA)
146
+
147
+
148
+ def _should_force_cpu_for_model(model_key: str) -> bool:
149
+ return (model_key or "").lower() == "deim"
150
+
151
+
152
+ def _choose_device(model_key: str) -> str:
153
+ if _should_force_cpu_for_model(model_key):
154
+ return "cpu"
155
+ if torch is not None and torch.cuda.is_available():
156
+ return "cuda"
157
+ return "cpu"
158
+
159
+
160
+ def _warmup_model(model, model_key: str, shape=(720, 1280, 3), conf: float = 0.25):
161
+ dummy = np.zeros(shape, dtype=np.uint8)
162
+ try:
163
+ if (model_key or "").lower() == "deim":
164
+ pil = Image.fromarray(cv2.cvtColor(dummy, cv2.COLOR_BGR2RGB))
165
+ model.predict_image(pil, min_confidence=conf)
166
+ else:
167
+ model.predict_and_visualize(dummy, min_confidence=conf, show_score=False)
168
+ except Exception:
169
+ pass
170
+
171
+
172
+ @contextmanager
173
+ def maybe_autocast(enabled: bool):
174
+ if enabled and torch is not None and torch.cuda.is_available():
175
+ with torch.cuda.amp.autocast():
176
+ yield
177
+ else:
178
+ yield
179
+
180
+
181
+ def _device_hint() -> str:
182
+ if torch is None:
183
+ return "cpu"
184
+ return "cuda" if torch.cuda.is_available() else "cpu"
185
+
186
+
187
+ # ================== Passthrough (no model, no boxes) ==================
188
+ def run_video_passthrough(
189
+ vid_bytes: bytes,
190
+ target_short_side: int = DEFAULT_TARGET_SHORT_SIDE,
191
+ max_preview_fps: int = DEFAULT_MAX_PREVIEW_FPS,
192
+ drop_if_behind: bool = DEFAULT_DROP_IF_BEHIND,
193
+ ):
194
+ """Play the uploaded video with scaling & pacing only (no inference, no overlays)."""
195
+ ts = int(time.time() * 1000)
196
+ tmp_in = Path(tempfile.gettempdir()) / f"in_{ts}{DEFAULT_TMP_EXT}"
197
+ with open(tmp_in, "wb") as f:
198
+ f.write(vid_bytes)
199
+
200
+ cap = cv2.VideoCapture(str(tmp_in), cv2.CAP_FFMPEG)
201
+ if not cap.isOpened():
202
+ st.error("Failed to open the uploaded video.")
203
+ return
204
+
205
+ try:
206
+ cap.set(cv2.CAP_PROP_BUFFERSIZE, 2)
207
+ except Exception:
208
+ pass
209
+
210
+ src_fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
211
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
212
+
213
+ # UI placeholders
214
+ frame_ph = st.empty()
215
+ info_ph = st.empty()
216
+ prog = st.progress(0.0, text="Preparing…")
217
+
218
+ # Reader thread -> queue
219
+ q: "queue.Queue[tuple[int, np.ndarray] | None]" = queue.Queue(maxsize=DEFAULT_QUEUE_SIZE)
220
+
221
+ def reader():
222
+ idx = 0
223
+ while True:
224
+ if st.session_state.get("stop_video", False):
225
+ break
226
+ ok, frm = cap.read()
227
+ if not ok:
228
+ break
229
+ if drop_if_behind and q.full():
230
+ try:
231
+ q.get_nowait()
232
+ except queue.Empty:
233
+ pass
234
+ try:
235
+ q.put((idx, frm), timeout=0.05)
236
+ except queue.Full:
237
+ pass
238
+ idx += 1
239
+ q.put(None)
240
+
241
+ reader_th = threading.Thread(target=reader, daemon=True)
242
+ reader_th.start()
243
+
244
+ # Writer (optional export)
245
+ tmp_out = Path(tempfile.gettempdir()) / f"out_{ts}{DEFAULT_TMP_EXT}"
246
+ writer = None
247
+
248
+ # Pacing and preview throttle
249
+ min_preview_interval = 1.0 / float(max_preview_fps)
250
+ last_preview_ts = 0.0
251
+ frame_interval = 1.0 / float(src_fps if src_fps > 0 else 25.0)
252
+ next_write_ts = time.perf_counter() + frame_interval
253
+
254
+ frames_done = 0
255
+ t0 = time.perf_counter()
256
+
257
+ try:
258
+ with st.spinner("Playing video…"):
259
+ while True:
260
+ if st.session_state.get("stop_video", False):
261
+ break
262
+
263
+ item = q.get()
264
+ if item is None:
265
+ break
266
+ idx, frame_bgr = item
267
+
268
+ # Downscale for speed/preview
269
+ vis_bgr = _resize_keep_aspect(frame_bgr, short_side=target_short_side)
270
+
271
+ # Init writer lazily
272
+ if writer is None:
273
+ H, W = vis_bgr.shape[:2]
274
+ fourcc = cv2.VideoWriter_fourcc(*DEFAULT_WRITER_CODEC)
275
+ writer = cv2.VideoWriter(str(tmp_out), fourcc, src_fps, (W, H))
276
+
277
+ # Pace writing to match source
278
+ now = time.perf_counter()
279
+ if now < next_write_ts:
280
+ time.sleep(max(0.0, next_write_ts - now))
281
+ writer.write(vis_bgr)
282
+ next_write_ts += frame_interval
283
+
284
+ frames_done += 1
285
+
286
+ # UI updates (throttled)
287
+ now = time.perf_counter()
288
+ if (now - last_preview_ts) >= min_preview_interval:
289
+ frame_ph.image(
290
+ cv2.cvtColor(vis_bgr, cv2.COLOR_BGR2RGB),
291
+ use_container_width=True,
292
+ output_format="JPEG",
293
+ channels="RGB",
294
+ )
295
+ elapsed = now - t0
296
+ fps_est = frames_done / max(elapsed, 1e-6)
297
+ info_ph.info(
298
+ f"Frames: {frames_done}/{total_frames or '?'} • "
299
+ f"Throughput: {fps_est:.1f} FPS • Source FPS: {src_fps:.1f} • "
300
+ f"Mode: Passthrough"
301
+ )
302
+ last_preview_ts = now
303
+
304
+ # Progress
305
+ progress = ((idx + 1) / total_frames) if total_frames > 0 else min(frames_done / (frames_done + 30), 0.99)
306
+ prog.progress(progress, text=f"Playing frame {idx + 1}{'/' + str(total_frames) if total_frames>0 else ''}…")
307
+
308
+ except Exception as exc:
309
+ st.error(f"Video playback failed: {exc}")
310
+ return
311
+ finally:
312
+ try:
313
+ cap.release()
314
+ if writer is not None:
315
+ writer.release()
316
+ except Exception:
317
+ pass
318
+
319
+ # Reset stop flag after finishing
320
+ st.session_state["stop_video"] = False
321
+
322
+ st.success("Done!")
323
+ if tmp_out.exists():
324
+ st.video(str(tmp_out))
325
+ with open(tmp_out, "rb") as f:
326
+ st.download_button(
327
+ "Download video",
328
+ data=f.read(),
329
+ file_name=tmp_out.name,
330
+ mime="video/mp4",
331
+ )
332
+ else:
333
+ st.error("Playback completed but output file was not created.")
334
+
335
+
336
+ # ================== Detection routines ==================
337
+ def run_image_detection(uploaded_file, conf_thr: float = 0.5, model_key: str = "deim"):
338
  try:
339
  data = uploaded_file.getvalue()
340
  img = Image.open(io.BytesIO(data)).convert("RGB")
 
345
 
346
  try:
347
  model = load_model(model_key)
348
+ device = _choose_device(model_key)
349
+ if torch is not None:
350
+ try:
351
+ model.to(device)
352
+ except Exception:
353
+ pass
354
+
355
+ _warmup_model(model, model_key=model_key, shape=(img.height, img.width, 3), conf=conf_thr)
356
+ use_amp = (device == "cuda") and not _should_force_cpu_for_model(model_key)
357
+
358
+ with st.spinner(f"Running detection on {device.upper()}…"):
359
+ with maybe_autocast(use_amp):
360
+ if (model_key or "").lower() == "deim":
361
+ annotated = model.predict_image(img, min_confidence=conf_thr)
362
+ else:
363
+ try:
364
+ annotated = model.predict_image(img, min_confidence=conf_thr)
365
+ except Exception:
366
+ np_bgr = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
367
+ _, vis = model.predict_and_visualize(np_bgr, min_confidence=conf_thr, show_score=True)
368
+ annotated = Image.fromarray(cv2.cvtColor(vis, cv2.COLOR_BGR2RGB))
369
 
370
  st.subheader("🎯 Detection Results")
371
+ st.image(annotated, caption="Detections", use_container_width=True)
372
+
373
+ if _should_force_cpu_for_model(model_key):
374
+ st.info("DEIM runs on CPU to avoid TorchScript device mismatch.")
375
+
376
  except Exception as e:
377
  st.error(f"Error during detection: {e}")
378
 
379
 
380
+ def run_video_detection(
381
+ vid_bytes: bytes,
382
+ conf_thr: float = 0.5,
383
+ model_key: str = "deim",
384
+ target_short_side: int = DEFAULT_TARGET_SHORT_SIDE,
385
+ max_preview_fps: int = DEFAULT_MAX_PREVIEW_FPS,
386
+ drop_if_behind: bool = DEFAULT_DROP_IF_BEHIND,
387
+ process_stride: int = DEFAULT_PROCESS_STRIDE,
388
+ ):
389
+ # Save upload to a temp file
390
+ ts = int(time.time() * 1000)
391
+ tmp_in = Path(tempfile.gettempdir()) / f"in_{ts}{DEFAULT_TMP_EXT}"
392
  with open(tmp_in, "wb") as f:
393
  f.write(vid_bytes)
394
 
395
+ # Load model & choose device
396
  model = load_model(model_key)
397
+ device = _choose_device(model_key)
398
+ if torch is not None:
399
+ try:
400
+ model.to(device)
401
+ except Exception:
402
+ pass
403
+
404
+ # Capture
405
+ cap = cv2.VideoCapture(str(tmp_in), cv2.CAP_FFMPEG)
406
  if not cap.isOpened():
407
  st.error("Failed to open the uploaded video.")
408
  return
409
 
410
+ try:
411
+ cap.set(cv2.CAP_PROP_BUFFERSIZE, 2)
412
+ except Exception:
413
+ pass
414
+
415
+ src_fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
416
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
417
+ src_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
418
+ src_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
419
 
 
420
  frame_ph = st.empty()
421
+ info_ph = st.empty()
422
+ prog = st.progress(0.0, text="Preparing…")
423
+
424
+ _warmup_model(model, model_key=model_key, shape=(min(src_h, src_w), max(src_h, src_w), 3), conf=conf_thr)
425
+
426
+ # Reader thread -> bounded queue
427
+ q: "queue.Queue[tuple[int, np.ndarray] | None]" = queue.Queue(maxsize=DEFAULT_QUEUE_SIZE)
428
+
429
+ def reader():
430
+ idx = 0
431
+ while True:
432
+ if st.session_state.get("stop_video", False):
433
+ break
434
+ ok, frm = cap.read()
435
+ if not ok:
436
+ break
437
+ if drop_if_behind and q.full():
438
+ # drop the oldest frame to keep things moving
439
+ try:
440
+ q.get_nowait()
441
+ except queue.Empty:
442
+ pass
443
+ try:
444
+ q.put((idx, frm), timeout=0.05)
445
+ except queue.Full:
446
+ pass
447
+ idx += 1
448
+ q.put(None)
449
+
450
+ reader_th = threading.Thread(target=reader, daemon=True)
451
+ reader_th.start()
452
+
453
+ tmp_out = Path(tempfile.gettempdir()) / f"out_{ts}{DEFAULT_TMP_EXT}"
454
+ writer = None
455
+
456
+ # Preview throttle
457
+ min_preview_interval = 1.0 / float(max_preview_fps)
458
+ last_preview_ts = 0.0
459
+
460
+ # Source pacing
461
+ frame_interval = 1.0 / float(src_fps if src_fps > 0 else 25.0)
462
+ next_write_ts = time.perf_counter() + frame_interval
463
+
464
+ frames_done = 0
465
+ t0 = time.perf_counter()
466
+ use_amp = (device == "cuda") and not _should_force_cpu_for_model(model_key)
467
+
468
+ last_vis_bgr = None # for stride reuse
469
 
470
  try:
471
+ with st.spinner(f"Processing video on {device.upper()} with live preview…"):
472
  while True:
473
+ if st.session_state.get("stop_video", False):
 
474
  break
475
 
476
+ item = q.get()
477
+ if item is None:
478
+ break
479
+ idx, frame_bgr = item
480
+
481
+ # Downscale for speed
482
+ proc_bgr = _resize_keep_aspect(frame_bgr, short_side=target_short_side)
483
+
484
+ run_infer = (process_stride <= 1) or ((idx % process_stride) == 0)
485
+
486
+ if run_infer:
487
+ # Run model
488
+ if (model_key or "").lower() == "deim":
489
+ img_rgb = cv2.cvtColor(proc_bgr, cv2.COLOR_BGR2RGB)
490
+ pil_img = Image.fromarray(img_rgb)
491
+ annotated_pil = model.predict_image(pil_img, min_confidence=conf_thr)
492
+ vis_bgr = cv2.cvtColor(np.array(annotated_pil), cv2.COLOR_RGB2BGR)
493
+ else:
494
+ with maybe_autocast(use_amp):
495
+ try:
496
+ _, vis_bgr = model.predict_and_visualize(
497
+ proc_bgr, min_confidence=conf_thr, show_score=True
498
+ )
499
+ except Exception:
500
+ pil = Image.fromarray(cv2.cvtColor(proc_bgr, cv2.COLOR_BGR2RGB))
501
+ annotated = model.predict_image(pil, min_confidence=conf_thr)
502
+ vis_bgr = cv2.cvtColor(np.array(annotated), cv2.COLOR_RGB2BGR)
503
+ last_vis_bgr = vis_bgr
504
  else:
505
+ # Reuse last visualised frame to avoid visible “skips”
506
+ vis_bgr = last_vis_bgr if last_vis_bgr is not None else proc_bgr
507
+
508
+ # Init writer when first output frame is ready
509
+ if writer is None:
510
+ H, W = vis_bgr.shape[:2]
511
+ fourcc = cv2.VideoWriter_fourcc(*DEFAULT_WRITER_CODEC) # avoids OpenH264 issues
512
+ out_fps = src_fps # preserve source FPS in output
513
+ writer = cv2.VideoWriter(str(tmp_out), fourcc, out_fps, (W, H))
514
+
515
+ # Pace writing to match the source timeline
516
+ now = time.perf_counter()
517
+ if now < next_write_ts:
518
+ time.sleep(max(0.0, next_write_ts - now))
519
+ writer.write(vis_bgr)
520
+ next_write_ts += frame_interval
521
+
522
+ frames_done += 1
523
+
524
+ # UI updates (throttled)
525
+ now = time.perf_counter()
526
+ if (now - last_preview_ts) >= min_preview_interval:
527
  frame_ph.image(
528
+ cv2.cvtColor(vis_bgr, cv2.COLOR_BGR2RGB),
529
  use_container_width=True,
530
  output_format="JPEG",
531
  channels="RGB",
532
  )
533
+ elapsed = now - t0
534
+ fps_est = frames_done / max(elapsed, 1e-6)
535
+ device_msg = f"{device.upper()}" if device != "cuda" else f"{device.upper()} ({_device_hint().upper()})"
536
+ info_text = (
537
+ f"Processed: {frames_done} / {total_frames if total_frames>0 else '?'} • "
538
+ f"Throughput: {fps_est:.1f} FPS • "
539
+ f"Source FPS: {src_fps:.1f} • Device: {device_msg} • "
540
+ f"Stride: {process_stride}x"
541
+ )
542
+ if _should_force_cpu_for_model(model_key):
543
+ info_text += " • Note: DEIM forced to CPU."
544
+ info_ph.info(info_text)
545
+ last_preview_ts = now
546
 
547
+ # Progress bar
548
+ progress = ((idx + 1) / total_frames) if total_frames > 0 else min(frames_done / (frames_done + 30), 0.99)
549
+ prog.progress(progress, text=f"Processing frame {idx + 1}{'/' + str(total_frames) if total_frames>0 else ''}…")
550
 
551
  except Exception as exc:
552
  st.error(f"Video detection failed: {exc}")
553
  return
554
  finally:
555
+ try:
556
+ cap.release()
557
+ if writer is not None:
558
+ writer.release()
559
+ except Exception:
560
+ pass
561
+
562
+ # Reset stop flag after finishing
563
+ st.session_state["stop_video"] = False
564
 
565
  st.success("Done!")
566
 
 
567
  if tmp_out.exists():
568
  st.video(str(tmp_out))
569
  with open(tmp_out, "rb") as f:
 
577
  st.error("Video processing completed but output file was not created.")
578
 
579
 
580
+ # ================== Main Actions ==================
581
  if run_img:
582
  if img_file is None:
583
  st.warning("Please upload an image first.")
584
  else:
585
+ run_image_detection(img_file, conf_thr=conf_thr, model_key=model_key)
586
+
587
+ # New: Passthrough mode
588
+ if run_vid_plain:
589
+ if vid_file is None:
590
+ st.warning("Please upload a video first.")
591
+ else:
592
+ st.session_state["stop_video"] = False
593
+ run_video_passthrough(
594
+ vid_bytes=vid_file.read(),
595
+ target_short_side=target_short_side,
596
+ max_preview_fps=max_preview_fps,
597
+ drop_if_behind=drop_if_behind,
598
+ )
599
 
600
+ # Original: Detection mode
601
  if run_vid:
602
  if vid_file is None:
603
  st.warning("Please upload a video first.")
604
  else:
605
+ st.session_state["stop_video"] = False
606
  run_video_detection(
607
+ vid_bytes=vid_file.read(),
608
+ conf_thr=conf_thr,
609
+ model_key=model_key,
610
+ target_short_side=target_short_side,
611
+ max_preview_fps=max_preview_fps,
612
+ drop_if_behind=drop_if_behind,
613
+ process_stride=process_stride,
614
  )
services/app_service/pages/lost_at_sea_KY.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # pages/lost_at_sea.py
2
+ import io
3
+ import tempfile
4
+ import time
5
+ from pathlib import Path
6
+
7
+ import cv2
8
+ import numpy as np
9
+ import streamlit as st
10
+ from PIL import Image
11
+ from utils.model_manager import get_model_manager, load_model
12
+
13
+ # =============== Page setup ===============
14
+ st.set_page_config(
15
+ page_title="Lost at Sea", layout="wide", initial_sidebar_state="expanded"
16
+ )
17
+
18
+ st.markdown(
19
+ "<h2 style='text-align:center;margin-top:0'>SAR-X<sup>ai</h2>"
20
+ "<h2 style='text-align:center;margin-top:0'>Lost at Sea 🌊</h2>",
21
+ unsafe_allow_html=True,
22
+ )
23
+
24
+ # =============== Sidebar: custom menu + cards ===============
25
+ with st.sidebar:
26
+ st.page_link("app.py", label="Home")
27
+ st.page_link("pages/bushland_beacon.py", label="Bushland Beacon")
28
+ st.page_link("pages/lost_at_sea.py", label="Lost at Sea")
29
+ st.page_link("pages/signal_watch.py", label="Signal Watch")
30
+ st.markdown("---")
31
+ st.page_link("pages/task_satellite.py", label="Task Satellite")
32
+ st.page_link("pages/task_drone.py", label="Task Drone")
33
+
34
+ # Simple "card" styling in the sidebar
35
+ st.markdown(
36
+ """
37
+ <style>
38
+ .sb-card {border:1px solid rgba(255,255,255,0.15); padding:14px; border-radius:8px; margin-bottom:16px;}
39
+ .sb-card h4 {margin:0 0 10px 0; font-weight:700;}
40
+ </style>
41
+ """,
42
+ unsafe_allow_html=True,
43
+ )
44
+
45
+ # Image Detection card
46
+ st.sidebar.header("Image Detection")
47
+ img_file = st.file_uploader(
48
+ "Upload an image", type=["jpg", "jpeg", "png"], key="img_up"
49
+ )
50
+ run_img = st.button("🔍 Run Image Detection", use_container_width=True)
51
+
52
+ # Video Detection card
53
+ st.sidebar.header("Video Detection")
54
+ vid_file = st.file_uploader(
55
+ "Upload a video", type=["mp4", "mov", "avi", "mkv"], key="vid_up"
56
+ )
57
+ run_vid = st.button("🎥 Run Video Detection", use_container_width=True)
58
+
59
+ st.sidebar.markdown("---")
60
+ # Parameters card (shared)
61
+ st.sidebar.header("Parameters")
62
+ conf_thr = st.slider("Minimum confidence threshold", 0.05, 0.95, 0.50, 0.01)
63
+
64
+ st.sidebar.markdown("---")
65
+
66
+ # Get model manager instance
67
+ model_manager = get_model_manager()
68
+
69
+ # Render model selection UI
70
+ model_label, model_key = model_manager.render_model_selection(
71
+ key_prefix="lost_at_sea"
72
+ )
73
+ st.sidebar.markdown("---")
74
+
75
+ # Render device information
76
+ model_manager.render_device_info()
77
+
78
+
79
+ # =============== Detection helpers ===============
80
+ def run_image_detection(uploaded_file, conf_thr: float = 0.5):
81
+ try:
82
+ data = uploaded_file.getvalue()
83
+ img = Image.open(io.BytesIO(data)).convert("RGB")
84
+ st.image(img, caption="Uploaded Image", use_container_width=True)
85
+ except Exception as e:
86
+ st.error(f"Error loading image: {e}")
87
+ return
88
+
89
+ try:
90
+ model = load_model(model_key)
91
+ with st.spinner("Running detection..."):
92
+ annotated = model.predict_image(img, min_confidence=conf_thr)
93
+
94
+ st.subheader("🎯 Detection Results")
95
+ st.image(annotated, caption="Detections", width="stretch")
96
+ except Exception as e:
97
+ st.error(f"Error during detection: {e}")
98
+
99
+
100
+ def run_video_detection(vid_bytes, conf_thr: float = 0.5, model_key: str = "deim"):
101
+ tmp_in = Path(tempfile.gettempdir()) / f"in_{int(time.time())}.mp4"
102
+ with open(tmp_in, "wb") as f:
103
+ f.write(vid_bytes)
104
+
105
+ model = load_model(model_key)
106
+
107
+ # Set up video capture for preview
108
+ cap = cv2.VideoCapture(str(tmp_in))
109
+ if not cap.isOpened():
110
+ st.error("Failed to open the uploaded video.")
111
+ return
112
+
113
+ fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
114
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
115
+
116
+ # Set up preview and progress
117
+ frame_ph = st.empty()
118
+ prog = st.progress(0.0, text="Processing…")
119
+
120
+ # Set up video writer for output
121
+ W = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
122
+ H = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
123
+ tmp_out = Path(tempfile.gettempdir()) / f"out_{int(time.time())}.mp4"
124
+ writer = cv2.VideoWriter(str(tmp_out), cv2.VideoWriter_fourcc(*"mp4v"), fps, (W, H))
125
+
126
+ frame_count = 0
127
+ last_preview_update = 0
128
+ preview_update_interval = 5 # Update preview every 5 frames
129
+
130
+ try:
131
+ with st.spinner("Processing video with live preview…"):
132
+ while True:
133
+ ok, frame = cap.read()
134
+ if not ok:
135
+ break
136
+
137
+ # Process frame with model
138
+ if model_key == "deim":
139
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
140
+ annotated_pil = model.predict_image(
141
+ Image.fromarray(frame_rgb), min_confidence=conf_thr
142
+ )
143
+ vis = cv2.cvtColor(np.array(annotated_pil), cv2.COLOR_RGB2BGR)
144
+ else:
145
+ _, vis = model.predict_and_visualize(
146
+ frame, min_confidence=conf_thr, show_score=True
147
+ )
148
+
149
+ # Update progress
150
+ progress = frame_count / total_frames if total_frames > 0 else 0
151
+ prog.progress(
152
+ progress,
153
+ text=f"Processing frame {frame_count + 1}/{total_frames}...",
154
+ )
155
+
156
+ # Update preview (throttled to prevent freezing)
157
+ if (frame_count - last_preview_update) >= preview_update_interval:
158
+ frame_ph.image(
159
+ cv2.cvtColor(vis, cv2.COLOR_BGR2RGB),
160
+ use_container_width=True,
161
+ output_format="JPEG",
162
+ channels="RGB",
163
+ )
164
+ last_preview_update = frame_count
165
+
166
+ # Write frame to output video
167
+ writer.write(vis)
168
+ frame_count += 1
169
+
170
+ except Exception as exc:
171
+ st.error(f"Video detection failed: {exc}")
172
+ return
173
+ finally:
174
+ cap.release()
175
+ writer.release()
176
+
177
+ st.success("Done!")
178
+
179
+ # Check if output file exists before trying to display it
180
+ if tmp_out.exists():
181
+ st.video(str(tmp_out))
182
+ with open(tmp_out, "rb") as f:
183
+ st.download_button(
184
+ "Download processed video",
185
+ data=f.read(),
186
+ file_name=tmp_out.name,
187
+ mime="video/mp4",
188
+ )
189
+ else:
190
+ st.error("Video processing completed but output file was not created.")
191
+
192
+
193
+ # =============== Main: hook up actions ===============
194
+ if run_img:
195
+ if img_file is None:
196
+ st.warning("Please upload an image first.")
197
+ else:
198
+ run_image_detection(img_file, conf_thr=conf_thr)
199
+
200
+ if run_vid:
201
+ if vid_file is None:
202
+ st.warning("Please upload a video first.")
203
+ else:
204
+ run_video_detection(
205
+ vid_bytes=vid_file.read(), conf_thr=conf_thr, model_key=model_key
206
+ )
services/app_service/pages/signal_watch.py CHANGED
@@ -91,7 +91,7 @@ with st.sidebar:
91
 
92
  col1, col2 = st.columns(2)
93
  with col1:
94
- run_detection = st.button("🎥 Run Detection", use_container_width=True)
95
  with col2:
96
  stop_detection = st.button(
97
  "⏹️ Stop", use_container_width=True, help="Stop current detection"
 
91
 
92
  col1, col2 = st.columns(2)
93
  with col1:
94
+ run_detection = st.button("🎥 Run", use_container_width=True)
95
  with col2:
96
  stop_detection = st.button(
97
  "⏹️ Stop", use_container_width=True, help="Stop current detection"
services/app_service/pages/task_drone DD.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # pages/lost_at_sea.py
2
+ import io
3
+ import tempfile
4
+ import time
5
+ from pathlib import Path
6
+
7
+ import cv2
8
+ import numpy as np
9
+ import streamlit as st
10
+ from PIL import Image
11
+ from utils.model_manager import get_model_manager, load_model
12
+ from urllib.parse import quote_plus
13
+
14
+ # =============== Page setup ===============
15
+ st.set_page_config(
16
+ page_title="Lost at Sea", layout="wide", initial_sidebar_state="expanded"
17
+ )
18
+
19
+ st.markdown(
20
+ "<h2 style='text-align:center;margin-top:0'>SAR-X<sup>ai</h2>"
21
+ "<h2 style='text-align:center;margin-top:0'>Task Drone(s) 🛸</h2>",
22
+ unsafe_allow_html=True,
23
+ )
24
+
25
+ # =============== Sidebar: custom menu + cards ===============
26
+ with st.sidebar:
27
+ st.page_link("app.py", label="Home")
28
+ st.page_link("pages/bushland_beacon.py", label="Bushland Beacon")
29
+ st.page_link("pages/lost_at_sea.py", label="Lost at Sea")
30
+ st.page_link("pages/signal_watch.py", label="Signal Watch")
31
+ st.markdown("---")
32
+ st.page_link("pages/task_satellite.py", label="Task Satellite")
33
+ st.page_link("pages/task_drone.py", label="Task Drone")
34
+
35
+ st.markdown("---")
36
+
37
+ # Simple "card" styling in the sidebar
38
+ st.markdown(
39
+ """
40
+ <style>
41
+ .sb-card {border:1px solid rgba(255,255,255,0.15); padding:14px; border-radius:8px; margin-bottom:16px;}
42
+ .sb-card h4 {margin:0 0 10px 0; font-weight:700;}
43
+ </style>
44
+ """,
45
+ unsafe_allow_html=True,
46
+ )
47
+
48
+ st.set_page_config(page_title="Map demo", layout="wide")
49
+
50
+ place = "Adelaide, South Australia"
51
+ embed_url = f"https://www.google.com/maps?q={quote_plus(place)}&output=embed"
52
+
53
+ st.components.v1.iframe(embed_url, height=500)
services/app_service/pages/task_drone.py CHANGED
@@ -1,28 +1,17 @@
1
- # pages/lost_at_sea.py
2
- import io
3
- import tempfile
4
- import time
5
- from pathlib import Path
6
-
7
- import cv2
8
- import numpy as np
9
  import streamlit as st
10
- from PIL import Image
11
- from utils.model_manager import get_model_manager, load_model
12
  from urllib.parse import quote_plus
13
 
14
  # =============== Page setup ===============
15
- st.set_page_config(
16
- page_title="Lost at Sea", layout="wide", initial_sidebar_state="expanded"
17
- )
18
 
19
  st.markdown(
20
- "<h2 style='text-align:center;margin-top:0'>SAR-X<sup>ai</h2>"
21
  "<h2 style='text-align:center;margin-top:0'>Task Drone(s) 🛸</h2>",
22
  unsafe_allow_html=True,
23
  )
24
 
25
- # =============== Sidebar: custom menu + cards ===============
26
  with st.sidebar:
27
  st.page_link("app.py", label="Home")
28
  st.page_link("pages/bushland_beacon.py", label="Bushland Beacon")
@@ -31,23 +20,119 @@ with st.sidebar:
31
  st.markdown("---")
32
  st.page_link("pages/task_satellite.py", label="Task Satellite")
33
  st.page_link("pages/task_drone.py", label="Task Drone")
34
-
35
  st.markdown("---")
36
 
37
- # Simple "card" styling in the sidebar
38
- st.markdown(
39
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  <style>
41
- .sb-card {border:1px solid rgba(255,255,255,0.15); padding:14px; border-radius:8px; margin-bottom:16px;}
42
- .sb-card h4 {margin:0 0 10px 0; font-weight:700;}
 
 
 
 
 
 
 
 
 
43
  </style>
44
  """,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  unsafe_allow_html=True,
46
  )
47
 
48
- st.set_page_config(page_title="Map demo", layout="wide")
 
49
 
50
- place = "Adelaide, South Australia"
51
- embed_url = f"https://www.google.com/maps?q={quote_plus(place)}&output=embed"
52
-
53
- st.components.v1.iframe(embed_url, height=500)
 
 
1
+ # pages/task_drone.py
 
 
 
 
 
 
 
2
  import streamlit as st
 
 
3
  from urllib.parse import quote_plus
4
 
5
  # =============== Page setup ===============
6
+ st.set_page_config(page_title="Task Drone – Map", layout="wide", initial_sidebar_state="expanded")
 
 
7
 
8
  st.markdown(
9
+ "<h2 style='text-align:center;margin-top:0'>SAR-X<sup>ai</sup></h2>"
10
  "<h2 style='text-align:center;margin-top:0'>Task Drone(s) 🛸</h2>",
11
  unsafe_allow_html=True,
12
  )
13
 
14
+ # =============== Sidebar navigation ===============
15
  with st.sidebar:
16
  st.page_link("app.py", label="Home")
17
  st.page_link("pages/bushland_beacon.py", label="Bushland Beacon")
 
20
  st.markdown("---")
21
  st.page_link("pages/task_satellite.py", label="Task Satellite")
22
  st.page_link("pages/task_drone.py", label="Task Drone")
 
23
  st.markdown("---")
24
 
25
+ # =============== Helpers & state ===============
26
+ DEFAULT_PLACE = "Adelaide, South Australia"
27
+
28
+ def make_embed_url(address: str | None, lat: float | None, lon: float | None) -> str:
29
+ """Build a Google Maps embed URL using either an address or lat/lon."""
30
+ if address and address.strip():
31
+ q = quote_plus(address.strip())
32
+ elif lat is not None and lon is not None:
33
+ q = quote_plus(f"{lat},{lon}")
34
+ else:
35
+ q = quote_plus(DEFAULT_PLACE)
36
+ return f"https://www.google.com/maps?q={q}&output=embed"
37
+
38
+ # --- State management ---
39
+ if "address" not in st.session_state:
40
+ st.session_state.address = ""
41
+ if "lat" not in st.session_state:
42
+ st.session_state.lat = None
43
+ if "lon" not in st.session_state:
44
+ st.session_state.lon = None
45
+ if "embed_url" not in st.session_state:
46
+ st.session_state.embed_url = make_embed_url(None, None, None)
47
+
48
+ # =============== Input row with aligned bottom buttons ===============
49
+ # Add spacing instead of header
50
+ st.markdown("<br><br>", unsafe_allow_html=True)
51
+
52
+ # Columns: Address | Lat | Lon | Update | Task Drone
53
+ c1, c2, c3, c4, c5 = st.columns([3, 1.2, 1.2, 1.1, 1.1])
54
+
55
+ with c1:
56
+ st.session_state.address = st.text_input(
57
+ "Address (optional)",
58
+ value=st.session_state.address,
59
+ placeholder="e.g., 25 Grenfell St, Adelaide SA",
60
+ help="Enter a full street address, suburb, or landmark."
61
+ )
62
+
63
+ with c2:
64
+ st.session_state.lat = st.number_input(
65
+ "Latitude (optional)",
66
+ value=st.session_state.lat if st.session_state.lat is not None else -34.9285,
67
+ format="%.8f"
68
+ )
69
+
70
+ with c3:
71
+ st.session_state.lon = st.number_input(
72
+ "Longitude (optional)",
73
+ value=st.session_state.lon if st.session_state.lon is not None else 138.6007,
74
+ format="%.8f"
75
+ )
76
+
77
+ # --- Custom button styles ---
78
+ st.markdown(
79
+ """
80
  <style>
81
+ div.stButton > button {
82
+ height: 2.8em;
83
+ border-radius: 6px;
84
+ font-weight: 600;
85
+ }
86
+ div.stButton > button.task-drone-btn {
87
+ background-color: #A7C7E7 !important; /* Pastel Blue */
88
+ color: black !important;
89
+ font-weight: 700 !important;
90
+ border: 1px solid rgba(0,0,0,0.15);
91
+ }
92
  </style>
93
  """,
94
+ unsafe_allow_html=True,
95
+ )
96
+
97
+ # --- Bottom-aligned buttons ---
98
+ with c4:
99
+ st.markdown("<div style='height:1.8em'></div>", unsafe_allow_html=True)
100
+ if st.button("Update Map", use_container_width=True):
101
+ addr = st.session_state.address.strip() if st.session_state.address else ""
102
+ lat = st.session_state.lat
103
+ lon = st.session_state.lon
104
+ addr = addr if addr else None
105
+ if (lat is None) or (lon is None):
106
+ lat, lon = None, None
107
+ st.session_state.embed_url = make_embed_url(addr, lat, lon)
108
+
109
+ with c5:
110
+ st.markdown("<div style='height:1.8em'></div>", unsafe_allow_html=True)
111
+ st.button("Task Drone", key="task_drone", use_container_width=True, type="secondary")
112
+
113
+ # Apply pastel blue style dynamically
114
+ st.markdown(
115
+ """
116
+ <script>
117
+ const interval = setInterval(() => {
118
+ const btns = window.parent.document.querySelectorAll('button[kind="secondary"]');
119
+ btns.forEach(b => {
120
+ if (b.innerText.includes('Task Drone')) {
121
+ b.classList.add('task-drone-btn');
122
+ clearInterval(interval);
123
+ }
124
+ });
125
+ }, 100);
126
+ </script>
127
+ """,
128
  unsafe_allow_html=True,
129
  )
130
 
131
+ # =============== Map embed ===============
132
+ st.components.v1.iframe(st.session_state.embed_url, height=520)
133
 
134
+ # Optional: link to open directly in Google Maps
135
+ st.markdown(
136
+ f"[Open in Google Maps]({st.session_state.embed_url.replace('output=embed', '')})",
137
+ help="Opens the current view in a new browser tab."
138
+ )
services/app_service/pages/task_satellite copy.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # pages/lost_at_sea.py
2
+ import io
3
+ import tempfile
4
+ import time
5
+ from pathlib import Path
6
+
7
+ import cv2
8
+ import numpy as np
9
+ import streamlit as st
10
+ from PIL import Image
11
+ from utils.model_manager import get_model_manager, load_model
12
+ from urllib.parse import quote_plus
13
+
14
+ # =============== Page setup ===============
15
+ st.set_page_config(
16
+ page_title="Task Satellite", layout="wide", initial_sidebar_state="expanded"
17
+ )
18
+
19
+ st.markdown(
20
+ "<h2 style='text-align:center;margin-top:0'>S A R - X ai\u200b</h2>"
21
+ "<h2 style='text-align:center;margin-top:0'>Task Satellite 🛰️</h2>",
22
+ unsafe_allow_html=True,
23
+ )
24
+
25
+ # =============== Sidebar: custom menu + cards ===============
26
+ with st.sidebar:
27
+ st.page_link("app.py", label="Home")
28
+ st.page_link("pages/bushland_beacon.py", label="Bushland Beacon")
29
+ st.page_link("pages/lost_at_sea.py", label="Lost at Sea")
30
+ st.page_link("pages/signal_watch.py", label="Signal Watch")
31
+ st.markdown("---")
32
+ st.page_link("pages/task_satellite.py", label="Task Satellite")
33
+ st.page_link("pages/task_drone.py", label="Task Drone")
34
+
35
+ st.markdown("---")
36
+
37
+ # Simple "card" styling in the sidebar
38
+ st.markdown(
39
+ """
40
+ <style>
41
+ .sb-card {border:1px solid rgba(255,255,255,0.15); padding:14px; border-radius:8px; margin-bottom:16px;}
42
+ .sb-card h4 {margin:0 0 10px 0; font-weight:700;}
43
+ </style>
44
+ """,
45
+ unsafe_allow_html=True,
46
+ )
47
+
48
+
49
+ st.set_page_config(page_title="Map demo", layout="wide")
50
+
51
+ place = "Yunta, South Australia"
52
+ embed_url = f"https://www.google.com/maps?q={quote_plus(place)}&output=embed"
53
+
54
+ st.components.v1.iframe(embed_url, height=500)
55
+
services/app_service/pages/task_satellite.py CHANGED
@@ -1,28 +1,29 @@
1
- # pages/lost_at_sea.py
2
- import io
3
- import tempfile
4
- import time
5
- from pathlib import Path
6
-
7
- import cv2
8
- import numpy as np
9
  import streamlit as st
10
- from PIL import Image
11
- from utils.model_manager import get_model_manager, load_model
12
- from urllib.parse import quote_plus
13
 
14
  # =============== Page setup ===============
15
  st.set_page_config(
16
- page_title="Task Satellite", layout="wide", initial_sidebar_state="expanded"
 
 
17
  )
18
 
19
- st.markdown(
20
- "<h2 style='text-align:center;margin-top:0'>S A R - X ai\u200b</h2>"
21
- "<h2 style='text-align:center;margin-top:0'>Task Satellite 🛰️</h2>",
22
- unsafe_allow_html=True,
23
- )
 
 
 
 
 
 
 
24
 
25
- # =============== Sidebar: custom menu + cards ===============
26
  with st.sidebar:
27
  st.page_link("app.py", label="Home")
28
  st.page_link("pages/bushland_beacon.py", label="Bushland Beacon")
@@ -31,25 +32,190 @@ with st.sidebar:
31
  st.markdown("---")
32
  st.page_link("pages/task_satellite.py", label="Task Satellite")
33
  st.page_link("pages/task_drone.py", label="Task Drone")
34
-
35
  st.markdown("---")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
- # Simple "card" styling in the sidebar
38
- st.markdown(
39
- """
40
- <style>
41
- .sb-card {border:1px solid rgba(255,255,255,0.15); padding:14px; border-radius:8px; margin-bottom:16px;}
42
- .sb-card h4 {margin:0 0 10px 0; font-weight:700;}
43
- </style>
44
- """,
45
- unsafe_allow_html=True,
46
  )
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
- st.set_page_config(page_title="Map demo", layout="wide")
 
 
 
 
50
 
51
- place = "Yunta, South Australia"
52
- embed_url = f"https://www.google.com/maps?q={quote_plus(place)}&output=embed"
53
 
54
- st.components.v1.iframe(embed_url, height=500)
 
 
 
 
 
 
 
55
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
+ from datetime import datetime, timedelta
3
+ import time
4
+ import random
5
 
6
  # =============== Page setup ===============
7
  st.set_page_config(
8
+ page_title="Task Satellite",
9
+ layout="wide",
10
+ initial_sidebar_state="expanded", # keep sidebar open
11
  )
12
 
13
+ # =============== Compact styling & header spacing (avoid cropping) ===============
14
+ st.markdown("""
15
+ <style>
16
+ .block-container { padding-top: 1.2rem; padding-bottom: 0.6rem; max-width: 1400px; }
17
+ h2, h3, h4 { margin: 0.4rem 0; }
18
+ textarea { min-height: 70px !important; }
19
+ /* Light outline on inputs */
20
+ .stSelectbox > div, .stTextInput > div, .stNumberInput > div, .stDateInput > div, .stTextArea > div {
21
+ border: 1px solid #D0D0D0; border-radius: 6px;
22
+ }
23
+ </style>
24
+ """, unsafe_allow_html=True)
25
 
26
+ # =============== Sidebar navigation ===============
27
  with st.sidebar:
28
  st.page_link("app.py", label="Home")
29
  st.page_link("pages/bushland_beacon.py", label="Bushland Beacon")
 
32
  st.markdown("---")
33
  st.page_link("pages/task_satellite.py", label="Task Satellite")
34
  st.page_link("pages/task_drone.py", label="Task Drone")
 
35
  st.markdown("---")
36
+ st.caption("SAR-X AI • Satellite Tasking Module")
37
+
38
+ # =============== Header ===============
39
+ st.markdown(
40
+ "<h2 style='text-align:center;'>SAR-X<sup>ai</sup></h2>"
41
+ "<h2 style='text-align:center;margin-top:0.2rem;'>Task Satellite 🛰️</h2>",
42
+ unsafe_allow_html=True,
43
+ )
44
+
45
+ # =============== Top Row: Provider & Notes (single row) ===============
46
+ satellite_providers = [
47
+ "Auto Select Lowest Cost",
48
+ "Auto Select Fastest Turnaround",
49
+ "BlackSky",
50
+ "Planet Labs",
51
+ "Maxar Technologies",
52
+ "Airbus Defence and Space",
53
+ "ICEYE",
54
+ "Capella Space",
55
+ "Satellogic",
56
+ "Earth-i",
57
+ "Umbra Space",
58
+ "Spire Global"
59
+ ]
60
+
61
+ # Metadata for cost/turnaround heuristics
62
+ provider_cost_multiplier = {
63
+ "BlackSky": 1.00, "Planet Labs": 0.85, "Maxar Technologies": 1.40,
64
+ "Airbus Defence and Space": 1.25, "ICEYE": 1.10, "Capella Space": 1.15,
65
+ "Satellogic": 0.80, "Earth-i": 0.95, "Umbra Space": 1.05, "Spire Global": 0.90
66
+ }
67
+ provider_turnaround_hours = {
68
+ "BlackSky": 6, "Planet Labs": 8, "Maxar Technologies": 6,
69
+ "Airbus Defence and Space": 7, "ICEYE": 5, "Capella Space": 5,
70
+ "Satellogic": 7, "Earth-i": 8, "Umbra Space": 6, "Spire Global": 8
71
+ }
72
+
73
+ # ===== Utility Functions =====
74
+ def resolve_provider(choice: str) -> str:
75
+ if choice == "Auto Select Lowest Cost":
76
+ return min(provider_cost_multiplier, key=provider_cost_multiplier.get)
77
+ if choice == "Auto Select Fastest Turnaround":
78
+ return min(provider_turnaround_hours, key=provider_turnaround_hours.get)
79
+ return choice
80
+
81
+ def estimate_cost(resolution: str, capture_type: str, priority: str, provider_choice: str) -> float:
82
+ base_by_res = {"0.3 m": 3500, "0.5 m": 2500, "1 m": 1500, "3 m": 800, "10 m": 300}
83
+ type_add = {"Optical": 0, "SAR (Radar)": 600, "Multispectral": 400, "Infrared": 500}
84
+ priority_mult = {"Low": 1.00, "Medium": 1.10, "High": 1.25}
85
+ prov = resolve_provider(provider_choice)
86
+ prov_mult = provider_cost_multiplier.get(prov, 1.0)
87
+ cost = (base_by_res[resolution] + type_add[capture_type]) * priority_mult[priority] * prov_mult
88
+ return round(cost, 2)
89
+
90
+ # ACST utilities
91
+ def now_acst() -> datetime:
92
+ return datetime.utcnow() + timedelta(hours=9, minutes=30)
93
+
94
+ def eta_time_acst(hours_from_now: int) -> datetime:
95
+ return now_acst() + timedelta(hours=hours_from_now)
96
 
97
+ # =============== Provider & Notes Row ===============
98
+ top_left, top_right = st.columns([0.32, 0.68])
99
+ with top_left:
100
+ selected_provider = st.selectbox("Satellite Provider", satellite_providers, index=0)
101
+ with top_right:
102
+ additional_notes = st.text_area(
103
+ "Provider Notes / Special Instructions",
104
+ placeholder="Any special imaging parameters or constraints…"
 
105
  )
106
 
107
+ st.markdown("---")
108
+
109
+ # =============== Bottom: Left (Form) / Right (Response) ===============
110
+ left_col, right_col = st.columns(2)
111
+
112
+ with left_col:
113
+ st.subheader("Tasking Parameters", divider=False)
114
+
115
+ # ---- FORM with specific row layout order ----
116
+ with st.form("task_satellite_form", clear_on_submit=False):
117
+ # Row 1: Target area name
118
+ target_name = st.text_input("Target Area Name", placeholder="e.g., Yunta, South Australia")
119
+
120
+ # Row 2: Latitude & Longitude
121
+ r2c1, r2c2 = st.columns(2)
122
+ with r2c1:
123
+ latitude = st.number_input("Latitude", format="%.6f", value=0.0)
124
+ with r2c2:
125
+ longitude = st.number_input("Longitude", format="%.6f", value=0.0)
126
+
127
+ # Row 3: Capture Type & Resolution
128
+ r3c1, r3c2 = st.columns(2)
129
+ with r3c1:
130
+ capture_type = st.selectbox("Capture Type", ["Optical", "SAR (Radar)", "Multispectral", "Infrared"])
131
+ with r3c2:
132
+ resolution = st.selectbox("Desired Resolution", ["0.3 m", "0.5 m", "1 m", "3 m", "10 m"], index=1)
133
+
134
+ # Row 4: Capture Date & Priority Level
135
+ r4c1, r4c2 = st.columns(2)
136
+ with r4c1:
137
+ request_date = st.date_input("Preferred Capture Date", datetime.now().date())
138
+ with r4c2:
139
+ priority = st.select_slider("Priority Level", ["Low", "Medium", "High"], value="Medium")
140
+
141
+ # Row 5: Buttons
142
+ b_l, b_r = st.columns([0.5, 0.5])
143
+ with b_l:
144
+ estimate_clicked = st.form_submit_button("Estimate Cost", use_container_width=True)
145
+ with b_r:
146
+ submitted = st.form_submit_button("Submit Satellite Task", use_container_width=True)
147
+
148
+ # =============== Right Side: Response Section ===============
149
+ with right_col:
150
+ st.subheader("Tasking Information Response", divider=False)
151
+
152
+ # --- Estimate Cost ---
153
+ if 'estimate_clicked' in locals() and estimate_clicked and not submitted:
154
+ resolved = resolve_provider(selected_provider)
155
+ est = estimate_cost(resolution, capture_type, priority, selected_provider)
156
+ indicative_hours = provider_turnaround_hours.get(resolved, 6)
157
+ indicative_eta = eta_time_acst(indicative_hours).strftime("%Y-%m-%d %H:%M")
158
+
159
+ st.info(
160
+ f"💰 Estimated cost with **{resolved}**: **${est:,.2f}** \n"
161
+ f"⏱️ Indicative delivery by **{indicative_eta} ACST (UTC+9:30)** "
162
+ f"(**+{indicative_hours} hours**)."
163
+ )
164
+
165
+ a, b = st.columns(2)
166
+ with a:
167
+ st.markdown(f"**Target:** {target_name or '—'}")
168
+ st.markdown(f"**Resolution:** {resolution}")
169
+ st.markdown(f"**Type:** {capture_type}")
170
+ with b:
171
+ st.markdown(f"**Priority:** {priority}")
172
+ st.markdown(f"**Provider Choice:** {selected_provider} → **{resolved}**")
173
+ if additional_notes.strip():
174
+ st.markdown("**Notes:**")
175
+ st.caption(additional_notes)
176
+
177
+ # --- Submit Task ---
178
+ if 'submitted' in locals() and submitted:
179
+ resolved = resolve_provider(selected_provider)
180
+ st.success(f"✅ Task submitted to **{resolved}**")
181
+
182
+ a, b = st.columns(2)
183
+ with a:
184
+ st.markdown(f"**Target:** {target_name or '—'}")
185
+ st.markdown(f"**Date:** {request_date.strftime('%Y-%m-%d')}")
186
+ st.markdown(f"**Coordinates:** ({latitude:.6f}, {longitude:.6f})")
187
+ with b:
188
+ st.markdown(f"**Resolution:** {resolution}")
189
+ st.markdown(f"**Type:** {capture_type}")
190
+ st.markdown(f"**Priority:** {priority}")
191
+
192
+ if additional_notes.strip():
193
+ st.markdown("**Notes:**")
194
+ st.caption(additional_notes)
195
+
196
+ est = estimate_cost(resolution, capture_type, priority, selected_provider)
197
+ st.markdown(f"**Estimated Cost:** ${est:,.2f}")
198
+
199
+ with st.spinner("Contacting satellite provider…"):
200
+ time.sleep(3)
201
 
202
+ # Calculate ETA hours and timestamp
203
+ if selected_provider == "Auto Select Fastest Turnaround":
204
+ eta_hours = provider_turnaround_hours.get(resolved, 4)
205
+ else:
206
+ eta_hours = random.randint(4, 8)
207
 
208
+ eta_local = eta_time_acst(eta_hours).strftime("%Y-%m-%d %H:%M")
 
209
 
210
+ st.info(
211
+ f"📩 {resolved} advises an ETA on image delivery **within {eta_hours} hours** — "
212
+ f"approximately **{eta_local} ACST (UTC+9:30)** "
213
+ f"(**+{eta_hours} hours**). \n"
214
+ f"🕒 Current time (ACST): **{now_acst().strftime('%Y-%m-%d %H:%M')}**."
215
+ )
216
+ elif not ('estimate_clicked' in locals() and estimate_clicked):
217
+ st.caption("Use **Estimate Cost** to preview pricing or **Submit** to create the task and get an ETA here (shown in **ACST**).")
218
 
219
+ # =============== Footer ===============
220
+ st.markdown("---")
221
+ st.caption("Task Satellite • SAR-X AI Satellite Imagery Request Interface · Times shown in **ACST (UTC+9:30)**")
services/app_service/resources/cookies/www.youtube.com_cookies (1).txt CHANGED
@@ -14,13 +14,13 @@
14
  .youtube.com TRUE / TRUE 1795860052 __Secure-1PSID g.a0002wgrpcW9K-QNYioDxFejw3GZD9efUs0mRhFq3acAXjra01COElc9yIf8E5lZjcmehkOAoAACgYKATYSARUSFQHGX2MiWlVwx7WCXoMqEpE8OPe6uBoVAUF8yKoNXCEYEM1OaXvtmwgRgVk50076
15
  .youtube.com TRUE / TRUE 1795860052 __Secure-3PSID g.a0002wgrpcW9K-QNYioDxFejw3GZD9efUs0mRhFq3acAXjra01COIiAEqiDC2XZPFcs_RH2U7AACgYKAfYSARUSFQHGX2Mi0KLOQQs0Au5u5xYXwmfaBhoVAUF8yKqN132DDK9He9JDLUX1bEz10076
16
  .youtube.com TRUE / TRUE 1795860053 LOGIN_INFO AFmmF2swRAIgRKXvUyDr8P18ZQwmyUgxyKGvHjNBMkeIsGSaWm76WDACIG2OvtxNgjffm-ENPsSwdsv6sn1Rv3OLmUXY8uVhCzEF:QUQ3MjNmeWxRMkNvOEtZejh3VEVRRGxlazNMY3hEa0Zhc0dUcl9vTlVFZWtDdk5PX2laWWpvLWE0WngxbW9oYlNQRzFackoxeUpmTkx5SGtsWW9kUXl4eFo1ZUphTVFiYkhlRGxsTksyUUZkRnBBbTNDWWFVLUdlS09QaUVyUU1SVEtVOVNaOElJSFl5X2JPS09TT2JSQkswelFSaW43TVln
17
- .youtube.com TRUE / FALSE 1793366586 SIDCC AKEyXzXCMOfvp8uO-J6COrOilaKcYtanBJUZCq0yJ5dX2AkhCKEaNIxKgblRFRR0AORNnTMwVA
18
- .youtube.com TRUE / TRUE 1793366586 __Secure-1PSIDCC AKEyXzWk2DwWgxLBLJ0HGYGlI1VNotnmdJ1Dv6qAUeoaPeSrLgEDJ4RerINu_Hjh0KPWqYS9
19
- .youtube.com TRUE / TRUE 1793366586 __Secure-3PSIDCC AKEyXzXF2OBd90Rh50Ct6Nwb2rChQY7FacAqWvrsAay3KmEKXObSNiCcnG4-AZTYECnjoz-fSw
20
  .youtube.com TRUE / TRUE 0 YSC JNOzy0pmT_0
21
- .youtube.com TRUE / TRUE 1777382586 VISITOR_INFO1_LIVE y1BPxdbbDNs
22
- .youtube.com TRUE / TRUE 1777382586 VISITOR_PRIVACY_METADATA CgJBVRIEGgAgJw%3D%3D
23
- .youtube.com TRUE / TRUE 1777352393 __Secure-ROLLOUT_TOKEN CMDh2N6P_q_XIBDpoLH5yLyQAxiel4z1kcuQAw%3D%3D
24
- .youtube.com TRUE / TRUE 1824902586 __Secure-YT_TVFAS t=489249&s=2
25
- .youtube.com TRUE / TRUE 1777382586 DEVICE_INFO ChxOelUyTlRBNE56VXlNek0xTnpBek16QTVOdz09ELrNjcgGGIWu8scG
26
- .youtube.com TRUE /tv TRUE 1794662586 __Secure-YT_DERP CPjNrY5_
 
14
  .youtube.com TRUE / TRUE 1795860052 __Secure-1PSID g.a0002wgrpcW9K-QNYioDxFejw3GZD9efUs0mRhFq3acAXjra01COElc9yIf8E5lZjcmehkOAoAACgYKATYSARUSFQHGX2MiWlVwx7WCXoMqEpE8OPe6uBoVAUF8yKoNXCEYEM1OaXvtmwgRgVk50076
15
  .youtube.com TRUE / TRUE 1795860052 __Secure-3PSID g.a0002wgrpcW9K-QNYioDxFejw3GZD9efUs0mRhFq3acAXjra01COIiAEqiDC2XZPFcs_RH2U7AACgYKAfYSARUSFQHGX2Mi0KLOQQs0Au5u5xYXwmfaBhoVAUF8yKqN132DDK9He9JDLUX1bEz10076
16
  .youtube.com TRUE / TRUE 1795860053 LOGIN_INFO AFmmF2swRAIgRKXvUyDr8P18ZQwmyUgxyKGvHjNBMkeIsGSaWm76WDACIG2OvtxNgjffm-ENPsSwdsv6sn1Rv3OLmUXY8uVhCzEF:QUQ3MjNmeWxRMkNvOEtZejh3VEVRRGxlazNMY3hEa0Zhc0dUcl9vTlVFZWtDdk5PX2laWWpvLWE0WngxbW9oYlNQRzFackoxeUpmTkx5SGtsWW9kUXl4eFo1ZUphTVFiYkhlRGxsTksyUUZkRnBBbTNDWWFVLUdlS09QaUVyUU1SVEtVOVNaOElJSFl5X2JPS09TT2JSQkswelFSaW43TVln
17
+ .youtube.com TRUE / FALSE 1793698107 SIDCC AKEyXzVAVwXKFd43pkOzcTiH8vfheMtilO22HqJv1v45HW7UjAA_Sly1FSTIk42AKdYYdUPUEg
18
+ .youtube.com TRUE / TRUE 1793698107 __Secure-1PSIDCC AKEyXzWYtNMieN0NnU8kxj9pm92taAEtt8L6g9z1Zp7cQ0eNL5rS-VOEqZ6XPcKjFoCFpkwg
19
+ .youtube.com TRUE / TRUE 1793698107 __Secure-3PSIDCC AKEyXzUr4JbwQQ3NQrl0BpGuofBrSW25fotVKT5RbHdauHq8lZvOw_Knp--RaXDxygvJu3ZZ1Q
20
  .youtube.com TRUE / TRUE 0 YSC JNOzy0pmT_0
21
+ .youtube.com TRUE / TRUE 1777714107 VISITOR_INFO1_LIVE y1BPxdbbDNs
22
+ .youtube.com TRUE / TRUE 1777714107 VISITOR_PRIVACY_METADATA CgJBVRIEGgAgJw%3D%3D
23
+ .youtube.com TRUE / TRUE 1777706424 __Secure-ROLLOUT_TOKEN CMDh2N6P_q_XIBDpoLH5yLyQAxjNjY3kuNWQAw%3D%3D
24
+ .youtube.com TRUE / TRUE 1825234107 __Secure-YT_TVFAS t=489249&s=2
25
+ .youtube.com TRUE / TRUE 1777714107 DEVICE_INFO ChxOelUyTlRBNE56VXlNek0xTnpBek16QTVOdz09ELvrocgGGIWu8scG
26
+ .youtube.com TRUE /tv TRUE 1794994107 __Secure-YT_DERP CPjNrY5_
services/app_service/resources/images/rescue3.jpg ADDED

Git LFS Details

  • SHA256: 2c43fe88260346c997f5252a23e6b2124a159999e623a037b19623da45519fbf
  • Pointer size: 131 Bytes
  • Size of remote file: 160 kB