Commit ·
ca3d3de
1
Parent(s): f90fef2
fix: download MLP + MediaPipe weights from HF Hub instead of bundling
Browse filesHF Spaces rejects binary files >5MB without going through Xet/LFS, and
LFS migration of an already-pushed history triggers a force-push that's
not appropriate here. Cleaner pattern: host the weights on a public HF
model repo (LucasLooTan/signbridge-asl-classifier) and pull via
hf_hub_download on first call. Cached under HF_HOME so subsequent calls
are instant.
Override env vars (still supported):
- SIGNBRIDGE_LANDMARK_MLP_PATH — local .pt
- SIGNBRIDGE_HAND_LANDMARKER_PATH — local .task
- SIGNBRIDGE_CLASSIFIER_HF_REPO — alternative HF repo
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
signbridge/recognizer/landmark_classifier.py
CHANGED
|
@@ -21,19 +21,35 @@ import numpy as np
|
|
| 21 |
|
| 22 |
logger = logging.getLogger(__name__)
|
| 23 |
|
| 24 |
-
#
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
)
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
_lock = threading.Lock()
|
| 39 |
_state: dict[str, object] = {"loaded": False, "landmarker": None, "mlp": None, "classes": None}
|
|
@@ -57,15 +73,14 @@ def _ensure_loaded() -> bool:
|
|
| 57 |
if _state["loaded"]:
|
| 58 |
return _state["landmarker"] is not None and _state["mlp"] is not None
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
|
|
|
| 62 |
_state["loaded"] = True
|
| 63 |
return False
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
_HAND_MODEL_PATH,
|
| 68 |
-
)
|
| 69 |
_state["loaded"] = True
|
| 70 |
return False
|
| 71 |
|
|
@@ -80,14 +95,14 @@ def _ensure_loaded() -> bool:
|
|
| 80 |
return False
|
| 81 |
|
| 82 |
opts = vision.HandLandmarkerOptions(
|
| 83 |
-
base_options=BaseOptions(model_asset_path=str(
|
| 84 |
num_hands=1,
|
| 85 |
min_hand_detection_confidence=0.3,
|
| 86 |
min_hand_presence_confidence=0.3,
|
| 87 |
)
|
| 88 |
landmarker = vision.HandLandmarker.create_from_options(opts)
|
| 89 |
|
| 90 |
-
ckpt = torch.load(str(
|
| 91 |
n_in = int(ckpt["n_in"])
|
| 92 |
n_out = int(ckpt["n_out"])
|
| 93 |
|
|
|
|
| 21 |
|
| 22 |
logger = logging.getLogger(__name__)
|
| 23 |
|
| 24 |
+
# Weights live in a public HF model repo so the Space repo stays small and
|
| 25 |
+
# free of LFS. Local clones can override via the env vars below for offline
|
| 26 |
+
# tests; the default behaviour is `hf_hub_download` on first call (cached
|
| 27 |
+
# under HF_HOME / ~/.cache/huggingface).
|
| 28 |
+
_HF_REPO = os.getenv("SIGNBRIDGE_CLASSIFIER_HF_REPO", "LucasLooTan/signbridge-asl-classifier")
|
| 29 |
+
_MLP_FILENAME = os.getenv("SIGNBRIDGE_MLP_FILENAME", "asl_landmark_mlp.pt")
|
| 30 |
+
_HAND_FILENAME = os.getenv("SIGNBRIDGE_HAND_FILENAME", "hand_landmarker.task")
|
| 31 |
+
_MLP_LOCAL_OVERRIDE = os.getenv("SIGNBRIDGE_LANDMARK_MLP_PATH")
|
| 32 |
+
_HAND_LOCAL_OVERRIDE = os.getenv("SIGNBRIDGE_HAND_LANDMARKER_PATH")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _resolve_weight(local_override: str | None, filename: str) -> Path | None:
|
| 36 |
+
"""Return a local Path for a weight file, downloading from HF Hub if needed."""
|
| 37 |
+
if local_override:
|
| 38 |
+
p = Path(local_override)
|
| 39 |
+
if p.exists():
|
| 40 |
+
return p
|
| 41 |
+
logger.warning("override %s does not exist; falling back to HF Hub.", local_override)
|
| 42 |
+
try:
|
| 43 |
+
from huggingface_hub import hf_hub_download
|
| 44 |
+
except ImportError:
|
| 45 |
+
logger.warning("huggingface_hub missing; cannot fetch %s.", filename)
|
| 46 |
+
return None
|
| 47 |
+
try:
|
| 48 |
+
local = hf_hub_download(repo_id=_HF_REPO, filename=filename, repo_type="model")
|
| 49 |
+
return Path(local)
|
| 50 |
+
except Exception as exc: # noqa: BLE001 — HF Hub can fail for many reasons
|
| 51 |
+
logger.warning("hf_hub_download(%s) failed: %s", filename, type(exc).__name__)
|
| 52 |
+
return None
|
| 53 |
|
| 54 |
_lock = threading.Lock()
|
| 55 |
_state: dict[str, object] = {"loaded": False, "landmarker": None, "mlp": None, "classes": None}
|
|
|
|
| 73 |
if _state["loaded"]:
|
| 74 |
return _state["landmarker"] is not None and _state["mlp"] is not None
|
| 75 |
|
| 76 |
+
mlp_path = _resolve_weight(_MLP_LOCAL_OVERRIDE, _MLP_FILENAME)
|
| 77 |
+
if mlp_path is None:
|
| 78 |
+
logger.info("MLP weights unavailable; landmark classifier disabled.")
|
| 79 |
_state["loaded"] = True
|
| 80 |
return False
|
| 81 |
+
hand_path = _resolve_weight(_HAND_LOCAL_OVERRIDE, _HAND_FILENAME)
|
| 82 |
+
if hand_path is None:
|
| 83 |
+
logger.info("hand_landmarker.task unavailable; landmark classifier disabled.")
|
|
|
|
|
|
|
| 84 |
_state["loaded"] = True
|
| 85 |
return False
|
| 86 |
|
|
|
|
| 95 |
return False
|
| 96 |
|
| 97 |
opts = vision.HandLandmarkerOptions(
|
| 98 |
+
base_options=BaseOptions(model_asset_path=str(hand_path)),
|
| 99 |
num_hands=1,
|
| 100 |
min_hand_detection_confidence=0.3,
|
| 101 |
min_hand_presence_confidence=0.3,
|
| 102 |
)
|
| 103 |
landmarker = vision.HandLandmarker.create_from_options(opts)
|
| 104 |
|
| 105 |
+
ckpt = torch.load(str(mlp_path), map_location="cpu", weights_only=False)
|
| 106 |
n_in = int(ckpt["n_in"])
|
| 107 |
n_out = int(ckpt["n_out"])
|
| 108 |
|