Sign2Voice / tests /test_asl_detector.py
lilblueyes's picture
Add sliding-window ASL phrase prototype
7d969c6
Raw
History Blame Contribute Delete
4.4 kB
import json
import numpy as np
import signspeak.asl.asl_detector as asl_detector_module
from signspeak.asl.asl_detector import ASLDetector
def test_load_labels_prefers_prediction_index_map(tmp_path):
model_dir = tmp_path / "asl"
model_dir.mkdir()
(model_dir / "sign_to_prediction_index_map.json").write_text(
json.dumps({"love": 2, "hello": 0, "thanks": 1}),
encoding="utf-8",
)
detector = ASLDetector(model_dir=model_dir)
assert detector.labels == ["hello", "thanks", "love"]
class FakeSignatureInterpreter:
def get_signature_list(self):
return {"serving_default": {"inputs": ["inputs"], "outputs": ["outputs"]}}
def get_signature_runner(self, signature_name):
assert signature_name == "serving_default"
def predict(**kwargs):
assert kwargs["inputs"].shape == (543, 3)
assert np.isnan(kwargs["inputs"][0][0])
return {"outputs": np.asarray([[0.1, 0.8, 0.1]], dtype=np.float32)}
return predict
def test_predict_uses_tflite_signature_runner(tmp_path):
detector = ASLDetector(model_dir=tmp_path)
keypoints = np.zeros((543, 3), dtype=np.float32)
keypoints[0][0] = np.nan
output = detector._predict(FakeSignatureInterpreter(), keypoints)
assert output.shape == (1, 3)
assert float(output[0][1]) == np.float32(0.8)
class FakeLandmarkResult:
keypoints = np.zeros((30, 543, 3), dtype=np.float32)
status = "ok"
detector = "fake"
error = None
class FakeLandmarksDetector:
def __init__(self, missing_value=0.0):
pass
def detect_sequence(self, frames):
return FakeLandmarkResult()
def close(self):
pass
def test_low_confidence_prediction_is_not_accepted(monkeypatch, tmp_path):
model_dir = tmp_path / "asl"
model_dir.mkdir()
(model_dir / "model.tflite").write_bytes(b"demo")
(model_dir / "sign_to_prediction_index_map.json").write_text(
json.dumps({"where": 0, "hello": 1}),
encoding="utf-8",
)
monkeypatch.setattr(asl_detector_module, "LandmarksDetector", FakeLandmarksDetector)
monkeypatch.setenv("ASL_CONFIDENCE_THRESHOLD", "0.70")
monkeypatch.setattr(ASLDetector, "_load_interpreter", lambda self: object())
monkeypatch.setattr(
ASLDetector,
"_predict",
lambda self, interpreter, keypoints: np.asarray([[0.667, 0.333]], dtype=np.float32),
)
result = ASLDetector(model_dir=model_dir).predict_from_frames([np.zeros((2, 2, 3), dtype=np.uint8)] * 30)
assert result["status"] == "low_confidence"
assert result["top_prediction"] == "where"
assert result["gloss_sequence"] == []
def test_sequence_prediction_collapses_accepted_windows(monkeypatch, tmp_path):
model_dir = tmp_path / "asl"
model_dir.mkdir()
(model_dir / "model.tflite").write_bytes(b"demo")
(model_dir / "sign_to_prediction_index_map.json").write_text(
json.dumps({"hello": 0, "water": 1}),
encoding="utf-8",
)
class SequenceLandmarkResult:
keypoints = np.zeros((60, 543, 3), dtype=np.float32)
status = "ok"
detector = "fake"
error = None
class SequenceLandmarksDetector:
def __init__(self, missing_value=0.0):
pass
def detect_sequence(self, frames):
return SequenceLandmarkResult()
def close(self):
pass
calls = iter(
[
np.asarray([[0.9, 0.1]], dtype=np.float32),
np.asarray([[0.86, 0.14]], dtype=np.float32),
np.asarray([[0.1, 0.9]], dtype=np.float32),
]
)
monkeypatch.setattr(asl_detector_module, "LandmarksDetector", SequenceLandmarksDetector)
monkeypatch.setenv("ASL_CONFIDENCE_THRESHOLD", "0.70")
monkeypatch.setenv("ASL_SEQUENCE_WINDOW", "30")
monkeypatch.setenv("ASL_SEQUENCE_STRIDE", "15")
monkeypatch.setattr(ASLDetector, "_load_interpreter", lambda self: object())
monkeypatch.setattr(ASLDetector, "_predict", lambda self, interpreter, keypoints: next(calls))
result = ASLDetector(model_dir=model_dir).predict_sequence_from_frames(
[np.zeros((2, 2, 3), dtype=np.uint8)] * 60
)
assert result["status"] == "ok"
assert result["gloss_sequence"] == ["hello", "water"]
assert result["windows_used"] == 3
assert result["recognition_mode"] == "sliding_window_sequence"