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"