Spaces:
Sleeping
Sleeping
CI: deploy Docker/PDM Space
Browse files
services/app_service/pages/bushland_beacon.py
CHANGED
|
@@ -91,15 +91,15 @@ with st.sidebar:
|
|
| 91 |
|
| 92 |
st.sidebar.header("Image Detection")
|
| 93 |
img_file = st.file_uploader("Upload an image", type=["jpg", "jpeg", "png"], key="img_up")
|
| 94 |
-
run_img = st.button("
|
| 95 |
|
| 96 |
st.sidebar.header("Video")
|
| 97 |
vid_file = st.file_uploader("Upload a video", type=["mp4", "mov", "avi", "mkv"], key="vid_up")
|
| 98 |
|
| 99 |
# New buttons
|
| 100 |
-
run_vid_plain = st.button("Play
|
| 101 |
-
run_vid = st.button("
|
| 102 |
-
stop_vid = st.button("Stop", use_container_width=True)
|
| 103 |
|
| 104 |
if stop_vid:
|
| 105 |
st.session_state["stop_video"] = True
|
|
|
|
| 91 |
|
| 92 |
st.sidebar.header("Image Detection")
|
| 93 |
img_file = st.file_uploader("Upload an image", type=["jpg", "jpeg", "png"], key="img_up")
|
| 94 |
+
run_img = st.button("🔎 Detect", use_container_width=True)
|
| 95 |
|
| 96 |
st.sidebar.header("Video")
|
| 97 |
vid_file = st.file_uploader("Upload a video", type=["mp4", "mov", "avi", "mkv"], key="vid_up")
|
| 98 |
|
| 99 |
# New buttons
|
| 100 |
+
run_vid_plain = st.button("▶️ Play", use_container_width=True)
|
| 101 |
+
run_vid = st.button("📽️ Detect", use_container_width=True)
|
| 102 |
+
stop_vid = st.button("🛑 Stop", use_container_width=True)
|
| 103 |
|
| 104 |
if stop_vid:
|
| 105 |
st.session_state["stop_video"] = True
|
services/app_service/pages/signal_watch.py
CHANGED
|
@@ -16,7 +16,6 @@ from PIL import Image
|
|
| 16 |
from utils.model_manager import get_model_manager, load_model
|
| 17 |
|
| 18 |
|
| 19 |
-
|
| 20 |
def _image_to_data_url(path: str) -> str:
|
| 21 |
p = Path(path)
|
| 22 |
if not p.is_absolute():
|
|
@@ -40,8 +39,6 @@ model_manager = get_model_manager()
|
|
| 40 |
|
| 41 |
# =============== Sidebar: custom menu + cards ===============
|
| 42 |
with st.sidebar:
|
| 43 |
-
|
| 44 |
-
|
| 45 |
logo_data_url = _image_to_data_url("../resources/images/lucid_insights_logo.png")
|
| 46 |
st.markdown(
|
| 47 |
f"""
|
|
@@ -59,8 +56,6 @@ with st.sidebar:
|
|
| 59 |
st.markdown("---")
|
| 60 |
st.page_link("pages/task_drone.py", label="Task Drone")
|
| 61 |
st.page_link("pages/task_satellite.py", label="Task Satellite")
|
| 62 |
-
|
| 63 |
-
|
| 64 |
st.markdown("---")
|
| 65 |
|
| 66 |
# ---------- Sidebar: Video feed dropdown ----------
|
|
@@ -116,17 +111,19 @@ with st.sidebar:
|
|
| 116 |
max_seconds = None # Always unlimited mode
|
| 117 |
st.info("⚠️ Video Stream is running in unlimited mode — use Stop to end")
|
| 118 |
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
|
|
|
|
|
|
| 127 |
|
| 128 |
# Parameters card (shared)
|
| 129 |
-
st.
|
| 130 |
confidence_threshold = st.slider("Minimum Confidence", 0.0, 1.0, 0.5, 0.01)
|
| 131 |
|
| 132 |
device = model_manager.device
|
|
@@ -146,13 +143,13 @@ with st.sidebar:
|
|
| 146 |
default_stride,
|
| 147 |
help="Higher values skip frames to keep inference responsive on slower hardware.",
|
| 148 |
)
|
| 149 |
-
st.
|
| 150 |
# Render model selection UI
|
| 151 |
model_label, model_key = model_manager.render_model_selection(
|
| 152 |
key_prefix="signal_watch"
|
| 153 |
)
|
| 154 |
|
| 155 |
-
st.
|
| 156 |
# Render device information
|
| 157 |
model_manager.render_device_info()
|
| 158 |
|
|
@@ -195,28 +192,22 @@ else:
|
|
| 195 |
|
| 196 |
def find_ytdlp_executable() -> str | None:
|
| 197 |
"""Find the yt-dlp executable, checking multiple possible locations."""
|
| 198 |
-
# First try the system PATH
|
| 199 |
ytdlp_path = shutil.which("yt-dlp")
|
| 200 |
if ytdlp_path:
|
| 201 |
return ytdlp_path
|
| 202 |
-
|
| 203 |
-
# Try common virtual environment locations
|
| 204 |
venv_paths = [
|
| 205 |
Path(sys.executable).parent / "yt-dlp",
|
| 206 |
Path(sys.executable).parent.parent / "bin" / "yt-dlp",
|
| 207 |
Path(__file__).parents[3] / ".venv" / "bin" / "yt-dlp",
|
| 208 |
Path(__file__).parents[3] / ".venv" / "Scripts" / "yt-dlp.exe", # Windows
|
| 209 |
]
|
| 210 |
-
|
| 211 |
for path in venv_paths:
|
| 212 |
if path.exists() and path.is_file():
|
| 213 |
return str(path)
|
| 214 |
-
|
| 215 |
return None
|
| 216 |
|
| 217 |
|
| 218 |
def is_youtube_url(url: str) -> bool:
|
| 219 |
-
"""Check if the URL is a YouTube URL."""
|
| 220 |
youtube_patterns = [
|
| 221 |
r"(?:https?://)?(?:www\.)?youtube\.com/watch\?v=",
|
| 222 |
r"(?:https?://)?(?:www\.)?youtu\.be/",
|
|
@@ -227,11 +218,9 @@ def is_youtube_url(url: str) -> bool:
|
|
| 227 |
|
| 228 |
|
| 229 |
def check_ytdlp_available() -> bool:
|
| 230 |
-
"""Check if yt-dlp is available on the system."""
|
| 231 |
ytdlp_path = find_ytdlp_executable()
|
| 232 |
if not ytdlp_path:
|
| 233 |
return False
|
| 234 |
-
|
| 235 |
try:
|
| 236 |
result = subprocess.run(
|
| 237 |
[ytdlp_path, "--version"],
|
|
@@ -246,69 +235,46 @@ def check_ytdlp_available() -> bool:
|
|
| 246 |
|
| 247 |
|
| 248 |
def is_live_youtube_stream(url: str) -> bool:
|
| 249 |
-
"""Check if a YouTube URL is a live stream."""
|
| 250 |
if not is_youtube_url(url):
|
| 251 |
return False
|
| 252 |
-
|
| 253 |
ytdlp_path = find_ytdlp_executable()
|
| 254 |
if not ytdlp_path:
|
| 255 |
return False
|
| 256 |
-
|
| 257 |
try:
|
| 258 |
-
# Get video info to check if it's live
|
| 259 |
cmd = [ytdlp_path, "--dump-json", "--no-playlist", "--no-warnings", url]
|
| 260 |
-
|
| 261 |
result = subprocess.run(
|
| 262 |
cmd, check=False, capture_output=True, text=True, timeout=15
|
| 263 |
)
|
| 264 |
-
|
| 265 |
if result.returncode == 0:
|
| 266 |
import json
|
| 267 |
-
|
| 268 |
video_info = json.loads(result.stdout)
|
| 269 |
return video_info.get("is_live", False)
|
| 270 |
except Exception:
|
| 271 |
pass
|
| 272 |
-
|
| 273 |
return False
|
| 274 |
|
| 275 |
|
| 276 |
def extract_youtube_stream_url(url: str, cookies_path: str | None = None) -> str | None:
|
| 277 |
-
"""Extract the actual stream URL from a YouTube URL using yt-dlp."""
|
| 278 |
ytdlp_path = find_ytdlp_executable()
|
| 279 |
if not ytdlp_path:
|
| 280 |
st.error("yt-dlp is not installed or not available in PATH.")
|
| 281 |
st.info("Please install yt-dlp: `pip install yt-dlp` or `brew install yt-dlp`")
|
| 282 |
return None
|
| 283 |
-
|
| 284 |
if not check_ytdlp_available():
|
| 285 |
st.error("yt-dlp found but not working properly.")
|
| 286 |
return None
|
| 287 |
-
|
| 288 |
try:
|
| 289 |
-
# Show progress message
|
| 290 |
with st.spinner("Extracting YouTube stream URL..."):
|
| 291 |
format_options = ["best[height<=720]", "best[height<=480]", "best", "worst"]
|
| 292 |
-
|
| 293 |
-
# First try with provided cookies if available
|
| 294 |
if cookies_path and Path(cookies_path).exists():
|
| 295 |
for fmt in format_options:
|
| 296 |
cmd = [
|
| 297 |
-
ytdlp_path,
|
| 298 |
-
"--
|
| 299 |
-
"--format",
|
| 300 |
-
fmt,
|
| 301 |
-
"--no-playlist",
|
| 302 |
-
"--no-warnings",
|
| 303 |
-
"--cookies",
|
| 304 |
-
cookies_path,
|
| 305 |
-
url,
|
| 306 |
]
|
| 307 |
-
|
| 308 |
result = subprocess.run(
|
| 309 |
cmd, check=False, capture_output=True, text=True, timeout=30
|
| 310 |
)
|
| 311 |
-
|
| 312 |
if result.returncode == 0:
|
| 313 |
stream_url = result.stdout.strip()
|
| 314 |
if stream_url and stream_url.startswith("http"):
|
|
@@ -316,14 +282,12 @@ def extract_youtube_stream_url(url: str, cookies_path: str | None = None) -> str
|
|
| 316 |
return stream_url
|
| 317 |
else:
|
| 318 |
st.warning(f"yt-dlp failed with format {fmt}: {result.stderr}")
|
| 319 |
-
|
| 320 |
except subprocess.TimeoutExpired:
|
| 321 |
st.error("Timeout while extracting YouTube stream URL. The video might be unavailable.")
|
| 322 |
except FileNotFoundError:
|
| 323 |
st.error("yt-dlp not found. Please install it: pip install yt-dlp")
|
| 324 |
except Exception as e:
|
| 325 |
st.error(f"Error extracting YouTube stream URL: {e!s}")
|
| 326 |
-
|
| 327 |
return None
|
| 328 |
|
| 329 |
|
|
@@ -386,8 +350,6 @@ def run_video_feed_detection(
|
|
| 386 |
is_youtube = isinstance(cap_source, str) and is_youtube_url(cap_source)
|
| 387 |
is_webcam = isinstance(cap_source, int)
|
| 388 |
|
| 389 |
-
# OpenCV capture
|
| 390 |
-
# On Windows, you may prefer: cv2.VideoCapture(cap_source, cv2.CAP_DSHOW)
|
| 391 |
cap = cv2.VideoCapture(cap_source)
|
| 392 |
|
| 393 |
# For webcam, set resolution & reduce buffering
|
|
@@ -396,7 +358,6 @@ def run_video_feed_detection(
|
|
| 396 |
if webcam_size:
|
| 397 |
cap.set(cv2.CAP_PROP_FRAME_WIDTH, webcam_size[0])
|
| 398 |
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, webcam_size[1])
|
| 399 |
-
# Hints for common backends
|
| 400 |
cap.set(cv2.CAP_PROP_FPS, 30)
|
| 401 |
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"MJPG"))
|
| 402 |
|
|
@@ -404,28 +365,24 @@ def run_video_feed_detection(
|
|
| 404 |
st.error("Failed to open the selected source.")
|
| 405 |
return
|
| 406 |
|
| 407 |
-
# Additional robustness for cloud deployment
|
| 408 |
try:
|
| 409 |
-
# Test if we can read at least one frame
|
| 410 |
test_ret, test_frame = cap.read()
|
| 411 |
if not test_ret or test_frame is None:
|
| 412 |
st.error("Video source is not providing frames. Please try a different stream.")
|
| 413 |
cap.release()
|
| 414 |
return
|
| 415 |
-
# Reset to beginning when it's a file
|
| 416 |
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
| 417 |
except Exception as e:
|
| 418 |
st.error(f"Error testing video source: {e}")
|
| 419 |
cap.release()
|
| 420 |
return
|
| 421 |
|
| 422 |
-
# Set buffer size for live streams to reduce latency
|
| 423 |
if is_youtube or isinstance(cap_source, str):
|
| 424 |
-
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
| 425 |
-
cap.set(cv2.CAP_PROP_FPS, 25)
|
| 426 |
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"MJPG"))
|
| 427 |
|
| 428 |
-
#
|
| 429 |
is_file = False
|
| 430 |
if isinstance(cap_source, str):
|
| 431 |
if cap_source.startswith(("http://", "https://", "rtsp://")):
|
|
@@ -445,7 +402,6 @@ def run_video_feed_detection(
|
|
| 445 |
start_t = time.time()
|
| 446 |
i = 0
|
| 447 |
last_preview_update = 0
|
| 448 |
-
# Use frame count-based preview throttling to prevent freezing
|
| 449 |
preview_update_interval = max(1, int(30 / (preview_max_fps or 10))) # Update every N frames
|
| 450 |
|
| 451 |
writer = None
|
|
@@ -460,7 +416,7 @@ def run_video_feed_detection(
|
|
| 460 |
if not is_file and preview_max_fps and preview_max_fps <= 6:
|
| 461 |
max_frame_skip = 15
|
| 462 |
else:
|
| 463 |
-
max_frame_skip = 5
|
| 464 |
|
| 465 |
progress_update_seconds = 0.0 if is_file else (
|
| 466 |
0.7 if not preview_max_fps else max(0.4, 1.0 / preview_max_fps)
|
|
@@ -469,16 +425,14 @@ def run_video_feed_detection(
|
|
| 469 |
|
| 470 |
try:
|
| 471 |
while True:
|
| 472 |
-
# Check for stop signal
|
| 473 |
if st.session_state.get("stop_processing", False):
|
| 474 |
st.info("🛑 Stopping detection...")
|
| 475 |
break
|
| 476 |
|
| 477 |
ok, frame = cap.read()
|
| 478 |
if not ok:
|
| 479 |
-
# For live streams, try to reconnect a few times before giving up
|
| 480 |
if is_youtube or not is_file:
|
| 481 |
-
if i < 10:
|
| 482 |
time.sleep(0.5)
|
| 483 |
continue
|
| 484 |
break
|
|
@@ -488,7 +442,6 @@ def run_video_feed_detection(
|
|
| 488 |
continue
|
| 489 |
|
| 490 |
inference_start = time.time()
|
| 491 |
-
|
| 492 |
if model_key == "deim" and hasattr(model, "annotate_frame_bgr"):
|
| 493 |
vis = model.annotate_frame_bgr(frame, min_confidence=conf_thr)
|
| 494 |
elif model_key == "deim":
|
|
@@ -501,9 +454,9 @@ def run_video_feed_detection(
|
|
| 501 |
_, vis = model.predict_and_visualize(
|
| 502 |
frame, min_confidence=conf_thr, show_score=True
|
| 503 |
)
|
| 504 |
-
|
| 505 |
inference_elapsed = time.time() - inference_start
|
| 506 |
|
|
|
|
| 507 |
if not is_file and frame_interval > 0.0 and inference_elapsed > frame_interval:
|
| 508 |
frames_to_drop = min(int(inference_elapsed / frame_interval) - 1, max_frame_skip)
|
| 509 |
for _ in range(frames_to_drop):
|
|
@@ -512,7 +465,7 @@ def run_video_feed_detection(
|
|
| 512 |
|
| 513 |
loop_now = time.time()
|
| 514 |
|
| 515 |
-
#
|
| 516 |
if is_file or (loop_now - last_progress_time) >= progress_update_seconds:
|
| 517 |
progress = i / total if total > 0 else 0
|
| 518 |
prog.progress(
|
|
@@ -521,24 +474,17 @@ def run_video_feed_detection(
|
|
| 521 |
)
|
| 522 |
last_progress_time = loop_now
|
| 523 |
|
| 524 |
-
#
|
| 525 |
if frame_ph is not None and (i - last_preview_update) >= preview_update_interval:
|
| 526 |
try:
|
| 527 |
-
# Convert to RGB and resize for better performance
|
| 528 |
display_frame = cv2.cvtColor(vis, cv2.COLOR_BGR2RGB)
|
| 529 |
-
|
| 530 |
-
# Resize large frames to improve performance for cloud deployment
|
| 531 |
height, width = display_frame.shape[:2]
|
| 532 |
if is_youtube or not is_file:
|
| 533 |
-
# More aggressive resizing for live sources (incl. webcam) to prevent freezing
|
| 534 |
if width > 720:
|
| 535 |
scale = 720 / width
|
| 536 |
new_width = 720
|
| 537 |
new_height = int(height * scale)
|
| 538 |
-
display_frame = cv2.resize(
|
| 539 |
-
display_frame, (new_width, new_height),
|
| 540 |
-
interpolation=cv2.INTER_LINEAR
|
| 541 |
-
)
|
| 542 |
elif width > 1280:
|
| 543 |
scale = 1280 / width
|
| 544 |
new_width = 1280
|
|
@@ -552,12 +498,10 @@ def run_video_feed_detection(
|
|
| 552 |
channels="RGB",
|
| 553 |
)
|
| 554 |
last_preview_update = i
|
| 555 |
-
|
| 556 |
except Exception as e:
|
| 557 |
-
|
| 558 |
-
if i % 30 == 0: # Only show error every 30 frames to avoid spam
|
| 559 |
st.warning(f"Display update failed: {e}")
|
| 560 |
-
last_preview_update = i
|
| 561 |
|
| 562 |
if writer is not None:
|
| 563 |
writer.write(vis)
|
|
@@ -569,7 +513,6 @@ def run_video_feed_detection(
|
|
| 569 |
else:
|
| 570 |
elapsed = time.time() - start_t
|
| 571 |
stream_type = "YouTube" if is_youtube else ("Live" if not is_file else "File")
|
| 572 |
-
|
| 573 |
if max_seconds is None:
|
| 574 |
prog.progress(0.0, text=f"{stream_type}… {int(elapsed)}s (unlimited)")
|
| 575 |
else:
|
|
@@ -580,18 +523,15 @@ def run_video_feed_detection(
|
|
| 580 |
if elapsed >= max_seconds:
|
| 581 |
return
|
| 582 |
|
| 583 |
-
# For live streams, add adaptive delay based on processing speed
|
| 584 |
if not is_file:
|
| 585 |
elapsed = time.time() - start_t
|
| 586 |
if elapsed > 0:
|
| 587 |
current_fps = i / elapsed
|
| 588 |
-
# Target 10–15 FPS for live streams to prevent freezing
|
| 589 |
if current_fps > 15:
|
| 590 |
-
time.sleep(0.1)
|
| 591 |
elif current_fps < 8:
|
| 592 |
-
time.sleep(0.02)
|
| 593 |
|
| 594 |
-
# Clear GPU memory periodically for long-running streams
|
| 595 |
if i % 100 == 0 and is_youtube:
|
| 596 |
if torch.cuda.is_available():
|
| 597 |
torch.cuda.empty_cache()
|
|
@@ -599,14 +539,11 @@ def run_video_feed_detection(
|
|
| 599 |
except Exception as e:
|
| 600 |
st.error(f"Error during processing: {e!s}")
|
| 601 |
finally:
|
| 602 |
-
# Ensure proper cleanup
|
| 603 |
cap.release()
|
| 604 |
if writer is not None:
|
| 605 |
writer.release()
|
| 606 |
-
# Clear GPU memory after processing
|
| 607 |
if torch.cuda.is_available():
|
| 608 |
torch.cuda.empty_cache()
|
| 609 |
-
# Reset stop flag
|
| 610 |
st.session_state.stop_processing = False
|
| 611 |
|
| 612 |
stream_type = "YouTube" if is_youtube else ("Live" if not is_file else "File")
|
|
@@ -622,15 +559,76 @@ def run_video_feed_detection(
|
|
| 622 |
)
|
| 623 |
|
| 624 |
|
| 625 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 626 |
if stop_detection:
|
| 627 |
st.session_state.stop_processing = True
|
| 628 |
-
st.info("🛑 Stop signal sent.
|
| 629 |
|
| 630 |
-
if
|
| 631 |
-
#
|
|
|
|
|
|
|
| 632 |
st.session_state.stop_processing = False
|
|
|
|
|
|
|
| 633 |
|
|
|
|
|
|
|
| 634 |
src = resolve_video_source(src_choice, cookies_path, webcam_index=webcam_index)
|
| 635 |
# Note: 0 (webcam) is a valid source; don't treat it as falsey
|
| 636 |
if src is None and src != 0:
|
|
@@ -646,6 +644,4 @@ if run_detection:
|
|
| 646 |
webcam_size=webcam_size if src_choice == "Webcam" else None,
|
| 647 |
)
|
| 648 |
else:
|
| 649 |
-
st.info(
|
| 650 |
-
"Pick a video source from the sidebar (including **Webcam** or YouTube) and click **Run Detection**."
|
| 651 |
-
)
|
|
|
|
| 16 |
from utils.model_manager import get_model_manager, load_model
|
| 17 |
|
| 18 |
|
|
|
|
| 19 |
def _image_to_data_url(path: str) -> str:
|
| 20 |
p = Path(path)
|
| 21 |
if not p.is_absolute():
|
|
|
|
| 39 |
|
| 40 |
# =============== Sidebar: custom menu + cards ===============
|
| 41 |
with st.sidebar:
|
|
|
|
|
|
|
| 42 |
logo_data_url = _image_to_data_url("../resources/images/lucid_insights_logo.png")
|
| 43 |
st.markdown(
|
| 44 |
f"""
|
|
|
|
| 56 |
st.markdown("---")
|
| 57 |
st.page_link("pages/task_drone.py", label="Task Drone")
|
| 58 |
st.page_link("pages/task_satellite.py", label="Task Satellite")
|
|
|
|
|
|
|
| 59 |
st.markdown("---")
|
| 60 |
|
| 61 |
# ---------- Sidebar: Video feed dropdown ----------
|
|
|
|
| 111 |
max_seconds = None # Always unlimited mode
|
| 112 |
st.info("⚠️ Video Stream is running in unlimited mode — use Stop to end")
|
| 113 |
|
| 114 |
+
# == Controls: Run / Stop / Play Only ==
|
| 115 |
+
c1, c2, c3 = st.columns(3)
|
| 116 |
+
with c1:
|
| 117 |
+
play_only = st.button("▶️ Play", use_container_width=True, help="Play raw video without detection")
|
| 118 |
+
with c2:
|
| 119 |
+
run_detection = st.button("🔎 Detect", use_container_width=True)
|
| 120 |
+
with c3:
|
| 121 |
+
stop_detection = st.button("🛑 Stop", use_container_width=True, help="Stop current stream")
|
| 122 |
+
|
| 123 |
+
st.markdown("---")
|
| 124 |
|
| 125 |
# Parameters card (shared)
|
| 126 |
+
st.header("Parameters")
|
| 127 |
confidence_threshold = st.slider("Minimum Confidence", 0.0, 1.0, 0.5, 0.01)
|
| 128 |
|
| 129 |
device = model_manager.device
|
|
|
|
| 143 |
default_stride,
|
| 144 |
help="Higher values skip frames to keep inference responsive on slower hardware.",
|
| 145 |
)
|
| 146 |
+
st.markdown("---")
|
| 147 |
# Render model selection UI
|
| 148 |
model_label, model_key = model_manager.render_model_selection(
|
| 149 |
key_prefix="signal_watch"
|
| 150 |
)
|
| 151 |
|
| 152 |
+
st.markdown("---")
|
| 153 |
# Render device information
|
| 154 |
model_manager.render_device_info()
|
| 155 |
|
|
|
|
| 192 |
|
| 193 |
def find_ytdlp_executable() -> str | None:
|
| 194 |
"""Find the yt-dlp executable, checking multiple possible locations."""
|
|
|
|
| 195 |
ytdlp_path = shutil.which("yt-dlp")
|
| 196 |
if ytdlp_path:
|
| 197 |
return ytdlp_path
|
|
|
|
|
|
|
| 198 |
venv_paths = [
|
| 199 |
Path(sys.executable).parent / "yt-dlp",
|
| 200 |
Path(sys.executable).parent.parent / "bin" / "yt-dlp",
|
| 201 |
Path(__file__).parents[3] / ".venv" / "bin" / "yt-dlp",
|
| 202 |
Path(__file__).parents[3] / ".venv" / "Scripts" / "yt-dlp.exe", # Windows
|
| 203 |
]
|
|
|
|
| 204 |
for path in venv_paths:
|
| 205 |
if path.exists() and path.is_file():
|
| 206 |
return str(path)
|
|
|
|
| 207 |
return None
|
| 208 |
|
| 209 |
|
| 210 |
def is_youtube_url(url: str) -> bool:
|
|
|
|
| 211 |
youtube_patterns = [
|
| 212 |
r"(?:https?://)?(?:www\.)?youtube\.com/watch\?v=",
|
| 213 |
r"(?:https?://)?(?:www\.)?youtu\.be/",
|
|
|
|
| 218 |
|
| 219 |
|
| 220 |
def check_ytdlp_available() -> bool:
|
|
|
|
| 221 |
ytdlp_path = find_ytdlp_executable()
|
| 222 |
if not ytdlp_path:
|
| 223 |
return False
|
|
|
|
| 224 |
try:
|
| 225 |
result = subprocess.run(
|
| 226 |
[ytdlp_path, "--version"],
|
|
|
|
| 235 |
|
| 236 |
|
| 237 |
def is_live_youtube_stream(url: str) -> bool:
|
|
|
|
| 238 |
if not is_youtube_url(url):
|
| 239 |
return False
|
|
|
|
| 240 |
ytdlp_path = find_ytdlp_executable()
|
| 241 |
if not ytdlp_path:
|
| 242 |
return False
|
|
|
|
| 243 |
try:
|
|
|
|
| 244 |
cmd = [ytdlp_path, "--dump-json", "--no-playlist", "--no-warnings", url]
|
|
|
|
| 245 |
result = subprocess.run(
|
| 246 |
cmd, check=False, capture_output=True, text=True, timeout=15
|
| 247 |
)
|
|
|
|
| 248 |
if result.returncode == 0:
|
| 249 |
import json
|
|
|
|
| 250 |
video_info = json.loads(result.stdout)
|
| 251 |
return video_info.get("is_live", False)
|
| 252 |
except Exception:
|
| 253 |
pass
|
|
|
|
| 254 |
return False
|
| 255 |
|
| 256 |
|
| 257 |
def extract_youtube_stream_url(url: str, cookies_path: str | None = None) -> str | None:
|
|
|
|
| 258 |
ytdlp_path = find_ytdlp_executable()
|
| 259 |
if not ytdlp_path:
|
| 260 |
st.error("yt-dlp is not installed or not available in PATH.")
|
| 261 |
st.info("Please install yt-dlp: `pip install yt-dlp` or `brew install yt-dlp`")
|
| 262 |
return None
|
|
|
|
| 263 |
if not check_ytdlp_available():
|
| 264 |
st.error("yt-dlp found but not working properly.")
|
| 265 |
return None
|
|
|
|
| 266 |
try:
|
|
|
|
| 267 |
with st.spinner("Extracting YouTube stream URL..."):
|
| 268 |
format_options = ["best[height<=720]", "best[height<=480]", "best", "worst"]
|
|
|
|
|
|
|
| 269 |
if cookies_path and Path(cookies_path).exists():
|
| 270 |
for fmt in format_options:
|
| 271 |
cmd = [
|
| 272 |
+
ytdlp_path, "--get-url", "--format", fmt, "--no-playlist",
|
| 273 |
+
"--no-warnings", "--cookies", cookies_path, url,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
]
|
|
|
|
| 275 |
result = subprocess.run(
|
| 276 |
cmd, check=False, capture_output=True, text=True, timeout=30
|
| 277 |
)
|
|
|
|
| 278 |
if result.returncode == 0:
|
| 279 |
stream_url = result.stdout.strip()
|
| 280 |
if stream_url and stream_url.startswith("http"):
|
|
|
|
| 282 |
return stream_url
|
| 283 |
else:
|
| 284 |
st.warning(f"yt-dlp failed with format {fmt}: {result.stderr}")
|
|
|
|
| 285 |
except subprocess.TimeoutExpired:
|
| 286 |
st.error("Timeout while extracting YouTube stream URL. The video might be unavailable.")
|
| 287 |
except FileNotFoundError:
|
| 288 |
st.error("yt-dlp not found. Please install it: pip install yt-dlp")
|
| 289 |
except Exception as e:
|
| 290 |
st.error(f"Error extracting YouTube stream URL: {e!s}")
|
|
|
|
| 291 |
return None
|
| 292 |
|
| 293 |
|
|
|
|
| 350 |
is_youtube = isinstance(cap_source, str) and is_youtube_url(cap_source)
|
| 351 |
is_webcam = isinstance(cap_source, int)
|
| 352 |
|
|
|
|
|
|
|
| 353 |
cap = cv2.VideoCapture(cap_source)
|
| 354 |
|
| 355 |
# For webcam, set resolution & reduce buffering
|
|
|
|
| 358 |
if webcam_size:
|
| 359 |
cap.set(cv2.CAP_PROP_FRAME_WIDTH, webcam_size[0])
|
| 360 |
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, webcam_size[1])
|
|
|
|
| 361 |
cap.set(cv2.CAP_PROP_FPS, 30)
|
| 362 |
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"MJPG"))
|
| 363 |
|
|
|
|
| 365 |
st.error("Failed to open the selected source.")
|
| 366 |
return
|
| 367 |
|
|
|
|
| 368 |
try:
|
|
|
|
| 369 |
test_ret, test_frame = cap.read()
|
| 370 |
if not test_ret or test_frame is None:
|
| 371 |
st.error("Video source is not providing frames. Please try a different stream.")
|
| 372 |
cap.release()
|
| 373 |
return
|
|
|
|
| 374 |
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
| 375 |
except Exception as e:
|
| 376 |
st.error(f"Error testing video source: {e}")
|
| 377 |
cap.release()
|
| 378 |
return
|
| 379 |
|
|
|
|
| 380 |
if is_youtube or isinstance(cap_source, str):
|
| 381 |
+
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
| 382 |
+
cap.set(cv2.CAP_PROP_FPS, 25)
|
| 383 |
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"MJPG"))
|
| 384 |
|
| 385 |
+
# Determine if local file
|
| 386 |
is_file = False
|
| 387 |
if isinstance(cap_source, str):
|
| 388 |
if cap_source.startswith(("http://", "https://", "rtsp://")):
|
|
|
|
| 402 |
start_t = time.time()
|
| 403 |
i = 0
|
| 404 |
last_preview_update = 0
|
|
|
|
| 405 |
preview_update_interval = max(1, int(30 / (preview_max_fps or 10))) # Update every N frames
|
| 406 |
|
| 407 |
writer = None
|
|
|
|
| 416 |
if not is_file and preview_max_fps and preview_max_fps <= 6:
|
| 417 |
max_frame_skip = 15
|
| 418 |
else:
|
| 419 |
+
max_frame_skip = 5
|
| 420 |
|
| 421 |
progress_update_seconds = 0.0 if is_file else (
|
| 422 |
0.7 if not preview_max_fps else max(0.4, 1.0 / preview_max_fps)
|
|
|
|
| 425 |
|
| 426 |
try:
|
| 427 |
while True:
|
|
|
|
| 428 |
if st.session_state.get("stop_processing", False):
|
| 429 |
st.info("🛑 Stopping detection...")
|
| 430 |
break
|
| 431 |
|
| 432 |
ok, frame = cap.read()
|
| 433 |
if not ok:
|
|
|
|
| 434 |
if is_youtube or not is_file:
|
| 435 |
+
if i < 10:
|
| 436 |
time.sleep(0.5)
|
| 437 |
continue
|
| 438 |
break
|
|
|
|
| 442 |
continue
|
| 443 |
|
| 444 |
inference_start = time.time()
|
|
|
|
| 445 |
if model_key == "deim" and hasattr(model, "annotate_frame_bgr"):
|
| 446 |
vis = model.annotate_frame_bgr(frame, min_confidence=conf_thr)
|
| 447 |
elif model_key == "deim":
|
|
|
|
| 454 |
_, vis = model.predict_and_visualize(
|
| 455 |
frame, min_confidence=conf_thr, show_score=True
|
| 456 |
)
|
|
|
|
| 457 |
inference_elapsed = time.time() - inference_start
|
| 458 |
|
| 459 |
+
# Drop buffered frames if we're slower than source FPS
|
| 460 |
if not is_file and frame_interval > 0.0 and inference_elapsed > frame_interval:
|
| 461 |
frames_to_drop = min(int(inference_elapsed / frame_interval) - 1, max_frame_skip)
|
| 462 |
for _ in range(frames_to_drop):
|
|
|
|
| 465 |
|
| 466 |
loop_now = time.time()
|
| 467 |
|
| 468 |
+
# Progress
|
| 469 |
if is_file or (loop_now - last_progress_time) >= progress_update_seconds:
|
| 470 |
progress = i / total if total > 0 else 0
|
| 471 |
prog.progress(
|
|
|
|
| 474 |
)
|
| 475 |
last_progress_time = loop_now
|
| 476 |
|
| 477 |
+
# Preview (throttled)
|
| 478 |
if frame_ph is not None and (i - last_preview_update) >= preview_update_interval:
|
| 479 |
try:
|
|
|
|
| 480 |
display_frame = cv2.cvtColor(vis, cv2.COLOR_BGR2RGB)
|
|
|
|
|
|
|
| 481 |
height, width = display_frame.shape[:2]
|
| 482 |
if is_youtube or not is_file:
|
|
|
|
| 483 |
if width > 720:
|
| 484 |
scale = 720 / width
|
| 485 |
new_width = 720
|
| 486 |
new_height = int(height * scale)
|
| 487 |
+
display_frame = cv2.resize(display_frame, (new_width, new_height), interpolation=cv2.INTER_LINEAR)
|
|
|
|
|
|
|
|
|
|
| 488 |
elif width > 1280:
|
| 489 |
scale = 1280 / width
|
| 490 |
new_width = 1280
|
|
|
|
| 498 |
channels="RGB",
|
| 499 |
)
|
| 500 |
last_preview_update = i
|
|
|
|
| 501 |
except Exception as e:
|
| 502 |
+
if i % 30 == 0:
|
|
|
|
| 503 |
st.warning(f"Display update failed: {e}")
|
| 504 |
+
last_preview_update = i
|
| 505 |
|
| 506 |
if writer is not None:
|
| 507 |
writer.write(vis)
|
|
|
|
| 513 |
else:
|
| 514 |
elapsed = time.time() - start_t
|
| 515 |
stream_type = "YouTube" if is_youtube else ("Live" if not is_file else "File")
|
|
|
|
| 516 |
if max_seconds is None:
|
| 517 |
prog.progress(0.0, text=f"{stream_type}… {int(elapsed)}s (unlimited)")
|
| 518 |
else:
|
|
|
|
| 523 |
if elapsed >= max_seconds:
|
| 524 |
return
|
| 525 |
|
|
|
|
| 526 |
if not is_file:
|
| 527 |
elapsed = time.time() - start_t
|
| 528 |
if elapsed > 0:
|
| 529 |
current_fps = i / elapsed
|
|
|
|
| 530 |
if current_fps > 15:
|
| 531 |
+
time.sleep(0.1)
|
| 532 |
elif current_fps < 8:
|
| 533 |
+
time.sleep(0.02)
|
| 534 |
|
|
|
|
| 535 |
if i % 100 == 0 and is_youtube:
|
| 536 |
if torch.cuda.is_available():
|
| 537 |
torch.cuda.empty_cache()
|
|
|
|
| 539 |
except Exception as e:
|
| 540 |
st.error(f"Error during processing: {e!s}")
|
| 541 |
finally:
|
|
|
|
| 542 |
cap.release()
|
| 543 |
if writer is not None:
|
| 544 |
writer.release()
|
|
|
|
| 545 |
if torch.cuda.is_available():
|
| 546 |
torch.cuda.empty_cache()
|
|
|
|
| 547 |
st.session_state.stop_processing = False
|
| 548 |
|
| 549 |
stream_type = "YouTube" if is_youtube else ("Live" if not is_file else "File")
|
|
|
|
| 559 |
)
|
| 560 |
|
| 561 |
|
| 562 |
+
# === NEW: play-only (no detection) ===
|
| 563 |
+
def play_video_only(src_choice: str, webcam_index: int, webcam_size: tuple[int, int]):
|
| 564 |
+
"""
|
| 565 |
+
Play raw video without running any model.
|
| 566 |
+
- For YouTube/live or local files: st.video(...)
|
| 567 |
+
- For Webcam: OpenCV capture loop until Stop pressed.
|
| 568 |
+
"""
|
| 569 |
+
# YouTube/live or local file -> use st.video for simplest playback
|
| 570 |
+
if src_choice in youtube_urls:
|
| 571 |
+
st.info("▶ Playing YouTube stream (no detection). Use ⏹️ Stop to end.")
|
| 572 |
+
st.video(youtube_urls[src_choice])
|
| 573 |
+
return
|
| 574 |
+
if src_choice in video_map:
|
| 575 |
+
st.info("▶ Playing local file (no detection). Use ⏹️ Stop to end.")
|
| 576 |
+
st.video(video_map[src_choice])
|
| 577 |
+
return
|
| 578 |
+
|
| 579 |
+
# Webcam fallback -> simple OpenCV loop
|
| 580 |
+
if src_choice == "Webcam":
|
| 581 |
+
st.info("▶ Playing webcam (no detection). Use ⏹️ Stop to end.")
|
| 582 |
+
ph = st.empty()
|
| 583 |
+
cap = cv2.VideoCapture(int(webcam_index))
|
| 584 |
+
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
| 585 |
+
cap.set(cv2.CAP_PROP_FRAME_WIDTH, webcam_size[0])
|
| 586 |
+
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, webcam_size[1])
|
| 587 |
+
cap.set(cv2.CAP_PROP_FPS, 30)
|
| 588 |
+
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"MJPG"))
|
| 589 |
+
if not cap.isOpened():
|
| 590 |
+
st.error("Failed to open webcam.")
|
| 591 |
+
return
|
| 592 |
+
try:
|
| 593 |
+
while True:
|
| 594 |
+
if st.session_state.get("stop_processing", False):
|
| 595 |
+
st.info("🛑 Stopping playback...")
|
| 596 |
+
break
|
| 597 |
+
ok, frame = cap.read()
|
| 598 |
+
if not ok:
|
| 599 |
+
time.sleep(0.05)
|
| 600 |
+
continue
|
| 601 |
+
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
| 602 |
+
ph.image(frame_rgb, channels="RGB", use_container_width=True, output_format="JPEG")
|
| 603 |
+
time.sleep(0.01)
|
| 604 |
+
except Exception as e:
|
| 605 |
+
st.error(f"Playback error: {e!s}")
|
| 606 |
+
finally:
|
| 607 |
+
cap.release()
|
| 608 |
+
st.session_state.stop_processing = False
|
| 609 |
+
st.success("Stopped webcam playback.")
|
| 610 |
+
else:
|
| 611 |
+
st.warning("Please choose a valid source to play.")
|
| 612 |
+
|
| 613 |
+
|
| 614 |
+
# Stop/Run/Play controls
|
| 615 |
+
if 'stop_processing' not in st.session_state:
|
| 616 |
+
st.session_state.stop_processing = False
|
| 617 |
+
|
| 618 |
if stop_detection:
|
| 619 |
st.session_state.stop_processing = True
|
| 620 |
+
st.info("🛑 Stop signal sent. Playback/detection will stop after current frame.")
|
| 621 |
|
| 622 |
+
if 'play_only' not in locals():
|
| 623 |
+
play_only = False # safety for environments that re-run blocks
|
| 624 |
+
|
| 625 |
+
if play_only:
|
| 626 |
st.session_state.stop_processing = False
|
| 627 |
+
# Use *display* URLs for simple playback (no extraction/model)
|
| 628 |
+
play_video_only(src_choice, webcam_index=webcam_index, webcam_size=webcam_size)
|
| 629 |
|
| 630 |
+
elif 'run_detection' in locals() and run_detection:
|
| 631 |
+
st.session_state.stop_processing = False
|
| 632 |
src = resolve_video_source(src_choice, cookies_path, webcam_index=webcam_index)
|
| 633 |
# Note: 0 (webcam) is a valid source; don't treat it as falsey
|
| 634 |
if src is None and src != 0:
|
|
|
|
| 644 |
webcam_size=webcam_size if src_choice == "Webcam" else None,
|
| 645 |
)
|
| 646 |
else:
|
| 647 |
+
st.info("Pick a source from the sidebar and click **🎥 Run** for detections or **▶ Play Only** for raw playback.")
|
|
|
|
|
|
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
|
| 18 |
-
.youtube.com TRUE / TRUE
|
| 19 |
-
.youtube.com TRUE / TRUE
|
| 20 |
.youtube.com TRUE / TRUE 0 YSC JNOzy0pmT_0
|
| 21 |
-
.youtube.com TRUE / TRUE
|
| 22 |
-
.youtube.com TRUE / TRUE
|
| 23 |
.youtube.com TRUE / TRUE 1777706424 __Secure-ROLLOUT_TOKEN CMDh2N6P_q_XIBDpoLH5yLyQAxjNjY3kuNWQAw%3D%3D
|
| 24 |
-
.youtube.com TRUE / TRUE
|
| 25 |
-
.youtube.com TRUE / TRUE
|
| 26 |
-
.youtube.com TRUE /tv TRUE
|
|
|
|
| 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 1793708909 SIDCC AKEyXzVbBAWpzyxngGYEjLYfhkCBFrkwJ6n7kwoUUOKXL7xvBcjg97pGTxuIXPkvu3QNrmzPuuc
|
| 18 |
+
.youtube.com TRUE / TRUE 1793708909 __Secure-1PSIDCC AKEyXzUh-hV97EvC1uklo2cTzWzL5ckOLmqxL8TXM1gyzisAEwG0OVSD01JyDncI8DQuCpHomQ
|
| 19 |
+
.youtube.com TRUE / TRUE 1793708909 __Secure-3PSIDCC AKEyXzUcK9Z5Rr5cU66egaTKxwLaUIsHQs_6__IEWxDGFhtEtUOvZktYMtlCxq-rul6__UsHYyE
|
| 20 |
.youtube.com TRUE / TRUE 0 YSC JNOzy0pmT_0
|
| 21 |
+
.youtube.com TRUE / TRUE 1777724909 VISITOR_INFO1_LIVE y1BPxdbbDNs
|
| 22 |
+
.youtube.com TRUE / TRUE 1777724909 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 1825244909 __Secure-YT_TVFAS t=489249&s=2
|
| 25 |
+
.youtube.com TRUE / TRUE 1777724909 DEVICE_INFO ChxOelUyTlRBNE56VXlNek0xTnpBek16QTVOdz09EO2/osgGGIWu8scG
|
| 26 |
+
.youtube.com TRUE /tv TRUE 1795004909 __Secure-YT_DERP CPjNrY5_
|