feat(pose): fist kill-switch, default off
CI build oscope-of / build-check (push) Has been cancelled

User request live 2026-07-02: disable the fist gesture completely
(false held-fist actions during play). fist is forced to 0.0 at the
extractor unless FIST_ENABLE=1; continuous features unchanged. SC
held-fist actions (reload/next) go silent without touching SC.
This commit is contained in:
L'électron rare
2026-07-02 14:22:31 +02:00
parent ba4a1bb103
commit 4ea0bb2c2a
2 changed files with 33 additions and 7 deletions
+13 -3
View File
@@ -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
+20 -4
View File
@@ -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