lilblueyes commited on
Commit
1122f32
·
1 Parent(s): b34cf5c

Integrate ASL TFLite model assets

Browse files
.gitignore CHANGED
@@ -2,3 +2,4 @@ __pycache__/
2
  *.py[cod]
3
  .pytest_cache/
4
  data/examples/*.mp4
 
 
2
  *.py[cod]
3
  .pytest_cache/
4
  data/examples/*.mp4
5
+ external/
README.md CHANGED
@@ -66,11 +66,18 @@ python3 scripts/test_asl_brick.py --gloss-override "I LOVE YOU"
66
 
67
  ## ASL model files
68
 
69
- Place the ASL classifier assets here when available:
70
 
71
  ```text
72
  data/models/asl/model.tflite
73
  data/models/asl/train.csv
 
 
 
 
 
 
 
74
  ```
75
 
76
  Without these files, the ASL brick still samples frames and emits `model_missing` diagnostics.
 
66
 
67
  ## ASL model files
68
 
69
+ The ASL classifier assets are stored under:
70
 
71
  ```text
72
  data/models/asl/model.tflite
73
  data/models/asl/train.csv
74
+ data/models/asl/sign_to_prediction_index_map.json
75
+ ```
76
+
77
+ They come from:
78
+
79
+ ```text
80
+ https://github.com/jamesjbustos/sign-language-recognition
81
  ```
82
 
83
  Without these files, the ASL brick still samples frames and emits `model_missing` diagnostics.
data/models/asl/model.tflite ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:af631244b8a7595a1925c8c7cd2b6b20b3086f6783061041d85f5bddbcb94da1
3
+ size 3383908
data/models/asl/sign_to_prediction_index_map.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"TV": 0, "after": 1, "airplane": 2, "all": 3, "alligator": 4, "animal": 5, "another": 6, "any": 7, "apple": 8, "arm": 9, "aunt": 10, "awake": 11, "backyard": 12, "bad": 13, "balloon": 14, "bath": 15, "because": 16, "bed": 17, "bedroom": 18, "bee": 19, "before": 20, "beside": 21, "better": 22, "bird": 23, "black": 24, "blow": 25, "blue": 26, "boat": 27, "book": 28, "boy": 29, "brother": 30, "brown": 31, "bug": 32, "bye": 33, "callonphone": 34, "can": 35, "car": 36, "carrot": 37, "cat": 38, "cereal": 39, "chair": 40, "cheek": 41, "child": 42, "chin": 43, "chocolate": 44, "clean": 45, "close": 46, "closet": 47, "cloud": 48, "clown": 49, "cow": 50, "cowboy": 51, "cry": 52, "cut": 53, "cute": 54, "dad": 55, "dance": 56, "dirty": 57, "dog": 58, "doll": 59, "donkey": 60, "down": 61, "drawer": 62, "drink": 63, "drop": 64, "dry": 65, "dryer": 66, "duck": 67, "ear": 68, "elephant": 69, "empty": 70, "every": 71, "eye": 72, "face": 73, "fall": 74, "farm": 75, "fast": 76, "feet": 77, "find": 78, "fine": 79, "finger": 80, "finish": 81, "fireman": 82, "first": 83, "fish": 84, "flag": 85, "flower": 86, "food": 87, "for": 88, "frenchfries": 89, "frog": 90, "garbage": 91, "gift": 92, "giraffe": 93, "girl": 94, "give": 95, "glasswindow": 96, "go": 97, "goose": 98, "grandma": 99, "grandpa": 100, "grass": 101, "green": 102, "gum": 103, "hair": 104, "happy": 105, "hat": 106, "hate": 107, "have": 108, "haveto": 109, "head": 110, "hear": 111, "helicopter": 112, "hello": 113, "hen": 114, "hesheit": 115, "hide": 116, "high": 117, "home": 118, "horse": 119, "hot": 120, "hungry": 121, "icecream": 122, "if": 123, "into": 124, "jacket": 125, "jeans": 126, "jump": 127, "kiss": 128, "kitty": 129, "lamp": 130, "later": 131, "like": 132, "lion": 133, "lips": 134, "listen": 135, "look": 136, "loud": 137, "mad": 138, "make": 139, "man": 140, "many": 141, "milk": 142, "minemy": 143, "mitten": 144, "mom": 145, "moon": 146, "morning": 147, "mouse": 148, "mouth": 149, "nap": 150, "napkin": 151, "night": 152, "no": 153, "noisy": 154, "nose": 155, "not": 156, "now": 157, "nuts": 158, "old": 159, "on": 160, "open": 161, "orange": 162, "outside": 163, "owie": 164, "owl": 165, "pajamas": 166, "pen": 167, "pencil": 168, "penny": 169, "person": 170, "pig": 171, "pizza": 172, "please": 173, "police": 174, "pool": 175, "potty": 176, "pretend": 177, "pretty": 178, "puppy": 179, "puzzle": 180, "quiet": 181, "radio": 182, "rain": 183, "read": 184, "red": 185, "refrigerator": 186, "ride": 187, "room": 188, "sad": 189, "same": 190, "say": 191, "scissors": 192, "see": 193, "shhh": 194, "shirt": 195, "shoe": 196, "shower": 197, "sick": 198, "sleep": 199, "sleepy": 200, "smile": 201, "snack": 202, "snow": 203, "stairs": 204, "stay": 205, "sticky": 206, "store": 207, "story": 208, "stuck": 209, "sun": 210, "table": 211, "talk": 212, "taste": 213, "thankyou": 214, "that": 215, "there": 216, "think": 217, "thirsty": 218, "tiger": 219, "time": 220, "tomorrow": 221, "tongue": 222, "tooth": 223, "toothbrush": 224, "touch": 225, "toy": 226, "tree": 227, "uncle": 228, "underwear": 229, "up": 230, "vacuum": 231, "wait": 232, "wake": 233, "water": 234, "wet": 235, "weus": 236, "where": 237, "white": 238, "who": 239, "why": 240, "will": 241, "wolf": 242, "yellow": 243, "yes": 244, "yesterday": 245, "yourself": 246, "yucky": 247, "zebra": 248, "zipper": 249}
data/models/asl/train.csv ADDED
The diff for this file is too large to render. See raw diff
 
requirements-asl-full.txt CHANGED
@@ -1,7 +1 @@
1
  -r requirements.txt
2
-
3
- protobuf>=4.25,<5.0
4
- mediapipe==0.10.14
5
- tensorflow-cpu==2.17.1
6
- deepface==0.0.100
7
- tf-keras==2.17.0
 
1
  -r requirements.txt
 
 
 
 
 
 
requirements.txt CHANGED
@@ -1,13 +1,35 @@
1
  --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu
2
  --prefer-binary
3
 
4
- gradio
5
  qwen-tts
6
  soundfile
7
  torch
8
  huggingface-hub
9
  llama-cpp-python
10
- numpy>=1.26,<2.0
11
  pandas>=2.1,<3.0
12
- opencv-python-headless==4.11.0.86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  pytest>=8.0,<9.0
 
1
  --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu
2
  --prefer-binary
3
 
4
+ gradio>=5.50,<6.0
5
  qwen-tts
6
  soundfile
7
  torch
8
  huggingface-hub
9
  llama-cpp-python
10
+ numpy==1.26.4
11
  pandas>=2.1,<3.0
12
+ protobuf==4.25.9
13
+ opencv-contrib-python==4.11.0.86
14
+ attrs>=26.1,<27
15
+ mediapipe==0.10.14
16
+ tensorflow-cpu==2.17.1
17
+ tf-keras==2.17.0
18
+ Flask
19
+ flask-cors
20
+ gdown
21
+ mtcnn
22
+ fire
23
+ gunicorn
24
+ lightphe
25
+ lightdsa
26
+ python-dotenv
27
+ beautifulsoup4
28
+ PySocks
29
+ joblib
30
+ lz4
31
+ sympy
32
+ deepface==0.0.100
33
+ retina-face==0.0.18
34
+ pillow==11.3.0
35
  pytest>=8.0,<9.0
signspeak/asl/asl_detector.py CHANGED
@@ -1,5 +1,6 @@
1
  from __future__ import annotations
2
 
 
3
  from pathlib import Path
4
  from typing import Any
5
 
@@ -16,6 +17,7 @@ class ASLDetector:
16
  self.model_dir = Path(configured_model_dir) if configured_model_dir else default_model_dir
17
  self.model_path = self.model_dir / "model.tflite"
18
  self.train_csv_path = self.model_dir / "train.csv"
 
19
  self.labels = self._load_labels()
20
 
21
  def predict_from_frames(self, frames: list[np.ndarray]) -> dict[str, Any]:
@@ -44,14 +46,7 @@ class ASLDetector:
44
 
45
  try:
46
  interpreter = self._load_interpreter()
47
- interpreter.allocate_tensors()
48
- input_details = interpreter.get_input_details()
49
- output_details = interpreter.get_output_details()
50
-
51
- input_data = self._prepare_input(keypoints, input_details[0])
52
- interpreter.set_tensor(input_details[0]["index"], input_data)
53
- interpreter.invoke()
54
- output = interpreter.get_tensor(output_details[0]["index"])
55
 
56
  probs = self._softmax_if_needed(np.asarray(output).reshape(-1))
57
  top_idx = int(np.argmax(probs))
@@ -86,6 +81,34 @@ class ASLDetector:
86
 
87
  return Interpreter(model_path=str(self.model_path))
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  def _prepare_input(self, keypoints: np.ndarray, input_detail: dict[str, Any]) -> np.ndarray:
90
  shape = input_detail.get("shape")
91
  dtype = input_detail.get("dtype", np.float32)
@@ -117,6 +140,16 @@ class ASLDetector:
117
  return fitted.reshape(target_shape)
118
 
119
  def _load_labels(self) -> list[str]:
 
 
 
 
 
 
 
 
 
 
120
  if not self.train_csv_path.exists():
121
  return []
122
  try:
 
1
  from __future__ import annotations
2
 
3
+ import json
4
  from pathlib import Path
5
  from typing import Any
6
 
 
17
  self.model_dir = Path(configured_model_dir) if configured_model_dir else default_model_dir
18
  self.model_path = self.model_dir / "model.tflite"
19
  self.train_csv_path = self.model_dir / "train.csv"
20
+ self.sign_map_path = self.model_dir / "sign_to_prediction_index_map.json"
21
  self.labels = self._load_labels()
22
 
23
  def predict_from_frames(self, frames: list[np.ndarray]) -> dict[str, Any]:
 
46
 
47
  try:
48
  interpreter = self._load_interpreter()
49
+ output = self._predict(interpreter, keypoints)
 
 
 
 
 
 
 
50
 
51
  probs = self._softmax_if_needed(np.asarray(output).reshape(-1))
52
  top_idx = int(np.argmax(probs))
 
81
 
82
  return Interpreter(model_path=str(self.model_path))
83
 
84
+ def _predict(self, interpreter: Any, keypoints: np.ndarray) -> np.ndarray:
85
+ signatures = interpreter.get_signature_list() if hasattr(interpreter, "get_signature_list") else {}
86
+ if "serving_default" in signatures:
87
+ prediction_fn = interpreter.get_signature_runner("serving_default")
88
+ input_name = self._signature_input_name(signatures["serving_default"])
89
+ input_data = np.nan_to_num(keypoints, nan=0.0, posinf=0.0, neginf=0.0).astype(np.float32)
90
+ prediction = prediction_fn(**{input_name: input_data})
91
+ output_name = self._signature_output_name(prediction)
92
+ return np.asarray(prediction[output_name])
93
+
94
+ interpreter.allocate_tensors()
95
+ input_details = interpreter.get_input_details()
96
+ output_details = interpreter.get_output_details()
97
+
98
+ input_data = self._prepare_input(keypoints, input_details[0])
99
+ interpreter.set_tensor(input_details[0]["index"], input_data)
100
+ interpreter.invoke()
101
+ return np.asarray(interpreter.get_tensor(output_details[0]["index"]))
102
+
103
+ def _signature_input_name(self, signature: dict[str, Any]) -> str:
104
+ inputs = signature.get("inputs") or ["inputs"]
105
+ return inputs[0]
106
+
107
+ def _signature_output_name(self, prediction: dict[str, Any]) -> str:
108
+ if "outputs" in prediction:
109
+ return "outputs"
110
+ return next(iter(prediction))
111
+
112
  def _prepare_input(self, keypoints: np.ndarray, input_detail: dict[str, Any]) -> np.ndarray:
113
  shape = input_detail.get("shape")
114
  dtype = input_detail.get("dtype", np.float32)
 
140
  return fitted.reshape(target_shape)
141
 
142
  def _load_labels(self) -> list[str]:
143
+ if self.sign_map_path.exists():
144
+ try:
145
+ sign_to_index = json.loads(self.sign_map_path.read_text(encoding="utf-8"))
146
+ labels = [""] * len(sign_to_index)
147
+ for sign, index in sign_to_index.items():
148
+ labels[int(index)] = str(sign)
149
+ return labels
150
+ except Exception:
151
+ pass
152
+
153
  if not self.train_csv_path.exists():
154
  return []
155
  try:
tests/test_asl_detector.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+
3
+ import numpy as np
4
+
5
+ from signspeak.asl.asl_detector import ASLDetector
6
+
7
+
8
+ def test_load_labels_prefers_prediction_index_map(tmp_path):
9
+ model_dir = tmp_path / "asl"
10
+ model_dir.mkdir()
11
+ (model_dir / "sign_to_prediction_index_map.json").write_text(
12
+ json.dumps({"love": 2, "hello": 0, "thanks": 1}),
13
+ encoding="utf-8",
14
+ )
15
+
16
+ detector = ASLDetector(model_dir=model_dir)
17
+
18
+ assert detector.labels == ["hello", "thanks", "love"]
19
+
20
+
21
+ class FakeSignatureInterpreter:
22
+ def get_signature_list(self):
23
+ return {"serving_default": {"inputs": ["inputs"], "outputs": ["outputs"]}}
24
+
25
+ def get_signature_runner(self, signature_name):
26
+ assert signature_name == "serving_default"
27
+
28
+ def predict(**kwargs):
29
+ assert kwargs["inputs"].shape == (543, 3)
30
+ return {"outputs": np.asarray([[0.1, 0.8, 0.1]], dtype=np.float32)}
31
+
32
+ return predict
33
+
34
+
35
+ def test_predict_uses_tflite_signature_runner(tmp_path):
36
+ detector = ASLDetector(model_dir=tmp_path)
37
+ keypoints = np.zeros((543, 3), dtype=np.float32)
38
+
39
+ output = detector._predict(FakeSignatureInterpreter(), keypoints)
40
+
41
+ assert output.shape == (1, 3)
42
+ assert float(output[0][1]) == np.float32(0.8)