diff --git a/data_only_viz/hand_features.py b/data_only_viz/hand_features.py index 0144759..bb7650d 100644 --- a/data_only_viz/hand_features.py +++ b/data_only_viz/hand_features.py @@ -43,11 +43,21 @@ def _clamp(v: float, lo: float, hi: float) -> float: class HandFeatureExtractor: - def __init__(self, history: int = 5): + def __init__(self, history: int = 5, fist_enable: bool | None = None): self._history = max(2, history) # two routed slots (hand_slots.route_hands): 0 = user's left (L), # 1 = user's right (R); cx fallback when chirality is absent. self._buf = [deque(maxlen=self._history), deque(maxlen=self._history)] + # Master fist kill-switch: user-disabled live 2026-07-02 (too many + # false held-fist actions). fist stays 0.0 for BOTH hands whatever + # the per-slot fist_enabled kwarg says. FIST_ENABLE=1 re-enables. + # TODO: migrate the env read to VizConfig once config.py is free. + if fist_enable is None: + import os + fist_enable = os.environ.get("FIST_ENABLE", "0") not in ( + "0", "", "false", "False", + ) + self._fist_enable = fist_enable def _features(self, lm: list) -> dict: xs = [_finite(_coord(p, "x", 0), 0.5) for p in lm[:21]] @@ -98,7 +108,7 @@ class HandFeatureExtractor: if left_lm is not None: lf = self._features(left_lm) - if not fist_enabled[0]: + if not (self._fist_enable and fist_enabled[0]): lf["fist"] = 0.0 lf["speed"] = self._speed(0, lf["cx"], lf["cy"]) out_l = lf @@ -107,7 +117,7 @@ class HandFeatureExtractor: if right_lm is not None: rf = self._features(right_lm) - if not fist_enabled[1]: + if not (self._fist_enable and fist_enabled[1]): rf["fist"] = 0.0 rf["speed"] = self._speed(1, rf["cx"], rf["cy"]) out_r = rf diff --git a/data_only_viz/tests/test_hand_features.py b/data_only_viz/tests/test_hand_features.py index 30e69cd..4844ab1 100644 --- a/data_only_viz/tests/test_hand_features.py +++ b/data_only_viz/tests/test_hand_features.py @@ -117,19 +117,19 @@ def _fist_hand(tip_ext): def test_fist_all_fingers_curled(): - out = HandFeatureExtractor().step([_fist_hand(0.20)]) # tips at knuckles + out = HandFeatureExtractor(fist_enable=True).step([_fist_hand(0.20)]) # tips at knuckles assert (out["L"] or out["R"])["fist"] > 0.9 def test_open_hand_is_not_a_fist(): - out = HandFeatureExtractor().step([_fist_hand(0.80)]) # fingers extended + out = HandFeatureExtractor(fist_enable=True).step([_fist_hand(0.80)]) # fingers extended assert (out["L"] or out["R"])["fist"] < 0.1 def test_one_extended_finger_breaks_the_fist(): h = _fist_hand(0.20) # all curled... h[8] = LM(0.5 + 0.80 * 0.10, 0.5) # ...except the index - out = HandFeatureExtractor().step([h]) + out = HandFeatureExtractor(fist_enable=True).step([h]) assert (out["L"] or out["R"])["fist"] < 0.1 # min over fingers @@ -171,7 +171,7 @@ def test_fist_enabled_false_zeroes_fist_preserves_openness(): def test_fist_enabled_true_computes_fist(): """fist_enabled=(True, True) leaves fist computation unchanged.""" - ext = HandFeatureExtractor() + ext = HandFeatureExtractor(fist_enable=True) h = _fist_hand(0.20) out = ext.step([h], fist_enabled=(True, True)) hand = out["L"] or out["R"] @@ -225,3 +225,19 @@ def test_fist_enabled_per_slot_independence(): assert out["L"]["fist"] == 0.0 # R slot (cx~0.8) should compute normally (open -> fist < 0.1) assert out["R"]["fist"] < 0.1 + + +def test_fist_kill_switch_default_off(monkeypatch): + """FIST_ENABLE unset -> fist forced 0.0 even on a perfect fist + (user-disabled live 2026-07-02); continuous features unaffected.""" + monkeypatch.delenv("FIST_ENABLE", raising=False) + out = HandFeatureExtractor().step([_fist_hand(0.20)]) + hand = out["L"] or out["R"] + assert hand["fist"] == 0.0 + assert hand["openness"] < 0.2 + + +def test_fist_kill_switch_env_reenable(monkeypatch): + monkeypatch.setenv("FIST_ENABLE", "1") + out = HandFeatureExtractor().step([_fist_hand(0.20)]) + assert (out["L"] or out["R"])["fist"] > 0.9