Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Upload LSCC-Demo-HMI-Human-Machine-Interface-Edge-Vision-Engine with upload_repo.py
Browse files- README.md +25 -14
- app.py +31 -29
- install_eve.py +2 -5
- shared/env_utils.py +1 -3
- shared/eve_messages.py +0 -1
- shared/eve_worker_pool.py +29 -39
- shared/eve_wrapper.py +1 -4
- shared/face_id_tab.py +19 -29
- shared/frame_utils.py +1 -3
- shared/live_inference.py +2 -2
- shared/live_stream_manager.py +6 -13
- shared/log_utils.py +4 -2
- shared/memory_monitor.py +4 -8
- shared/video_file_server.py +4 -2
- shared/video_processing.py +9 -10
README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
|
|
|
|
| 4 |
emoji: 🏆
|
| 5 |
colorFrom: yellow
|
| 6 |
colorTo: gray
|
|
@@ -8,18 +9,24 @@ sdk: docker
|
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
## About
|
| 25 |
|
|
@@ -28,3 +35,7 @@ This demo is maintained by **Lattice Semiconductor** (LatticeSemi).
|
|
| 28 |
## License
|
| 29 |
|
| 30 |
Proprietary - Lattice Semiconductor Corporation. All rights reserved.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Lattice sensAI Edge Vision Engine SDK
|
| 3 |
+
slug: HMI-Human-Machine-Interface-Edge-Vision-Engine
|
| 4 |
+
short_description: Best in class human sensing SDK for far edge
|
| 5 |
emoji: 🏆
|
| 6 |
colorFrom: yellow
|
| 7 |
colorTo: gray
|
|
|
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
+
The Lattice sensAI Edge Vision Engine SDK (or simply EVE!) provides an Extern-C interface and ready to use Python bindings to access retrieve the following from a camera image:
|
| 13 |
+
|
| 14 |
+
| Human Sensing Attribute | Description | Can be visualized in 🤗 space |
|
| 15 |
+
| --- | --- | :---: |
|
| 16 |
+
| 3D Head Pose | x, y, z, roll, pitch and yaw of user(s) | yes |
|
| 17 |
+
| Face Landmarks | 23 or 68 face landmarks | yes |
|
| 18 |
+
| Fatigue | Karolinska Sleepiness score | |
|
| 19 |
+
| Face ID | Provides identification of detected user (if user was registered) | yes |
|
| 20 |
+
| Gaze | Gaze vectors of main user | |
|
| 21 |
+
| ROI Selection | Indicates if gaze (or projection of face) of main user intersects custom ROI(s) | |
|
| 22 |
+
| Visual Speech Detection | Indicates if main user is speaking | |
|
| 23 |
+
| Personal protective equipment detection | Indicates if protective eyewear, hat and gloves are worn for detected user(s) | |
|
| 24 |
+
| Person Detection | Person bounding box(es) | yes |
|
| 25 |
+
| Depth | Depth of user(s) up to 5m | |
|
| 26 |
+
| Object Detection | Bounding box(es) and classification of 90 objects | |
|
| 27 |
+
|
| 28 |
+
Our models have a low computation footprint and are ideal for <.5 TOPS devices like FPGAs, small NPUs and SOCs.
|
| 29 |
+
EVE SDK packages are available for Windows, Linux and RPI.
|
| 30 |
|
| 31 |
## About
|
| 32 |
|
|
|
|
| 35 |
## License
|
| 36 |
|
| 37 |
Proprietary - Lattice Semiconductor Corporation. All rights reserved.
|
| 38 |
+
|
| 39 |
+
## Need a Longer or Commercial License?
|
| 40 |
+
|
| 41 |
+
- **Email**: senseaiAccessrequest@latticesemi.com
|
app.py
CHANGED
|
@@ -15,9 +15,8 @@ sys.path.insert(0, str(Path(__file__).resolve().parent / "shared"))
|
|
| 15 |
from env_utils import load_dotenv_if_present, require_secrets
|
| 16 |
from eve_messages import FeatureFlags
|
| 17 |
from eve_worker_pool import EveWorkerPool
|
| 18 |
-
from frame_utils import draw_countdown_banner, draw_overlay, draw_session_timer
|
| 19 |
-
from video_file_server import VideoFileServer
|
| 20 |
from face_id_tab import FaceEntry, FaceIdTab
|
|
|
|
| 21 |
from live_inference import (
|
| 22 |
RtcConfigProvider,
|
| 23 |
build_webrtc_stream,
|
|
@@ -25,12 +24,14 @@ from live_inference import (
|
|
| 25 |
)
|
| 26 |
from live_stream_manager import LiveStreamManager
|
| 27 |
from log_utils import setup_logger
|
|
|
|
| 28 |
from video_processing import (
|
| 29 |
VideoLimits,
|
| 30 |
build_video_constraints_accordion,
|
| 31 |
get_example_videos,
|
| 32 |
wire_video_upload,
|
| 33 |
)
|
|
|
|
| 34 |
# --- Handlers ---
|
| 35 |
|
| 36 |
|
|
@@ -124,9 +125,7 @@ def run_eve_inference(
|
|
| 124 |
total_frames=total_frames,
|
| 125 |
progress=progress,
|
| 126 |
)
|
| 127 |
-
logger.info(
|
| 128 |
-
f"[{session}] run_eve_inference: done, {frames_processed} frames processed"
|
| 129 |
-
)
|
| 130 |
finally:
|
| 131 |
pool.release(worker)
|
| 132 |
logger.info(f"[{session}] run_eve_inference: worker {worker.worker_id} released")
|
|
@@ -231,12 +230,12 @@ def _build_video_processing_tab() -> tuple[
|
|
| 231 |
Tuple of (video_tab, input_video, output_video, cb_face, cb_person,
|
| 232 |
cb_face_id, cb_hand_gesture, process_btn, example_dataset).
|
| 233 |
"""
|
| 234 |
-
with gr.TabItem("
|
| 235 |
with gr.Accordion("Instructions", open=False):
|
| 236 |
gr.Markdown(
|
| 237 |
"1. Select the features that will be processed on the video\n"
|
| 238 |
"2. Select a video (or upload your own in the Input Video frame)\n"
|
| 239 |
-
"3. Press the **Process Video** button\n"
|
| 240 |
"Once the video has been processed, you can play the video in the "
|
| 241 |
"Output Video frame"
|
| 242 |
)
|
|
@@ -304,7 +303,6 @@ def _build_live_tab(
|
|
| 304 |
return tab, webrtc_stream, cb_face, cb_person, cb_face_id, cb_hand_gesture
|
| 305 |
|
| 306 |
|
| 307 |
-
|
| 308 |
def _process_live_frame(
|
| 309 |
frame: np.ndarray,
|
| 310 |
face_detection: bool,
|
|
@@ -341,12 +339,8 @@ def _process_live_frame(
|
|
| 341 |
mins, secs = divmod(int(eta), 60)
|
| 342 |
eta_text = f"\nest. wait ~{mins}:{secs:02d}"
|
| 343 |
if total > 1:
|
| 344 |
-
return draw_overlay(
|
| 345 |
-
|
| 346 |
-
)
|
| 347 |
-
return draw_overlay(
|
| 348 |
-
frame, f"Waiting for available worker...{eta_text}"
|
| 349 |
-
)
|
| 350 |
# Terminal reason (session expired, pressure timeout, etc.)
|
| 351 |
# — close the WebRTC stream so the UI resets to "Start Inference".
|
| 352 |
from fastrtc import CloseStream
|
|
@@ -401,7 +395,7 @@ if __name__ == "__main__":
|
|
| 401 |
max_workers = int(os.environ.get("MAX_WORKERS", os.cpu_count()))
|
| 402 |
max_ram_gb = float(os.environ.get("MAX_RAM_GB", 32))
|
| 403 |
pool = EveWorkerPool(max_workers=max_workers, max_ram_gb=max_ram_gb, ram_headroom_gb=2.0)
|
| 404 |
-
stream_manager = LiveStreamManager(pool, session_lifetime_seconds=60*4)
|
| 405 |
|
| 406 |
# Separate HTTP server for video output — bypasses Chrome's per-origin
|
| 407 |
# connection limit that blocks /file= requests while SSE connections are open.
|
|
@@ -427,17 +421,34 @@ if __name__ == "__main__":
|
|
| 427 |
)
|
| 428 |
|
| 429 |
with gr.Blocks(title="Eve HMI Demo") as demo:
|
| 430 |
-
gr.Markdown("# Edge Vision Engine
|
| 431 |
gr.Markdown(
|
| 432 |
-
"
|
| 433 |
-
"
|
| 434 |
-
"
|
| 435 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 436 |
)
|
| 437 |
|
| 438 |
session_registry = gr.State(value={})
|
| 439 |
|
| 440 |
with gr.Tabs():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
(
|
| 442 |
video_tab,
|
| 443 |
input_video,
|
|
@@ -452,15 +463,6 @@ if __name__ == "__main__":
|
|
| 452 |
|
| 453 |
face_id_tab.build()
|
| 454 |
|
| 455 |
-
(
|
| 456 |
-
_live_tab,
|
| 457 |
-
webrtc_stream,
|
| 458 |
-
live_cb_face,
|
| 459 |
-
live_cb_person,
|
| 460 |
-
live_cb_face_id,
|
| 461 |
-
live_cb_hand_gesture,
|
| 462 |
-
) = _build_live_tab(rtc_config_provider.get(), face_id_tab)
|
| 463 |
-
|
| 464 |
# --- Video Processing wiring ---
|
| 465 |
|
| 466 |
feature_controls = [
|
|
|
|
| 15 |
from env_utils import load_dotenv_if_present, require_secrets
|
| 16 |
from eve_messages import FeatureFlags
|
| 17 |
from eve_worker_pool import EveWorkerPool
|
|
|
|
|
|
|
| 18 |
from face_id_tab import FaceEntry, FaceIdTab
|
| 19 |
+
from frame_utils import draw_countdown_banner, draw_overlay, draw_session_timer
|
| 20 |
from live_inference import (
|
| 21 |
RtcConfigProvider,
|
| 22 |
build_webrtc_stream,
|
|
|
|
| 24 |
)
|
| 25 |
from live_stream_manager import LiveStreamManager
|
| 26 |
from log_utils import setup_logger
|
| 27 |
+
from video_file_server import VideoFileServer
|
| 28 |
from video_processing import (
|
| 29 |
VideoLimits,
|
| 30 |
build_video_constraints_accordion,
|
| 31 |
get_example_videos,
|
| 32 |
wire_video_upload,
|
| 33 |
)
|
| 34 |
+
|
| 35 |
# --- Handlers ---
|
| 36 |
|
| 37 |
|
|
|
|
| 125 |
total_frames=total_frames,
|
| 126 |
progress=progress,
|
| 127 |
)
|
| 128 |
+
logger.info(f"[{session}] run_eve_inference: done, {frames_processed} frames processed")
|
|
|
|
|
|
|
| 129 |
finally:
|
| 130 |
pool.release(worker)
|
| 131 |
logger.info(f"[{session}] run_eve_inference: worker {worker.worker_id} released")
|
|
|
|
| 230 |
Tuple of (video_tab, input_video, output_video, cb_face, cb_person,
|
| 231 |
cb_face_id, cb_hand_gesture, process_btn, example_dataset).
|
| 232 |
"""
|
| 233 |
+
with gr.TabItem("Offline Inference") as video_tab:
|
| 234 |
with gr.Accordion("Instructions", open=False):
|
| 235 |
gr.Markdown(
|
| 236 |
"1. Select the features that will be processed on the video\n"
|
| 237 |
"2. Select a video (or upload your own in the Input Video frame)\n"
|
| 238 |
+
"3. Press the **Process Video** button\n\n"
|
| 239 |
"Once the video has been processed, you can play the video in the "
|
| 240 |
"Output Video frame"
|
| 241 |
)
|
|
|
|
| 303 |
return tab, webrtc_stream, cb_face, cb_person, cb_face_id, cb_hand_gesture
|
| 304 |
|
| 305 |
|
|
|
|
| 306 |
def _process_live_frame(
|
| 307 |
frame: np.ndarray,
|
| 308 |
face_detection: bool,
|
|
|
|
| 339 |
mins, secs = divmod(int(eta), 60)
|
| 340 |
eta_text = f"\nest. wait ~{mins}:{secs:02d}"
|
| 341 |
if total > 1:
|
| 342 |
+
return draw_overlay(frame, f"In queue (position {pos}/{total}){eta_text}")
|
| 343 |
+
return draw_overlay(frame, f"Waiting for available worker...{eta_text}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
# Terminal reason (session expired, pressure timeout, etc.)
|
| 345 |
# — close the WebRTC stream so the UI resets to "Start Inference".
|
| 346 |
from fastrtc import CloseStream
|
|
|
|
| 395 |
max_workers = int(os.environ.get("MAX_WORKERS", os.cpu_count()))
|
| 396 |
max_ram_gb = float(os.environ.get("MAX_RAM_GB", 32))
|
| 397 |
pool = EveWorkerPool(max_workers=max_workers, max_ram_gb=max_ram_gb, ram_headroom_gb=2.0)
|
| 398 |
+
stream_manager = LiveStreamManager(pool, session_lifetime_seconds=60 * 4)
|
| 399 |
|
| 400 |
# Separate HTTP server for video output — bypasses Chrome's per-origin
|
| 401 |
# connection limit that blocks /file= requests while SSE connections are open.
|
|
|
|
| 421 |
)
|
| 422 |
|
| 423 |
with gr.Blocks(title="Eve HMI Demo") as demo:
|
| 424 |
+
gr.Markdown("# Lattice sensAI Edge Vision Engine SDK")
|
| 425 |
gr.Markdown(
|
| 426 |
+
"Our SDK solves the human sensing challenges by outputting ready-to-use data."
|
| 427 |
+
" Our models have a low computation footprint and are ideal for <.5 TOPS devices"
|
| 428 |
+
" like FPGAs, small NPUs and SOCs.\n\n"
|
| 429 |
+
# TODO: Insert performance summary table
|
| 430 |
+
"EVE SDK packages are available for Windows, Linux and RPI.\n\n"
|
| 431 |
+
"Follow the instructions [here](https://huggingface.co/LatticeSemi/LSCC-SDK-HMI-Human-Machine-Interface-Edge-Vision-Engine)"
|
| 432 |
+
" to download and start using the EVE SDK within minutes.\n\n"
|
| 433 |
+
"You can also preview the EVE SDK with the following tabs:\n\n"
|
| 434 |
+
"- **Live Inference** to run it live from your webcam\n"
|
| 435 |
+
"- **Offline Inference** to test it with videos you can upload\n"
|
| 436 |
+
"- Use the **Face ID Registration** tab to register face(s) you can use in either Live or Offline Inference to test the Face ID model.\n"
|
| 437 |
+
"\n\n"
|
| 438 |
)
|
| 439 |
|
| 440 |
session_registry = gr.State(value={})
|
| 441 |
|
| 442 |
with gr.Tabs():
|
| 443 |
+
(
|
| 444 |
+
_live_tab,
|
| 445 |
+
webrtc_stream,
|
| 446 |
+
live_cb_face,
|
| 447 |
+
live_cb_person,
|
| 448 |
+
live_cb_face_id,
|
| 449 |
+
live_cb_hand_gesture,
|
| 450 |
+
) = _build_live_tab(rtc_config_provider.get(), face_id_tab)
|
| 451 |
+
|
| 452 |
(
|
| 453 |
video_tab,
|
| 454 |
input_video,
|
|
|
|
| 463 |
|
| 464 |
face_id_tab.build()
|
| 465 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 466 |
# --- Video Processing wiring ---
|
| 467 |
|
| 468 |
feature_controls = [
|
install_eve.py
CHANGED
|
@@ -21,7 +21,7 @@ import sys
|
|
| 21 |
from huggingface_hub import hf_hub_download
|
| 22 |
|
| 23 |
EVE_REPO = "LatticeSemi/PRIVATE-Edge-Vision-Engine-EVE-v7.0"
|
| 24 |
-
EVE_DEB = "LINUX_X86-
|
| 25 |
EVE_LICENSE_REPO = "LatticeSemi/PRIVATE-Edge-Vision-Engine-EVE-v7.0-License"
|
| 26 |
EVE_LICENSE = "libEveDevLicense.so"
|
| 27 |
SECRET_PATH = "/run/secrets/MODEL_ACCESS_TOKEN"
|
|
@@ -88,10 +88,7 @@ def main():
|
|
| 88 |
subprocess.run(["apt-get", "install", "-y", deb_path], check=True)
|
| 89 |
|
| 90 |
license_path = hf_hub_download(
|
| 91 |
-
repo_id=EVE_LICENSE_REPO,
|
| 92 |
-
filename=EVE_LICENSE,
|
| 93 |
-
local_dir=DOWNLOAD_DIR,
|
| 94 |
-
token=token
|
| 95 |
)
|
| 96 |
|
| 97 |
destination_path = get_license_destination_path()
|
|
|
|
| 21 |
from huggingface_hub import hf_hub_download
|
| 22 |
|
| 23 |
EVE_REPO = "LatticeSemi/PRIVATE-Edge-Vision-Engine-EVE-v7.0"
|
| 24 |
+
EVE_DEB = "LINUX_X86-5-7.0-eve-huggingface_7.0.5~git20260402.50d837a_amd64.deb"
|
| 25 |
EVE_LICENSE_REPO = "LatticeSemi/PRIVATE-Edge-Vision-Engine-EVE-v7.0-License"
|
| 26 |
EVE_LICENSE = "libEveDevLicense.so"
|
| 27 |
SECRET_PATH = "/run/secrets/MODEL_ACCESS_TOKEN"
|
|
|
|
| 88 |
subprocess.run(["apt-get", "install", "-y", deb_path], check=True)
|
| 89 |
|
| 90 |
license_path = hf_hub_download(
|
| 91 |
+
repo_id=EVE_LICENSE_REPO, filename=EVE_LICENSE, local_dir=DOWNLOAD_DIR, token=token
|
|
|
|
|
|
|
|
|
|
| 92 |
)
|
| 93 |
|
| 94 |
destination_path = get_license_destination_path()
|
shared/env_utils.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
import os
|
| 2 |
import sys
|
| 3 |
-
|
| 4 |
from pathlib import Path
|
| 5 |
|
| 6 |
from dotenv import load_dotenv # type: ignore
|
|
@@ -44,8 +43,7 @@ def require_secrets(*names: str) -> None:
|
|
| 44 |
|
| 45 |
settings_url = f"https://huggingface.co/spaces/{space_id}/settings"
|
| 46 |
print(
|
| 47 |
-
f"ERROR: Missing required secrets: {', '.join(missing)}\n"
|
| 48 |
-
f"Set them at: {settings_url}",
|
| 49 |
file=sys.stderr,
|
| 50 |
)
|
| 51 |
sys.exit(1)
|
|
|
|
| 1 |
import os
|
| 2 |
import sys
|
|
|
|
| 3 |
from pathlib import Path
|
| 4 |
|
| 5 |
from dotenv import load_dotenv # type: ignore
|
|
|
|
| 43 |
|
| 44 |
settings_url = f"https://huggingface.co/spaces/{space_id}/settings"
|
| 45 |
print(
|
| 46 |
+
f"ERROR: Missing required secrets: {', '.join(missing)}\n" f"Set them at: {settings_url}",
|
|
|
|
| 47 |
file=sys.stderr,
|
| 48 |
)
|
| 49 |
sys.exit(1)
|
shared/eve_messages.py
CHANGED
|
@@ -6,7 +6,6 @@ main Gradio process and Eve SDK worker processes.
|
|
| 6 |
|
| 7 |
from dataclasses import dataclass
|
| 8 |
|
| 9 |
-
|
| 10 |
# ---------------------------------------------------------------------------
|
| 11 |
# Shared data types
|
| 12 |
# ---------------------------------------------------------------------------
|
|
|
|
| 6 |
|
| 7 |
from dataclasses import dataclass
|
| 8 |
|
|
|
|
| 9 |
# ---------------------------------------------------------------------------
|
| 10 |
# Shared data types
|
| 11 |
# ---------------------------------------------------------------------------
|
shared/eve_worker_pool.py
CHANGED
|
@@ -22,12 +22,11 @@ import multiprocessing as mp
|
|
| 22 |
import os
|
| 23 |
import threading
|
| 24 |
import time
|
|
|
|
| 25 |
from dataclasses import dataclass
|
| 26 |
from multiprocessing.connection import Connection
|
| 27 |
-
from collections.abc import Callable
|
| 28 |
|
| 29 |
import numpy as np
|
| 30 |
-
|
| 31 |
from eve_messages import (
|
| 32 |
CalibrateNewUserCmd,
|
| 33 |
CalibrateOkResponse,
|
|
@@ -62,7 +61,9 @@ logger = setup_logger("EveWorkerPool")
|
|
| 62 |
# (required on Windows; avoids CDLL sharing on Linux/fork).
|
| 63 |
_mp_ctx = mp.get_context("spawn")
|
| 64 |
|
| 65 |
-
DO_PROBE_WORKER =
|
|
|
|
|
|
|
| 66 |
|
| 67 |
|
| 68 |
# ---------------------------------------------------------------------------
|
|
@@ -127,9 +128,7 @@ def _eve_worker_main(
|
|
| 127 |
|
| 128 |
try:
|
| 129 |
if isinstance(cmd, InferenceCmd):
|
| 130 |
-
frame = np.frombuffer(cmd.frame_bytes, dtype=cmd.dtype).reshape(
|
| 131 |
-
cmd.shape
|
| 132 |
-
)
|
| 133 |
features = cmd.features
|
| 134 |
del cmd # free recv'd bytes early; numpy holds its own ref
|
| 135 |
|
|
@@ -167,9 +166,7 @@ def _eve_worker_main(
|
|
| 167 |
result = eve.calibrate_new_user(frames)
|
| 168 |
conn.send(
|
| 169 |
CalibrateOkResponse(
|
| 170 |
-
result=CalibrationResultMsg(
|
| 171 |
-
result.success, result.user_id, result.message
|
| 172 |
-
),
|
| 173 |
)
|
| 174 |
)
|
| 175 |
|
|
@@ -178,16 +175,13 @@ def _eve_worker_main(
|
|
| 178 |
conn.send(RemoveUsersOkResponse(result=ok))
|
| 179 |
|
| 180 |
elif isinstance(cmd, RestoreGalleryCmd):
|
| 181 |
-
frames_per_user = [
|
| 182 |
-
_deserialize_frames(fd) for fd in cmd.frames_per_user_data
|
| 183 |
-
]
|
| 184 |
eve.remove_all_users()
|
| 185 |
results = eve.restore_gallery(frames_per_user)
|
| 186 |
conn.send(
|
| 187 |
RestoreGalleryOkResponse(
|
| 188 |
results=[
|
| 189 |
-
CalibrationResultMsg(r.success, r.user_id, r.message)
|
| 190 |
-
for r in results
|
| 191 |
],
|
| 192 |
)
|
| 193 |
)
|
|
@@ -209,14 +203,11 @@ def _eve_worker_main(
|
|
| 209 |
if cmd.remove_all_users:
|
| 210 |
eve.remove_all_users()
|
| 211 |
if cmd.gallery_paths:
|
| 212 |
-
frames_per_user = [
|
| 213 |
-
load_media_frames_raw(p) for p in cmd.gallery_paths
|
| 214 |
-
]
|
| 215 |
raw_results = eve.restore_gallery(frames_per_user)
|
| 216 |
del frames_per_user
|
| 217 |
gallery_results = [
|
| 218 |
-
CalibrationResultMsg(r.success, r.user_id, r.message)
|
| 219 |
-
for r in raw_results
|
| 220 |
]
|
| 221 |
del raw_results
|
| 222 |
conn.send(GalleryRestoredResponse(results=gallery_results))
|
|
@@ -233,9 +224,7 @@ def _eve_worker_main(
|
|
| 233 |
|
| 234 |
# Video processing loop
|
| 235 |
cap = cv2.VideoCapture(cmd.input_path)
|
| 236 |
-
container = av.open(
|
| 237 |
-
cmd.output_path, mode="w", options={"movflags": "faststart"}
|
| 238 |
-
)
|
| 239 |
stream = container.add_stream("libx264", rate=round(cmd.fps))
|
| 240 |
stream.width = cmd.width
|
| 241 |
stream.height = cmd.height
|
|
@@ -274,7 +263,11 @@ def _eve_worker_main(
|
|
| 274 |
finally:
|
| 275 |
cap.release()
|
| 276 |
container.close()
|
| 277 |
-
del
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
|
| 279 |
job_count += 1
|
| 280 |
conn.send(
|
|
@@ -491,14 +484,10 @@ class EveWorker:
|
|
| 491 |
return resp.frames_processed, gallery_results
|
| 492 |
|
| 493 |
elif isinstance(resp, ErrorResponse):
|
| 494 |
-
raise RuntimeError(
|
| 495 |
-
f"Worker {self.worker_id} process_video error: {resp.error}"
|
| 496 |
-
)
|
| 497 |
|
| 498 |
else:
|
| 499 |
-
raise RuntimeError(
|
| 500 |
-
f"Worker {self.worker_id} unexpected response: {resp}"
|
| 501 |
-
)
|
| 502 |
|
| 503 |
def send_shutdown(self) -> None:
|
| 504 |
try:
|
|
@@ -760,7 +749,7 @@ class EveWorkerPool:
|
|
| 760 |
headroom_mb = self._cfg.ram_headroom_gb * 1024
|
| 761 |
|
| 762 |
# Spawn a probe worker to measure init RSS
|
| 763 |
-
|
| 764 |
if DO_PROBE_WORKER:
|
| 765 |
probe = self._spawn_worker(probe=True)
|
| 766 |
try:
|
|
@@ -780,7 +769,7 @@ class EveWorkerPool:
|
|
| 780 |
# +1 because the probe worker already consumed memory
|
| 781 |
max_by_ram = max(1, int(usable_mb / estimated_peak_mb) + 1)
|
| 782 |
if DO_PROBE_WORKER:
|
| 783 |
-
actual = min(self._cfg.max_workers-1, max_by_ram)
|
| 784 |
else:
|
| 785 |
actual = min(self._cfg.max_workers, max_by_ram)
|
| 786 |
|
|
@@ -807,15 +796,19 @@ class EveWorkerPool:
|
|
| 807 |
parent_conn, child_conn = _mp_ctx.Pipe()
|
| 808 |
proc = _mp_ctx.Process(
|
| 809 |
target=_eve_worker_main,
|
| 810 |
-
args=(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 811 |
daemon=True,
|
| 812 |
)
|
| 813 |
proc.start()
|
| 814 |
return wid, proc, parent_conn
|
| 815 |
|
| 816 |
-
def _wait_for_ready(
|
| 817 |
-
self, wid: int, proc: mp.Process, parent_conn: Connection
|
| 818 |
-
) -> EveWorker:
|
| 819 |
"""Block until a launched worker sends its "ready" message.
|
| 820 |
|
| 821 |
Raises:
|
|
@@ -889,9 +882,7 @@ class EveWorkerPool:
|
|
| 889 |
# Detect crashed / zombie processes
|
| 890 |
if not w.process.is_alive():
|
| 891 |
if w.pending_recycle:
|
| 892 |
-
logger.info(
|
| 893 |
-
f"Worker {w.worker_id} (pid={w.process.pid}) recycled cleanly"
|
| 894 |
-
)
|
| 895 |
else:
|
| 896 |
logger.error(
|
| 897 |
f"Worker {w.worker_id} (pid={w.process.pid}) died unexpectedly"
|
|
@@ -914,4 +905,3 @@ class EveWorkerPool:
|
|
| 914 |
f"Worker {w.worker_id} busy for {elapsed:.0f}s "
|
| 915 |
f"(threshold={self._cfg.stuck_timeout_s}s)"
|
| 916 |
)
|
| 917 |
-
|
|
|
|
| 22 |
import os
|
| 23 |
import threading
|
| 24 |
import time
|
| 25 |
+
from collections.abc import Callable
|
| 26 |
from dataclasses import dataclass
|
| 27 |
from multiprocessing.connection import Connection
|
|
|
|
| 28 |
|
| 29 |
import numpy as np
|
|
|
|
| 30 |
from eve_messages import (
|
| 31 |
CalibrateNewUserCmd,
|
| 32 |
CalibrateOkResponse,
|
|
|
|
| 61 |
# (required on Windows; avoids CDLL sharing on Linux/fork).
|
| 62 |
_mp_ctx = mp.get_context("spawn")
|
| 63 |
|
| 64 |
+
DO_PROBE_WORKER = (
|
| 65 |
+
False # Set to False to skip the RAM probe and just use an estimate of the RAM used
|
| 66 |
+
)
|
| 67 |
|
| 68 |
|
| 69 |
# ---------------------------------------------------------------------------
|
|
|
|
| 128 |
|
| 129 |
try:
|
| 130 |
if isinstance(cmd, InferenceCmd):
|
| 131 |
+
frame = np.frombuffer(cmd.frame_bytes, dtype=cmd.dtype).reshape(cmd.shape)
|
|
|
|
|
|
|
| 132 |
features = cmd.features
|
| 133 |
del cmd # free recv'd bytes early; numpy holds its own ref
|
| 134 |
|
|
|
|
| 166 |
result = eve.calibrate_new_user(frames)
|
| 167 |
conn.send(
|
| 168 |
CalibrateOkResponse(
|
| 169 |
+
result=CalibrationResultMsg(result.success, result.user_id, result.message),
|
|
|
|
|
|
|
| 170 |
)
|
| 171 |
)
|
| 172 |
|
|
|
|
| 175 |
conn.send(RemoveUsersOkResponse(result=ok))
|
| 176 |
|
| 177 |
elif isinstance(cmd, RestoreGalleryCmd):
|
| 178 |
+
frames_per_user = [_deserialize_frames(fd) for fd in cmd.frames_per_user_data]
|
|
|
|
|
|
|
| 179 |
eve.remove_all_users()
|
| 180 |
results = eve.restore_gallery(frames_per_user)
|
| 181 |
conn.send(
|
| 182 |
RestoreGalleryOkResponse(
|
| 183 |
results=[
|
| 184 |
+
CalibrationResultMsg(r.success, r.user_id, r.message) for r in results
|
|
|
|
| 185 |
],
|
| 186 |
)
|
| 187 |
)
|
|
|
|
| 203 |
if cmd.remove_all_users:
|
| 204 |
eve.remove_all_users()
|
| 205 |
if cmd.gallery_paths:
|
| 206 |
+
frames_per_user = [load_media_frames_raw(p) for p in cmd.gallery_paths]
|
|
|
|
|
|
|
| 207 |
raw_results = eve.restore_gallery(frames_per_user)
|
| 208 |
del frames_per_user
|
| 209 |
gallery_results = [
|
| 210 |
+
CalibrationResultMsg(r.success, r.user_id, r.message) for r in raw_results
|
|
|
|
| 211 |
]
|
| 212 |
del raw_results
|
| 213 |
conn.send(GalleryRestoredResponse(results=gallery_results))
|
|
|
|
| 224 |
|
| 225 |
# Video processing loop
|
| 226 |
cap = cv2.VideoCapture(cmd.input_path)
|
| 227 |
+
container = av.open(cmd.output_path, mode="w", options={"movflags": "faststart"})
|
|
|
|
|
|
|
| 228 |
stream = container.add_stream("libx264", rate=round(cmd.fps))
|
| 229 |
stream.width = cmd.width
|
| 230 |
stream.height = cmd.height
|
|
|
|
| 263 |
finally:
|
| 264 |
cap.release()
|
| 265 |
container.close()
|
| 266 |
+
del (
|
| 267 |
+
cap,
|
| 268 |
+
container,
|
| 269 |
+
stream,
|
| 270 |
+
)
|
| 271 |
|
| 272 |
job_count += 1
|
| 273 |
conn.send(
|
|
|
|
| 484 |
return resp.frames_processed, gallery_results
|
| 485 |
|
| 486 |
elif isinstance(resp, ErrorResponse):
|
| 487 |
+
raise RuntimeError(f"Worker {self.worker_id} process_video error: {resp.error}")
|
|
|
|
|
|
|
| 488 |
|
| 489 |
else:
|
| 490 |
+
raise RuntimeError(f"Worker {self.worker_id} unexpected response: {resp}")
|
|
|
|
|
|
|
| 491 |
|
| 492 |
def send_shutdown(self) -> None:
|
| 493 |
try:
|
|
|
|
| 749 |
headroom_mb = self._cfg.ram_headroom_gb * 1024
|
| 750 |
|
| 751 |
# Spawn a probe worker to measure init RSS
|
| 752 |
+
|
| 753 |
if DO_PROBE_WORKER:
|
| 754 |
probe = self._spawn_worker(probe=True)
|
| 755 |
try:
|
|
|
|
| 769 |
# +1 because the probe worker already consumed memory
|
| 770 |
max_by_ram = max(1, int(usable_mb / estimated_peak_mb) + 1)
|
| 771 |
if DO_PROBE_WORKER:
|
| 772 |
+
actual = min(self._cfg.max_workers - 1, max_by_ram)
|
| 773 |
else:
|
| 774 |
actual = min(self._cfg.max_workers, max_by_ram)
|
| 775 |
|
|
|
|
| 796 |
parent_conn, child_conn = _mp_ctx.Pipe()
|
| 797 |
proc = _mp_ctx.Process(
|
| 798 |
target=_eve_worker_main,
|
| 799 |
+
args=(
|
| 800 |
+
child_conn,
|
| 801 |
+
self._eve_bin_path,
|
| 802 |
+
self._eve_lib_path,
|
| 803 |
+
wid,
|
| 804 |
+
self._cfg.max_jobs_per_worker,
|
| 805 |
+
),
|
| 806 |
daemon=True,
|
| 807 |
)
|
| 808 |
proc.start()
|
| 809 |
return wid, proc, parent_conn
|
| 810 |
|
| 811 |
+
def _wait_for_ready(self, wid: int, proc: mp.Process, parent_conn: Connection) -> EveWorker:
|
|
|
|
|
|
|
| 812 |
"""Block until a launched worker sends its "ready" message.
|
| 813 |
|
| 814 |
Raises:
|
|
|
|
| 882 |
# Detect crashed / zombie processes
|
| 883 |
if not w.process.is_alive():
|
| 884 |
if w.pending_recycle:
|
| 885 |
+
logger.info(f"Worker {w.worker_id} (pid={w.process.pid}) recycled cleanly")
|
|
|
|
|
|
|
| 886 |
else:
|
| 887 |
logger.error(
|
| 888 |
f"Worker {w.worker_id} (pid={w.process.pid}) died unexpectedly"
|
|
|
|
| 905 |
f"Worker {w.worker_id} busy for {elapsed:.0f}s "
|
| 906 |
f"(threshold={self._cfg.stuck_timeout_s}s)"
|
| 907 |
)
|
|
|
shared/eve_wrapper.py
CHANGED
|
@@ -3,12 +3,10 @@ import glob
|
|
| 3 |
import os
|
| 4 |
import platform
|
| 5 |
import sys
|
| 6 |
-
|
| 7 |
from dataclasses import dataclass
|
| 8 |
|
| 9 |
import cv2
|
| 10 |
import numpy as np
|
| 11 |
-
|
| 12 |
from eve_python import eve_sdk as sdk
|
| 13 |
from eve_python.structs.CFaceIdStructs import (
|
| 14 |
EveFaceIdCommand,
|
|
@@ -170,8 +168,7 @@ class EveWrapper:
|
|
| 170 |
if enabled
|
| 171 |
else sdk.structs.EveOptionEnabled.EVE_OPTION_DISABLED
|
| 172 |
)
|
| 173 |
-
options = sdk.structs.EveFaceIdOptions(enabled=eve_enabled,
|
| 174 |
-
threshold=threshold)
|
| 175 |
if enabled:
|
| 176 |
options.calibrationPoses = (
|
| 177 |
sdk.structs.EveFaceIdCalibrationPoseMode.EVE_FACEID_CALIBRATION_FRONTAL_ONLY
|
|
|
|
| 3 |
import os
|
| 4 |
import platform
|
| 5 |
import sys
|
|
|
|
| 6 |
from dataclasses import dataclass
|
| 7 |
|
| 8 |
import cv2
|
| 9 |
import numpy as np
|
|
|
|
| 10 |
from eve_python import eve_sdk as sdk
|
| 11 |
from eve_python.structs.CFaceIdStructs import (
|
| 12 |
EveFaceIdCommand,
|
|
|
|
| 168 |
if enabled
|
| 169 |
else sdk.structs.EveOptionEnabled.EVE_OPTION_DISABLED
|
| 170 |
)
|
| 171 |
+
options = sdk.structs.EveFaceIdOptions(enabled=eve_enabled, threshold=threshold)
|
|
|
|
| 172 |
if enabled:
|
| 173 |
options.calibrationPoses = (
|
| 174 |
sdk.structs.EveFaceIdCalibrationPoseMode.EVE_FACEID_CALIBRATION_FRONTAL_ONLY
|
shared/face_id_tab.py
CHANGED
|
@@ -133,8 +133,7 @@ class FaceIdTab:
|
|
| 133 |
with gr.Accordion("Instructions", open=False):
|
| 134 |
gr.Markdown(
|
| 135 |
(
|
| 136 |
-
"1. Choose between registering a face from an **Image** or a "
|
| 137 |
-
"**Video**\n"
|
| 138 |
if self._accept_video
|
| 139 |
else "1. Select an example image or upload your own\n"
|
| 140 |
)
|
|
@@ -158,7 +157,9 @@ class FaceIdTab:
|
|
| 158 |
with gr.Column(scale=3):
|
| 159 |
if self._accept_video:
|
| 160 |
# Image section (expanded by default)
|
| 161 |
-
with gr.Accordion(
|
|
|
|
|
|
|
| 162 |
if self._image_examples:
|
| 163 |
with gr.Accordion("Examples", open=True):
|
| 164 |
self._image_example_dataset = gr.Dataset(
|
|
@@ -173,7 +174,9 @@ class FaceIdTab:
|
|
| 173 |
)
|
| 174 |
|
| 175 |
# Video section (collapsed by default)
|
| 176 |
-
with gr.Accordion(
|
|
|
|
|
|
|
| 177 |
if self._video_examples:
|
| 178 |
with gr.Accordion("Examples", open=True):
|
| 179 |
self._video_example_dataset = gr.Dataset(
|
|
@@ -244,14 +247,10 @@ class FaceIdTab:
|
|
| 244 |
column = gr.Column(scale=scale, min_width=100, visible=False)
|
| 245 |
with column:
|
| 246 |
gr.Markdown("**Registered Faces**")
|
| 247 |
-
hint = gr.Markdown(
|
| 248 |
-
"_Go to the **Face ID Registration** tab to register faces._"
|
| 249 |
-
)
|
| 250 |
for _ in range(self._max_users):
|
| 251 |
imgs.append(gr.HTML(value="", visible=False))
|
| 252 |
-
self._summaries.append(
|
| 253 |
-
{"column": column, "hint": hint, "imgs": imgs, "height": height}
|
| 254 |
-
)
|
| 255 |
|
| 256 |
# ------------------------------------------------------------------
|
| 257 |
# Event wiring
|
|
@@ -429,18 +428,13 @@ class FaceIdTab:
|
|
| 429 |
media_source = image_path if image_path is not None else video_path
|
| 430 |
ext = os.path.splitext(media_source)[1]
|
| 431 |
session_tmp = _session_dir(request.session_hash)
|
| 432 |
-
stored_path = os.path.join(
|
| 433 |
-
session_tmp, f"face_id_{uuid.uuid4().hex[:8]}{ext}"
|
| 434 |
-
)
|
| 435 |
shutil.copy2(media_source, stored_path)
|
| 436 |
|
| 437 |
next_key = max(registry.keys(), default=0) + 1
|
| 438 |
registry = {**registry, next_key: FaceEntry(path=stored_path)}
|
| 439 |
|
| 440 |
-
|
| 441 |
-
all_frames = [
|
| 442 |
-
load_media_frames(entry.path) for entry in registry.values()
|
| 443 |
-
]
|
| 444 |
restore_results = worker.send_restore_gallery(all_frames)
|
| 445 |
for entry, r in zip(registry.values(), restore_results):
|
| 446 |
entry.sdk_id = r.user_id if r.success else None
|
|
@@ -463,9 +457,11 @@ class FaceIdTab:
|
|
| 463 |
return (
|
| 464 |
*self._slot_updates(registry),
|
| 465 |
*self._summary_updates(registry),
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
|
|
|
|
|
|
| 469 |
None,
|
| 470 |
None,
|
| 471 |
registry,
|
|
@@ -504,13 +500,9 @@ class FaceIdTab:
|
|
| 504 |
frames = load_media_frames(entry.path)
|
| 505 |
result = worker.send_calibrate_new_user(frames)
|
| 506 |
if result.success:
|
| 507 |
-
new_registry[u] = FaceEntry(
|
| 508 |
-
path=entry.path, sdk_id=result.user_id
|
| 509 |
-
)
|
| 510 |
else:
|
| 511 |
-
logger.warning(
|
| 512 |
-
f"Failed to re-register user {u}: {result.message}"
|
| 513 |
-
)
|
| 514 |
if os.path.exists(entry.path):
|
| 515 |
os.remove(entry.path)
|
| 516 |
registry = new_registry
|
|
@@ -593,9 +585,7 @@ class FaceIdTab:
|
|
| 593 |
updates.append(gr.update(interactive=False))
|
| 594 |
return tuple(updates)
|
| 595 |
|
| 596 |
-
def _summary_updates_single(
|
| 597 |
-
self, registry: dict[int, FaceEntry], summary: dict
|
| 598 |
-
) -> list:
|
| 599 |
"""Build updates for one summary group (column + hint + images)."""
|
| 600 |
updates: list = []
|
| 601 |
user_ids = sorted(registry.keys())
|
|
|
|
| 133 |
with gr.Accordion("Instructions", open=False):
|
| 134 |
gr.Markdown(
|
| 135 |
(
|
| 136 |
+
"1. Choose between registering a face from an **Image** or a " "**Video**\n"
|
|
|
|
| 137 |
if self._accept_video
|
| 138 |
else "1. Select an example image or upload your own\n"
|
| 139 |
)
|
|
|
|
| 157 |
with gr.Column(scale=3):
|
| 158 |
if self._accept_video:
|
| 159 |
# Image section (expanded by default)
|
| 160 |
+
with gr.Accordion(
|
| 161 |
+
"Input from an Image", open=True
|
| 162 |
+
) as self._image_accordion:
|
| 163 |
if self._image_examples:
|
| 164 |
with gr.Accordion("Examples", open=True):
|
| 165 |
self._image_example_dataset = gr.Dataset(
|
|
|
|
| 174 |
)
|
| 175 |
|
| 176 |
# Video section (collapsed by default)
|
| 177 |
+
with gr.Accordion(
|
| 178 |
+
"Input from a Video", open=False
|
| 179 |
+
) as self._video_accordion:
|
| 180 |
if self._video_examples:
|
| 181 |
with gr.Accordion("Examples", open=True):
|
| 182 |
self._video_example_dataset = gr.Dataset(
|
|
|
|
| 247 |
column = gr.Column(scale=scale, min_width=100, visible=False)
|
| 248 |
with column:
|
| 249 |
gr.Markdown("**Registered Faces**")
|
| 250 |
+
hint = gr.Markdown("_Go to the **Face ID Registration** tab to register faces._")
|
|
|
|
|
|
|
| 251 |
for _ in range(self._max_users):
|
| 252 |
imgs.append(gr.HTML(value="", visible=False))
|
| 253 |
+
self._summaries.append({"column": column, "hint": hint, "imgs": imgs, "height": height})
|
|
|
|
|
|
|
| 254 |
|
| 255 |
# ------------------------------------------------------------------
|
| 256 |
# Event wiring
|
|
|
|
| 428 |
media_source = image_path if image_path is not None else video_path
|
| 429 |
ext = os.path.splitext(media_source)[1]
|
| 430 |
session_tmp = _session_dir(request.session_hash)
|
| 431 |
+
stored_path = os.path.join(session_tmp, f"face_id_{uuid.uuid4().hex[:8]}{ext}")
|
|
|
|
|
|
|
| 432 |
shutil.copy2(media_source, stored_path)
|
| 433 |
|
| 434 |
next_key = max(registry.keys(), default=0) + 1
|
| 435 |
registry = {**registry, next_key: FaceEntry(path=stored_path)}
|
| 436 |
|
| 437 |
+
all_frames = [load_media_frames(entry.path) for entry in registry.values()]
|
|
|
|
|
|
|
|
|
|
| 438 |
restore_results = worker.send_restore_gallery(all_frames)
|
| 439 |
for entry, r in zip(registry.values(), restore_results):
|
| 440 |
entry.sdk_id = r.user_id if r.success else None
|
|
|
|
| 457 |
return (
|
| 458 |
*self._slot_updates(registry),
|
| 459 |
*self._summary_updates(registry),
|
| 460 |
+
(
|
| 461 |
+
f"Successfully registered (Face ID: {registry[next_key].sdk_id})."
|
| 462 |
+
if registry[next_key].sdk_id is not None
|
| 463 |
+
else f"Successfully registered as User {next_key}."
|
| 464 |
+
),
|
| 465 |
None,
|
| 466 |
None,
|
| 467 |
registry,
|
|
|
|
| 500 |
frames = load_media_frames(entry.path)
|
| 501 |
result = worker.send_calibrate_new_user(frames)
|
| 502 |
if result.success:
|
| 503 |
+
new_registry[u] = FaceEntry(path=entry.path, sdk_id=result.user_id)
|
|
|
|
|
|
|
| 504 |
else:
|
| 505 |
+
logger.warning(f"Failed to re-register user {u}: {result.message}")
|
|
|
|
|
|
|
| 506 |
if os.path.exists(entry.path):
|
| 507 |
os.remove(entry.path)
|
| 508 |
registry = new_registry
|
|
|
|
| 585 |
updates.append(gr.update(interactive=False))
|
| 586 |
return tuple(updates)
|
| 587 |
|
| 588 |
+
def _summary_updates_single(self, registry: dict[int, FaceEntry], summary: dict) -> list:
|
|
|
|
|
|
|
| 589 |
"""Build updates for one summary group (column + hint + images)."""
|
| 590 |
updates: list = []
|
| 591 |
user_ids = sorted(registry.keys())
|
shared/frame_utils.py
CHANGED
|
@@ -49,9 +49,7 @@ def load_media_frames_raw(media_path: str) -> list[np.ndarray]:
|
|
| 49 |
return [img]
|
| 50 |
|
| 51 |
|
| 52 |
-
def extract_frames(
|
| 53 |
-
image_path: str | None, video_path: str | None
|
| 54 |
-
) -> list[np.ndarray]:
|
| 55 |
"""Extract BGR frames from an image or video.
|
| 56 |
|
| 57 |
Args:
|
|
|
|
| 49 |
return [img]
|
| 50 |
|
| 51 |
|
| 52 |
+
def extract_frames(image_path: str | None, video_path: str | None) -> list[np.ndarray]:
|
|
|
|
|
|
|
| 53 |
"""Extract BGR frames from an image or video.
|
| 54 |
|
| 55 |
Args:
|
shared/live_inference.py
CHANGED
|
@@ -14,7 +14,6 @@ from typing import Any, Callable
|
|
| 14 |
import cv2
|
| 15 |
import gradio as gr
|
| 16 |
import numpy as np
|
| 17 |
-
|
| 18 |
from log_utils import setup_logger
|
| 19 |
|
| 20 |
InferenceFn = Callable[[np.ndarray], np.ndarray]
|
|
@@ -180,9 +179,10 @@ def patch_fastrtc_frame_queue() -> None:
|
|
| 180 |
Must be called **before** any ``WebRTC`` component is created
|
| 181 |
(typically at the top of ``__main__``).
|
| 182 |
"""
|
|
|
|
|
|
|
| 183 |
import fastrtc.tracks as frt
|
| 184 |
from aiortc.mediastreams import MediaStreamError
|
| 185 |
-
from typing import cast as _cast
|
| 186 |
|
| 187 |
_orig_init = frt.VideoCallback.__init__
|
| 188 |
|
|
|
|
| 14 |
import cv2
|
| 15 |
import gradio as gr
|
| 16 |
import numpy as np
|
|
|
|
| 17 |
from log_utils import setup_logger
|
| 18 |
|
| 19 |
InferenceFn = Callable[[np.ndarray], np.ndarray]
|
|
|
|
| 179 |
Must be called **before** any ``WebRTC`` component is created
|
| 180 |
(typically at the top of ``__main__``).
|
| 181 |
"""
|
| 182 |
+
from typing import cast as _cast
|
| 183 |
+
|
| 184 |
import fastrtc.tracks as frt
|
| 185 |
from aiortc.mediastreams import MediaStreamError
|
|
|
|
| 186 |
|
| 187 |
_orig_init = frt.VideoCallback.__init__
|
| 188 |
|
shared/live_stream_manager.py
CHANGED
|
@@ -134,15 +134,12 @@ class LiveStreamManager:
|
|
| 134 |
self._release_entry(connection_id)
|
| 135 |
with self._lock:
|
| 136 |
self._expired.add(connection_id)
|
| 137 |
-
return None, (
|
| 138 |
-
"Session ended\nclick `Start Inference` to restart."
|
| 139 |
-
)
|
| 140 |
|
| 141 |
# Pressure-based countdown (only when enabled)
|
| 142 |
if self._timeout_s is not None:
|
| 143 |
-
under_pressure = (
|
| 144 |
-
self._pool.
|
| 145 |
-
and (self._pool.waiting_count > 0 or len(self._waiting) > 0)
|
| 146 |
)
|
| 147 |
if under_pressure:
|
| 148 |
if entry.pressure_start is None:
|
|
@@ -279,8 +276,7 @@ class LiveStreamManager:
|
|
| 279 |
with self._lock:
|
| 280 |
entries = list(self._streams.values())
|
| 281 |
known = sorted(
|
| 282 |
-
max(0.0, self._session_lifetime_s - (now - e.start_time))
|
| 283 |
-
for e in entries
|
| 284 |
)
|
| 285 |
|
| 286 |
# Workers doing video processing (busy, not live-stream) have
|
|
@@ -351,12 +347,9 @@ class LiveStreamManager:
|
|
| 351 |
with self._lock:
|
| 352 |
self._waiting.pop(connection_id, None)
|
| 353 |
logger.info(
|
| 354 |
-
f"Live stream ({connection_id}) acquired worker {worker.worker_id} "
|
| 355 |
-
f"after waiting"
|
| 356 |
-
)
|
| 357 |
-
return self._setup_stream(
|
| 358 |
-
connection_id, waiting.session_hash, worker, waiting.registry
|
| 359 |
)
|
|
|
|
| 360 |
|
| 361 |
def _release_entry(self, connection_id: str) -> None:
|
| 362 |
with self._lock:
|
|
|
|
| 134 |
self._release_entry(connection_id)
|
| 135 |
with self._lock:
|
| 136 |
self._expired.add(connection_id)
|
| 137 |
+
return None, ("Session ended\nclick `Start Inference` to restart.")
|
|
|
|
|
|
|
| 138 |
|
| 139 |
# Pressure-based countdown (only when enabled)
|
| 140 |
if self._timeout_s is not None:
|
| 141 |
+
under_pressure = self._pool.idle_count == 0 and (
|
| 142 |
+
self._pool.waiting_count > 0 or len(self._waiting) > 0
|
|
|
|
| 143 |
)
|
| 144 |
if under_pressure:
|
| 145 |
if entry.pressure_start is None:
|
|
|
|
| 276 |
with self._lock:
|
| 277 |
entries = list(self._streams.values())
|
| 278 |
known = sorted(
|
| 279 |
+
max(0.0, self._session_lifetime_s - (now - e.start_time)) for e in entries
|
|
|
|
| 280 |
)
|
| 281 |
|
| 282 |
# Workers doing video processing (busy, not live-stream) have
|
|
|
|
| 347 |
with self._lock:
|
| 348 |
self._waiting.pop(connection_id, None)
|
| 349 |
logger.info(
|
| 350 |
+
f"Live stream ({connection_id}) acquired worker {worker.worker_id} " f"after waiting"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
)
|
| 352 |
+
return self._setup_stream(connection_id, waiting.session_hash, worker, waiting.registry)
|
| 353 |
|
| 354 |
def _release_entry(self, connection_id: str) -> None:
|
| 355 |
with self._lock:
|
shared/log_utils.py
CHANGED
|
@@ -11,8 +11,10 @@ def setup_logger(name: str = "hf_demos", level: str | None = None) -> logging.Lo
|
|
| 11 |
logger.setLevel(getattr(logging, lvl, logging.INFO))
|
| 12 |
|
| 13 |
ch = logging.StreamHandler()
|
| 14 |
-
fmt = logging.Formatter(
|
|
|
|
|
|
|
| 15 |
ch.setFormatter(fmt)
|
| 16 |
logger.addHandler(ch)
|
| 17 |
logger.propagate = False
|
| 18 |
-
return logger
|
|
|
|
| 11 |
logger.setLevel(getattr(logging, lvl, logging.INFO))
|
| 12 |
|
| 13 |
ch = logging.StreamHandler()
|
| 14 |
+
fmt = logging.Formatter(
|
| 15 |
+
"%(asctime)s [%(levelname)s] [%(name)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
|
| 16 |
+
)
|
| 17 |
ch.setFormatter(fmt)
|
| 18 |
logger.addHandler(ch)
|
| 19 |
logger.propagate = False
|
| 20 |
+
return logger
|
shared/memory_monitor.py
CHANGED
|
@@ -13,6 +13,7 @@ from log_utils import setup_logger
|
|
| 13 |
|
| 14 |
logger = setup_logger("MemoryMonitor")
|
| 15 |
|
|
|
|
| 16 |
class _HasWorkerInfo(Protocol):
|
| 17 |
"""Minimal interface a worker object must expose for memory reporting."""
|
| 18 |
|
|
@@ -25,7 +26,7 @@ def start_memory_reporter(
|
|
| 25 |
get_workers: callable,
|
| 26 |
lock: threading.Condition,
|
| 27 |
shutdown_flag: callable,
|
| 28 |
-
interval_s: float = 60*60,
|
| 29 |
max_ram_gb: float = 0,
|
| 30 |
) -> threading.Thread:
|
| 31 |
"""Launch a daemon thread that logs memory usage periodically.
|
|
@@ -117,9 +118,7 @@ def _log_report(
|
|
| 117 |
workers = list(get_workers())
|
| 118 |
for w in workers:
|
| 119 |
if w.status == "dead" or not w.process.is_alive():
|
| 120 |
-
lines.append(
|
| 121 |
-
f" worker {w.worker_id} (pid={w.process.pid}): dead"
|
| 122 |
-
)
|
| 123 |
continue
|
| 124 |
try:
|
| 125 |
w_rss = psutil.Process(w.process.pid).memory_info().rss / (1024 * 1024)
|
|
@@ -129,10 +128,7 @@ def _log_report(
|
|
| 129 |
f"RSS={w_rss:.0f} MB, status={w.status}"
|
| 130 |
)
|
| 131 |
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
| 132 |
-
lines.append(
|
| 133 |
-
f" worker {w.worker_id} (pid={w.process.pid}): "
|
| 134 |
-
f"not accessible"
|
| 135 |
-
)
|
| 136 |
|
| 137 |
lines.append(
|
| 138 |
f" total: main={main_rss:.0f} MB + "
|
|
|
|
| 13 |
|
| 14 |
logger = setup_logger("MemoryMonitor")
|
| 15 |
|
| 16 |
+
|
| 17 |
class _HasWorkerInfo(Protocol):
|
| 18 |
"""Minimal interface a worker object must expose for memory reporting."""
|
| 19 |
|
|
|
|
| 26 |
get_workers: callable,
|
| 27 |
lock: threading.Condition,
|
| 28 |
shutdown_flag: callable,
|
| 29 |
+
interval_s: float = 60 * 60,
|
| 30 |
max_ram_gb: float = 0,
|
| 31 |
) -> threading.Thread:
|
| 32 |
"""Launch a daemon thread that logs memory usage periodically.
|
|
|
|
| 118 |
workers = list(get_workers())
|
| 119 |
for w in workers:
|
| 120 |
if w.status == "dead" or not w.process.is_alive():
|
| 121 |
+
lines.append(f" worker {w.worker_id} (pid={w.process.pid}): dead")
|
|
|
|
|
|
|
| 122 |
continue
|
| 123 |
try:
|
| 124 |
w_rss = psutil.Process(w.process.pid).memory_info().rss / (1024 * 1024)
|
|
|
|
| 128 |
f"RSS={w_rss:.0f} MB, status={w.status}"
|
| 129 |
)
|
| 130 |
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
| 131 |
+
lines.append(f" worker {w.worker_id} (pid={w.process.pid}): " f"not accessible")
|
|
|
|
|
|
|
|
|
|
| 132 |
|
| 133 |
lines.append(
|
| 134 |
f" total: main={main_rss:.0f} MB + "
|
shared/video_file_server.py
CHANGED
|
@@ -56,7 +56,9 @@ class _VideoHandler(http.server.BaseHTTPRequestHandler):
|
|
| 56 |
self.send_header("Access-Control-Allow-Origin", "*")
|
| 57 |
self.send_header("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS")
|
| 58 |
self.send_header("Access-Control-Allow-Headers", "Range")
|
| 59 |
-
self.send_header(
|
|
|
|
|
|
|
| 60 |
|
| 61 |
# ------------------------------------------------------------------
|
| 62 |
# HTTP methods
|
|
@@ -151,7 +153,7 @@ class _VideoHandler(http.server.BaseHTTPRequestHandler):
|
|
| 151 |
"""Parse ``Range: bytes=start-end`` and return (start, end) inclusive."""
|
| 152 |
if not header.startswith("bytes="):
|
| 153 |
raise ValueError(header)
|
| 154 |
-
spec = header[len("bytes="):]
|
| 155 |
parts = spec.split("-", 1)
|
| 156 |
if len(parts) != 2:
|
| 157 |
raise ValueError(header)
|
|
|
|
| 56 |
self.send_header("Access-Control-Allow-Origin", "*")
|
| 57 |
self.send_header("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS")
|
| 58 |
self.send_header("Access-Control-Allow-Headers", "Range")
|
| 59 |
+
self.send_header(
|
| 60 |
+
"Access-Control-Expose-Headers", "Content-Range, Content-Length, Accept-Ranges"
|
| 61 |
+
)
|
| 62 |
|
| 63 |
# ------------------------------------------------------------------
|
| 64 |
# HTTP methods
|
|
|
|
| 153 |
"""Parse ``Range: bytes=start-end`` and return (start, end) inclusive."""
|
| 154 |
if not header.startswith("bytes="):
|
| 155 |
raise ValueError(header)
|
| 156 |
+
spec = header[len("bytes=") :]
|
| 157 |
parts = spec.split("-", 1)
|
| 158 |
if len(parts) != 2:
|
| 159 |
raise ValueError(header)
|
shared/video_processing.py
CHANGED
|
@@ -4,17 +4,17 @@ Provides validation, frame-by-frame inference processing, and Gradio UI helpers
|
|
| 4 |
that are common across all video-based demos.
|
| 5 |
"""
|
| 6 |
|
| 7 |
-
import av
|
| 8 |
-
import cv2
|
| 9 |
-
import gradio as gr
|
| 10 |
-
import numpy as np
|
| 11 |
import os
|
| 12 |
import tempfile
|
| 13 |
-
|
| 14 |
from dataclasses import dataclass
|
| 15 |
from pathlib import Path
|
| 16 |
from typing import Any, Callable
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
InferenceFn = Callable[[np.ndarray], np.ndarray]
|
| 19 |
|
| 20 |
|
|
@@ -115,7 +115,8 @@ def validate_video(video_path: str, limits: VideoLimits = DEFAULT_LIMITS) -> Non
|
|
| 115 |
|
| 116 |
if duration > limits.max_duration_seconds:
|
| 117 |
raise gr.Error(
|
| 118 |
-
f"Video duration ({duration:.1f}s) exceeds the maximum allowed length of {limits.max_duration_seconds} seconds."
|
|
|
|
| 119 |
finally:
|
| 120 |
video_capture.release()
|
| 121 |
|
|
@@ -184,7 +185,7 @@ def process_video(
|
|
| 184 |
|
| 185 |
fps = video_capture.get(cv2.CAP_PROP_FPS)
|
| 186 |
width = int(video_capture.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 187 |
-
height = int(video_capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 188 |
total_frames = int(video_capture.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 189 |
|
| 190 |
# Webcam-recorded videos (especially WebM from browsers) can report bogus
|
|
@@ -279,9 +280,7 @@ def wire_video_upload(
|
|
| 279 |
)
|
| 280 |
|
| 281 |
input_video.stop_recording(
|
| 282 |
-
fn=validate_and_update,
|
| 283 |
-
inputs=input_video,
|
| 284 |
-
outputs=[input_video, process_btn]
|
| 285 |
)
|
| 286 |
|
| 287 |
input_video.clear(
|
|
|
|
| 4 |
that are common across all video-based demos.
|
| 5 |
"""
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
import os
|
| 8 |
import tempfile
|
|
|
|
| 9 |
from dataclasses import dataclass
|
| 10 |
from pathlib import Path
|
| 11 |
from typing import Any, Callable
|
| 12 |
|
| 13 |
+
import av
|
| 14 |
+
import cv2
|
| 15 |
+
import gradio as gr
|
| 16 |
+
import numpy as np
|
| 17 |
+
|
| 18 |
InferenceFn = Callable[[np.ndarray], np.ndarray]
|
| 19 |
|
| 20 |
|
|
|
|
| 115 |
|
| 116 |
if duration > limits.max_duration_seconds:
|
| 117 |
raise gr.Error(
|
| 118 |
+
f"Video duration ({duration:.1f}s) exceeds the maximum allowed length of {limits.max_duration_seconds} seconds."
|
| 119 |
+
)
|
| 120 |
finally:
|
| 121 |
video_capture.release()
|
| 122 |
|
|
|
|
| 185 |
|
| 186 |
fps = video_capture.get(cv2.CAP_PROP_FPS)
|
| 187 |
width = int(video_capture.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 188 |
+
height = int(video_capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 189 |
total_frames = int(video_capture.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 190 |
|
| 191 |
# Webcam-recorded videos (especially WebM from browsers) can report bogus
|
|
|
|
| 280 |
)
|
| 281 |
|
| 282 |
input_video.stop_recording(
|
| 283 |
+
fn=validate_and_update, inputs=input_video, outputs=[input_video, process_btn]
|
|
|
|
|
|
|
| 284 |
)
|
| 285 |
|
| 286 |
input_video.clear(
|