LucasLooTan Claude Opus 4.7 (1M context) commited on
Commit
ca3d3de
·
1 Parent(s): f90fef2

fix: download MLP + MediaPipe weights from HF Hub instead of bundling

Browse files

HF 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
- # Model files. Override via env for HF Space deploys.
25
- _MLP_PATH = Path(
26
- os.getenv(
27
- "SIGNBRIDGE_LANDMARK_MLP_PATH",
28
- str(Path(__file__).resolve().parent.parent.parent / "models" / "asl_landmark_mlp.pt"),
29
- )
30
- )
31
- _HAND_MODEL_PATH = Path(
32
- os.getenv(
33
- "SIGNBRIDGE_HAND_LANDMARKER_PATH",
34
- str(Path(__file__).resolve().parent.parent.parent / "models" / "hand_landmarker.task"),
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
- if not _MLP_PATH.exists():
61
- logger.info("landmark MLP weights missing at %s; classifier disabled.", _MLP_PATH)
 
62
  _state["loaded"] = True
63
  return False
64
- if not _HAND_MODEL_PATH.exists():
65
- logger.info(
66
- "MediaPipe hand_landmarker.task missing at %s; classifier disabled.",
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(_HAND_MODEL_PATH)),
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(_MLP_PATH), map_location="cpu", weights_only=False)
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