tiena2cva commited on
Commit
6d4caef
·
1 Parent(s): 2357a36

fix(hf-spaces): install mediapipe native libraries

Browse files
README.md CHANGED
@@ -85,6 +85,8 @@ The runtime uses these defaults:
85
  The MediaPipe Tasks backend tries its GPU delegate on ZeroGPU and falls back to CPU if unavailable.
86
  The older `mp.solutions.pose` path remains CPU-only. The Torch exercise router loads and predicts
87
  inside its ZeroGPU-wrapped function, so model objects are not pickled across the API boundary.
 
 
88
 
89
  When running in real mode, the UI summary now shows:
90
 
 
85
  The MediaPipe Tasks backend tries its GPU delegate on ZeroGPU and falls back to CPU if unavailable.
86
  The older `mp.solutions.pose` path remains CPU-only. The Torch exercise router loads and predicts
87
  inside its ZeroGPU-wrapped function, so model objects are not pickled across the API boundary.
88
+ The root `packages.txt` installs the native GLES/EGL/OpenGL libraries required by MediaPipe Tasks
89
+ on the Hugging Face runtime.
90
 
91
  When running in real mode, the UI summary now shows:
92
 
packages.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ libegl1
2
+ libgl1
3
+ libgles2
4
+ libglib2.0-0
src/pozify/steps/pose_backends/__init__.py CHANGED
@@ -1,6 +1,10 @@
1
  from __future__ import annotations
2
 
3
- from pozify.steps.pose_backends.base import PoseBackend, PoseDetection
 
 
 
 
4
  from pozify.steps.pose_backends.landmarks import (
5
  LANDMARK_NAMES,
6
  LANDMARK_SCHEMA,
@@ -20,6 +24,7 @@ __all__ = [
20
  "MediaPipePoseBackend",
21
  "MockPoseBackend",
22
  "PoseBackend",
 
23
  "PoseDetection",
24
  "create_pose_backend",
25
  "landmark_list_to_dict",
 
1
  from __future__ import annotations
2
 
3
+ from pozify.steps.pose_backends.base import (
4
+ PoseBackend,
5
+ PoseBackendUnavailableError,
6
+ PoseDetection,
7
+ )
8
  from pozify.steps.pose_backends.landmarks import (
9
  LANDMARK_NAMES,
10
  LANDMARK_SCHEMA,
 
24
  "MediaPipePoseBackend",
25
  "MockPoseBackend",
26
  "PoseBackend",
27
+ "PoseBackendUnavailableError",
28
  "PoseDetection",
29
  "create_pose_backend",
30
  "landmark_list_to_dict",
src/pozify/steps/pose_backends/base.py CHANGED
@@ -11,6 +11,10 @@ class PoseDetection:
11
  source: str
12
 
13
 
 
 
 
 
14
  class PoseBackend(Protocol):
15
  source: str
16
 
 
11
  source: str
12
 
13
 
14
+ class PoseBackendUnavailableError(RuntimeError):
15
+ pass
16
+
17
+
18
  class PoseBackend(Protocol):
19
  source: str
20
 
src/pozify/steps/pose_backends/mediapipe.py CHANGED
@@ -6,7 +6,10 @@ from typing import Any
6
  from urllib.request import urlretrieve
7
 
8
  from pozify.hf_spaces import zero_gpu_enabled
9
- from pozify.steps.pose_backends.base import PoseDetection
 
 
 
10
  from pozify.steps.pose_backends.landmarks import landmark_list_to_dict
11
 
12
 
@@ -53,21 +56,27 @@ class MediaPipePoseBackend:
53
  try:
54
  import mediapipe as mp
55
  except ImportError as exc:
56
- raise RuntimeError(
57
  "MediaPipe is required for the mediapipe pose backend. Install dependencies with uv sync."
58
  ) from exc
59
 
60
  if hasattr(mp, "solutions"):
61
- return mp.solutions.pose.Pose(
62
- static_image_mode=False,
63
- model_complexity=1,
64
- smooth_landmarks=False,
65
- enable_segmentation=False,
66
- min_detection_confidence=0.5,
67
- min_tracking_confidence=0.5,
68
- )
 
 
 
69
 
70
- return _MediaPipeTasksPoseAdapter(mp, _ensure_pose_task_model())
 
 
 
71
 
72
 
73
  class _MediaPipeTasksPoseAdapter:
@@ -149,3 +158,11 @@ def _ensure_pose_task_model() -> Path:
149
  model_path.parent.mkdir(parents=True, exist_ok=True)
150
  urlretrieve(POSE_TASK_MODEL_URL, model_path)
151
  return model_path
 
 
 
 
 
 
 
 
 
6
  from urllib.request import urlretrieve
7
 
8
  from pozify.hf_spaces import zero_gpu_enabled
9
+ from pozify.steps.pose_backends.base import (
10
+ PoseBackendUnavailableError,
11
+ PoseDetection,
12
+ )
13
  from pozify.steps.pose_backends.landmarks import landmark_list_to_dict
14
 
15
 
 
56
  try:
57
  import mediapipe as mp
58
  except ImportError as exc:
59
+ raise PoseBackendUnavailableError(
60
  "MediaPipe is required for the mediapipe pose backend. Install dependencies with uv sync."
61
  ) from exc
62
 
63
  if hasattr(mp, "solutions"):
64
+ try:
65
+ return mp.solutions.pose.Pose(
66
+ static_image_mode=False,
67
+ model_complexity=1,
68
+ smooth_landmarks=False,
69
+ enable_segmentation=False,
70
+ min_detection_confidence=0.5,
71
+ min_tracking_confidence=0.5,
72
+ )
73
+ except OSError as exc:
74
+ raise _native_library_error(exc) from exc
75
 
76
+ try:
77
+ return _MediaPipeTasksPoseAdapter(mp, _ensure_pose_task_model())
78
+ except OSError as exc:
79
+ raise _native_library_error(exc) from exc
80
 
81
 
82
  class _MediaPipeTasksPoseAdapter:
 
158
  model_path.parent.mkdir(parents=True, exist_ok=True)
159
  urlretrieve(POSE_TASK_MODEL_URL, model_path)
160
  return model_path
161
+
162
+
163
+ def _native_library_error(exc: OSError) -> PoseBackendUnavailableError:
164
+ return PoseBackendUnavailableError(
165
+ "MediaPipe could not load native system libraries. On Hugging Face Spaces, "
166
+ "add libgles2, libegl1, libgl1, and libglib2.0-0 to packages.txt. "
167
+ f"Original error: {exc}"
168
+ )
src/pozify/steps/pose_landmarker.py CHANGED
@@ -12,6 +12,7 @@ from pozify.steps.pose_backends import (
12
  LANDMARK_SCHEMA,
13
  MockPoseBackend,
14
  PoseBackend,
 
15
  create_pose_backend,
16
  )
17
  from pozify.steps.video_qc import sample_frame_indices
@@ -120,6 +121,33 @@ def _empty_sequence() -> PoseSequence:
120
  )
121
 
122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  def _run_with_backend(manifest: VideoManifest, backend: PoseBackend) -> PoseSequence:
124
  if not manifest.analysis_allowed or not manifest.video_path:
125
  return _empty_sequence()
@@ -167,7 +195,10 @@ def _gpu_duration(*_args: Any, **_kwargs: Any) -> int:
167
 
168
  @spaces_gpu(duration=_gpu_duration)
169
  def _run_named_backend(manifest: VideoManifest, backend_name: str) -> PoseSequence:
170
- selected_backend = create_pose_backend(backend_name)
 
 
 
171
  return _run_with_backend(manifest, selected_backend)
172
 
173
 
 
12
  LANDMARK_SCHEMA,
13
  MockPoseBackend,
14
  PoseBackend,
15
+ PoseBackendUnavailableError,
16
  create_pose_backend,
17
  )
18
  from pozify.steps.video_qc import sample_frame_indices
 
121
  )
122
 
123
 
124
+ def _unavailable_sequence(reason: str) -> PoseSequence:
125
+ return PoseSequence(
126
+ frames=[
127
+ PoseFrame(
128
+ frame_index=0,
129
+ timestamp_sec=0.0,
130
+ landmarks={},
131
+ world_landmarks={},
132
+ pose_quality={
133
+ "mean_visibility": 0.0,
134
+ "critical_landmarks_visible": False,
135
+ "full_body_visibility_proxy": 0.0,
136
+ "landmark_count": 0,
137
+ "pose_warning": "pose_backend_unavailable",
138
+ "source": "none",
139
+ "landmark_schema": LANDMARK_SCHEMA,
140
+ "coordinate_source": "none",
141
+ "reason": reason,
142
+ },
143
+ )
144
+ ],
145
+ normalized=False,
146
+ smoothing_method="none",
147
+ pose_valid_ratio=0.0,
148
+ )
149
+
150
+
151
  def _run_with_backend(manifest: VideoManifest, backend: PoseBackend) -> PoseSequence:
152
  if not manifest.analysis_allowed or not manifest.video_path:
153
  return _empty_sequence()
 
195
 
196
  @spaces_gpu(duration=_gpu_duration)
197
  def _run_named_backend(manifest: VideoManifest, backend_name: str) -> PoseSequence:
198
+ try:
199
+ selected_backend = create_pose_backend(backend_name)
200
+ except PoseBackendUnavailableError as exc:
201
+ return _unavailable_sequence(str(exc))
202
  return _run_with_backend(manifest, selected_backend)
203
 
204
 
tests/test_pose_steps.py CHANGED
@@ -19,7 +19,11 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
19
 
20
  from pozify.contracts import PoseFrame, PoseSequence, VideoManifest
21
  from pozify.steps import pose_cleaning, pose_landmarker
22
- from pozify.steps.pose_backends import PoseDetection, landmark_list_to_dict
 
 
 
 
23
  from pozify.steps.pose_backends.mediapipe import _MediaPipeTasksPoseAdapter
24
 
25
 
@@ -133,6 +137,40 @@ class PoseStepTests(unittest.TestCase):
133
 
134
  self.assertEqual(len(sequence.frames), 130)
135
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  def test_dense_video_iteration_reads_sequentially_without_reseeking(self) -> None:
137
  class FakeCapture:
138
  def __init__(self) -> None:
 
19
 
20
  from pozify.contracts import PoseFrame, PoseSequence, VideoManifest
21
  from pozify.steps import pose_cleaning, pose_landmarker
22
+ from pozify.steps.pose_backends import (
23
+ PoseBackendUnavailableError,
24
+ PoseDetection,
25
+ landmark_list_to_dict,
26
+ )
27
  from pozify.steps.pose_backends.mediapipe import _MediaPipeTasksPoseAdapter
28
 
29
 
 
137
 
138
  self.assertEqual(len(sequence.frames), 130)
139
 
140
+ def test_pose_landmarker_returns_unavailable_sequence_for_missing_backend_libs(
141
+ self,
142
+ ) -> None:
143
+ path = self._write_video()
144
+ manifest = VideoManifest(
145
+ video_path=str(path),
146
+ fps=30.0,
147
+ duration_sec=0.133,
148
+ total_frames=4,
149
+ sampled_frames=4,
150
+ width=640,
151
+ height=480,
152
+ codec="mp4v",
153
+ container="mp4",
154
+ brightness_mean=120.0,
155
+ blur_laplacian_var=100.0,
156
+ quality_warnings=[],
157
+ analysis_allowed=True,
158
+ )
159
+
160
+ with patch(
161
+ "pozify.steps.pose_landmarker.create_pose_backend",
162
+ side_effect=PoseBackendUnavailableError("missing libGLESv2.so.2"),
163
+ ):
164
+ sequence = pose_landmarker.run(manifest, backend_name="mediapipe")
165
+
166
+ self.assertEqual(sequence.pose_valid_ratio, 0.0)
167
+ self.assertEqual(len(sequence.frames), 1)
168
+ self.assertEqual(
169
+ sequence.frames[0].pose_quality["pose_warning"],
170
+ "pose_backend_unavailable",
171
+ )
172
+ self.assertIn("libGLESv2", sequence.frames[0].pose_quality["reason"])
173
+
174
  def test_dense_video_iteration_reads_sequentially_without_reseeking(self) -> None:
175
  class FakeCapture:
176
  def __init__(self) -> None: