caa41fd7f1
CI build oscope-of / build-check (push) Has been cancelled
Detection v3 review follow-ups: pin hand_persist_grace/near_off/ hold_frames defaults; use the module-level route_hands (restores the monkeypatch seam, drops the dead local alias); document the grace count-inheritance window in the gate docstring.
368 lines
12 KiB
Python
368 lines
12 KiB
Python
"""Tests for data_only_viz.config — VizConfig defaults, bool conventions, round-trip."""
|
|
from __future__ import annotations
|
|
|
|
import threading
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from data_only_viz.config import VizConfig
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Default values
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_defaults_mediapipe():
|
|
cfg = VizConfig.from_env({})
|
|
assert cfg.mediapipe_delegate == "gpu"
|
|
assert cfg.video_rotate == "none"
|
|
|
|
|
|
def test_defaults_pose_discrimination():
|
|
cfg = VizConfig.from_env({})
|
|
assert cfg.pose_ghost_min_visible == 10
|
|
assert cfg.pose_ghost_min_conf == 0.5
|
|
assert cfg.pose_hand_min_visible == 15
|
|
assert cfg.pose_face_min_visible == 50
|
|
assert cfg.pose_nms_iou == 0.7
|
|
|
|
|
|
def test_defaults_pose_filter():
|
|
cfg = VizConfig.from_env({})
|
|
assert cfg.pose_filter is None
|
|
assert cfg.pose_filter_face is None
|
|
assert cfg.pose_filter_hand is None
|
|
|
|
|
|
def test_defaults_finger_piano():
|
|
cfg = VizConfig.from_env({})
|
|
assert cfg.finger_piano is False
|
|
assert cfg.finger_strike_vel == pytest.approx(0.02)
|
|
assert cfg.finger_strike_refractory_ms == pytest.approx(120.0)
|
|
assert cfg.finger_source == "auto"
|
|
assert cfg.finger_debug is False
|
|
|
|
|
|
def test_defaults_pinch():
|
|
cfg = VizConfig.from_env({})
|
|
assert cfg.pinch_enable is False
|
|
assert cfg.pinch_ratio_on == pytest.approx(0.45)
|
|
assert cfg.pinch_ratio_off == pytest.approx(0.65)
|
|
assert cfg.pinch_refractory_ms == pytest.approx(250.0)
|
|
assert cfg.pinch_margin == pytest.approx(0.20)
|
|
assert cfg.pinch_ext_ratio == pytest.approx(1.35)
|
|
assert cfg.pinch_ext_min == 2
|
|
assert cfg.pinch_debounce_frames == 3
|
|
|
|
|
|
def test_defaults_hand_display():
|
|
cfg = VizConfig.from_env({})
|
|
assert cfg.hand_conf_min == pytest.approx(0.45)
|
|
assert cfg.hand_persist_frames == 3
|
|
# HAND_SWAP_LR validated correct 2026-07-02: default OFF
|
|
assert cfg.hand_swap_lr is False
|
|
assert cfg.hand_near_min == pytest.approx(0.10)
|
|
assert cfg.arkit_bone_max == pytest.approx(0.5)
|
|
assert cfg.arkit_full_skeleton is True
|
|
# detection v3 stabilizer knobs
|
|
assert cfg.hand_persist_grace == 2
|
|
assert cfg.hand_near_off == pytest.approx(0.08)
|
|
assert cfg.hand_hold_frames == 2
|
|
|
|
|
|
def test_defaults_misc():
|
|
cfg = VizConfig.from_env({})
|
|
assert cfg.viz_hud is False
|
|
assert cfg.iphone_osc_port == 57128
|
|
assert cfg.concert_mirror is True
|
|
|
|
|
|
def test_defaults_icp():
|
|
cfg = VizConfig.from_env({})
|
|
assert cfg.icp_fusion is False
|
|
assert cfg.icp_lidar_host is None
|
|
assert cfg.icp_lidar_port == 5500
|
|
assert cfg.icp_lidar_extrinsic is None
|
|
|
|
|
|
def test_defaults_multihmr():
|
|
cfg = VizConfig.from_env({})
|
|
assert cfg.multihmr_backend == "pytorch"
|
|
assert cfg.multihmr_loop_fps == pytest.approx(30.0)
|
|
assert cfg.multihmr_autocast is False
|
|
assert cfg.multihmr_remote_host == "192.168.0.175"
|
|
assert cfg.multihmr_remote_port == 57140
|
|
assert cfg.multihmr_remote_jpeg is True
|
|
assert cfg.multihmr_remote_jpeg_quality == 80
|
|
assert cfg.multihmr_remote_async is True
|
|
assert cfg.multihmr_reid == "dino"
|
|
assert cfg.multihmr_reid_alpha == pytest.approx(0.5)
|
|
|
|
|
|
def test_defaults_osc():
|
|
cfg = VizConfig.from_env({})
|
|
assert cfg.avbody_host == "127.0.0.1"
|
|
assert cfg.vdmx_osc_host is None
|
|
assert cfg.vdmx_osc_port == 1234
|
|
|
|
|
|
def test_defaults_worker_selection():
|
|
cfg = VizConfig.from_env({})
|
|
assert cfg.av_live_mediapipe is True
|
|
assert cfg.av_live_apple_vision is True
|
|
assert cfg.av_live_coreml is True
|
|
assert cfg.av_live_detrpose is False
|
|
assert cfg.av_live_parallel_pose == "both"
|
|
assert cfg.mesh_rig is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Bool convention: standard (not in ("0","","false","False") → True)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.mark.parametrize("val,expected", [
|
|
("0", False),
|
|
("", False),
|
|
("false", False),
|
|
("False", False),
|
|
("1", True),
|
|
("true", True),
|
|
("True", True),
|
|
("yes", True),
|
|
])
|
|
def test_bool_std_finger_piano(val, expected):
|
|
cfg = VizConfig.from_env({"FINGER_PIANO": val})
|
|
assert cfg.finger_piano is expected
|
|
|
|
|
|
@pytest.mark.parametrize("val,expected", [
|
|
("0", False),
|
|
("", False),
|
|
("false", False),
|
|
("False", False),
|
|
("1", True),
|
|
])
|
|
def test_bool_std_pinch_enable(val, expected):
|
|
cfg = VizConfig.from_env({"PINCH_ENABLE": val})
|
|
assert cfg.pinch_enable is expected
|
|
|
|
|
|
# HAND_SWAP_LR: standard bool, default False
|
|
@pytest.mark.parametrize("val,expected", [
|
|
("0", False),
|
|
("", False),
|
|
("false", False),
|
|
("False", False),
|
|
("1", True),
|
|
])
|
|
def test_bool_std_hand_swap_lr(val, expected):
|
|
cfg = VizConfig.from_env({"HAND_SWAP_LR": val})
|
|
assert cfg.hand_swap_lr is expected
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Bool convention: ne0 — != "0" (empty string is True)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.mark.parametrize("val,expected", [
|
|
("0", False),
|
|
("1", True),
|
|
("", True), # ne0: "" != "0" = True
|
|
("false", True), # ne0: "false" != "0" = True
|
|
])
|
|
def test_bool_ne0_concert_mirror(val, expected):
|
|
cfg = VizConfig.from_env({"CONCERT_MIRROR": val})
|
|
assert cfg.concert_mirror is expected
|
|
|
|
|
|
@pytest.mark.parametrize("val,expected", [
|
|
("0", False),
|
|
("1", True),
|
|
("", True), # ne0: enabled by default when not "0"
|
|
])
|
|
def test_bool_ne0_av_live_mediapipe(val, expected):
|
|
cfg = VizConfig.from_env({"AV_LIVE_MEDIAPIPE": val})
|
|
assert cfg.av_live_mediapipe is expected
|
|
|
|
|
|
# When AV_LIVE_MEDIAPIPE is not set, default = True
|
|
def test_bool_ne0_av_live_unset():
|
|
cfg = VizConfig.from_env({})
|
|
assert cfg.av_live_mediapipe is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Bool convention: eq1 — only "1" is True
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.mark.parametrize("val,expected", [
|
|
("1", True),
|
|
("0", False),
|
|
("true", False), # eq1: only exact "1"
|
|
("", False),
|
|
])
|
|
def test_bool_eq1_icp_fusion(val, expected):
|
|
cfg = VizConfig.from_env({"ICP_FUSION": val})
|
|
assert cfg.icp_fusion is expected
|
|
|
|
|
|
@pytest.mark.parametrize("val,expected", [
|
|
("1", True),
|
|
("0", False),
|
|
("true", False),
|
|
])
|
|
def test_bool_eq1_multihmr_autocast(val, expected):
|
|
cfg = VizConfig.from_env({"MULTIHMR_AUTOCAST": val})
|
|
assert cfg.multihmr_autocast is expected
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Bool convention: flag — in ("1","true","yes","on") → True
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.mark.parametrize("val,expected", [
|
|
("1", True),
|
|
("true", True),
|
|
("yes", True),
|
|
("on", True),
|
|
("0", False),
|
|
("false", False),
|
|
("no", False),
|
|
("off", False),
|
|
("", False),
|
|
])
|
|
def test_bool_flag_multihmr_remote_async(val, expected):
|
|
cfg = VizConfig.from_env({"MULTIHMR_REMOTE_ASYNC": val})
|
|
assert cfg.multihmr_remote_async is expected
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Numeric casts
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_int_cast():
|
|
cfg = VizConfig.from_env({"POSE_GHOST_MIN_VISIBLE": "7"})
|
|
assert cfg.pose_ghost_min_visible == 7
|
|
|
|
|
|
def test_float_cast():
|
|
cfg = VizConfig.from_env({"POSE_GHOST_MIN_CONF": "0.8"})
|
|
assert cfg.pose_ghost_min_conf == pytest.approx(0.8)
|
|
|
|
|
|
def test_int_cast_fallback_on_invalid():
|
|
cfg = VizConfig.from_env({"POSE_GHOST_MIN_VISIBLE": "notanumber"})
|
|
assert cfg.pose_ghost_min_visible == 10 # falls back to default
|
|
|
|
|
|
def test_float_cast_fallback_on_invalid():
|
|
cfg = VizConfig.from_env({"POSE_GHOST_MIN_CONF": "bad"})
|
|
assert cfg.pose_ghost_min_conf == pytest.approx(0.5)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Monkeypatched environ round-trip
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_roundtrip_via_monkeypatch(monkeypatch):
|
|
monkeypatch.setenv("FINGER_PIANO", "1")
|
|
monkeypatch.setenv("FINGER_STRIKE_VEL", "0.05")
|
|
monkeypatch.setenv("PINCH_EXT_RATIO", "1.5")
|
|
monkeypatch.setenv("PINCH_EXT_MIN", "4")
|
|
monkeypatch.setenv("HAND_SWAP_LR", "1")
|
|
cfg = VizConfig.from_env()
|
|
assert cfg.finger_piano is True
|
|
assert cfg.finger_strike_vel == pytest.approx(0.05)
|
|
assert cfg.pinch_ext_ratio == pytest.approx(1.5)
|
|
assert cfg.pinch_ext_min == 4
|
|
assert cfg.hand_swap_lr is True
|
|
|
|
|
|
def test_roundtrip_uses_live_environ(monkeypatch):
|
|
"""from_env() without args reads os.environ — picks up monkeypatches."""
|
|
monkeypatch.setenv("POSE_NMS_IOU", "0.55")
|
|
cfg = VizConfig.from_env()
|
|
assert cfg.pose_nms_iou == pytest.approx(0.55)
|
|
|
|
|
|
def test_custom_mapping_does_not_pollute_os_environ(monkeypatch):
|
|
"""Passing an explicit mapping leaves os.environ untouched."""
|
|
monkeypatch.delenv("FINGER_PIANO", raising=False)
|
|
cfg_custom = VizConfig.from_env({"FINGER_PIANO": "1"})
|
|
cfg_env = VizConfig.from_env()
|
|
assert cfg_custom.finger_piano is True
|
|
assert cfg_env.finger_piano is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Part B: MultiWorker with iphone_usb=True does not call landmarker builders
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_iphone_usb_skips_ensure_model():
|
|
"""_ensure_model must never be called when iphone_usb=True.
|
|
|
|
When iphone_usb is set, the entire MediaPipe block (import + model
|
|
download + landmarker construction) is gated out. We verify by
|
|
patching _ensure_model to raise on call, then running _run with
|
|
iphone_usb=True and a fake IphoneUSBSource that fails to start
|
|
(so _run returns early without entering the capture loop).
|
|
"""
|
|
import data_only_viz.multi as multi_mod
|
|
from data_only_viz.multi import MultiWorker
|
|
from data_only_viz.state import State
|
|
|
|
ensure_calls: list[str] = []
|
|
|
|
def fail_ensure(name: str) -> Path:
|
|
ensure_calls.append(name)
|
|
raise AssertionError(
|
|
f"_ensure_model({name!r}) called in iphone_usb mode — "
|
|
"MediaPipe must not be loaded under --iphone-usb"
|
|
)
|
|
|
|
# Minimal MultiWorker without calling __init__ (avoids ActionHead load)
|
|
w = object.__new__(MultiWorker)
|
|
w.iphone_usb = True
|
|
w._stop = threading.Event()
|
|
w.state = MagicMock(spec=State)
|
|
w.state.lock.return_value = MagicMock(__enter__=MagicMock(return_value=None),
|
|
__exit__=MagicMock(return_value=False))
|
|
w.period = 0.033
|
|
w.num_persons = 1
|
|
w.min_conf = 0.4
|
|
w.camera_index = 0
|
|
|
|
# fake_cap.start() returns False → _run returns after source setup
|
|
fake_cap = MagicMock()
|
|
fake_cap.start.return_value = False
|
|
|
|
with patch.object(multi_mod, "_ensure_model", fail_ensure):
|
|
# Patch the inline `from .iphone_usb_source import IphoneUSBSource`
|
|
# by pre-populating sys.modules so the local import resolves to our stub.
|
|
import sys
|
|
fake_src_mod = MagicMock()
|
|
fake_src_mod.IphoneUSBSource.return_value = fake_cap
|
|
with patch.dict(sys.modules,
|
|
{"data_only_viz.iphone_usb_source": fake_src_mod}):
|
|
multi_mod._run = None # guard against any indirect recursion
|
|
try:
|
|
w._run()
|
|
except Exception:
|
|
pass # early return from failed cap.start() is expected
|
|
|
|
assert ensure_calls == [], (
|
|
f"_ensure_model was called under iphone_usb=True: {ensure_calls}"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("val,expected", [
|
|
("0", False),
|
|
("1", True),
|
|
("", True), # ne0: "" != "0" = True (drift to _bool_std would break this)
|
|
])
|
|
def test_bool_ne0_mesh_rig(val, expected):
|
|
cfg = VizConfig.from_env({"MESH_RIG": val})
|
|
assert cfg.mesh_rig is expected
|