876 Commits

Author SHA1 Message Date
L'électron rare e2a41c0d8a test(viz): VizConfig and iphone-usb gating tests
61 tests: all VizConfig defaults, four bool conventions,
numeric cast fallback, monkeypatch round-trip, and Part B
gating (_ensure_model never called under --iphone-usb).
Fix hand_swap_lr from_env default: was True, now False.
2026-07-02 11:32:24 +02:00
L'électron rare 65bf3aad08 refactor(viz): remaining consumers read VizConfig
pose_filter _parse_env_* read POSE_FILTER* via VizConfig.
multi_hmr_worker reads MULTIHMR_BACKEND/AUTOCAST/REMOTE via VizConfig.
multihmr_remote reads JPEG/ASYNC/HOST/PORT via VizConfig.
smplx_osc_sender reads AVBODY_HOST/REID/ALPHA via VizConfig.
pose_bridge reads AVBODY_HOST/VDMX_* via VizConfig.
iphone_usb_source reads CONCERT_MIRROR via VizConfig.
lidar_calib reads ICP_LIDAR_EXTRINSIC via VizConfig.
multihmr_coreml reads COREML_COMPUTE_UNITS via VizConfig.
2026-07-02 11:26:57 +02:00
L'électron rare c0cde337c9 refactor(viz): consumers read VizConfig
multi.py reads 5 discrimination thresholds via VizConfig.
main.py reads ICP/Multi-HMR/AV_LIVE_* flags via VizConfig.
2026-07-02 11:24:19 +02:00
L'électron rare e7ce016688 feat(viz): central VizConfig from env
Add data_only_viz/config.py: frozen dataclass VizConfig with typed
fields for all 56 env reads in data_only_viz. from_env() reads
os.environ at call time — no module-level singleton, test-compat.

Migrate action_head_pub._init_finger_piano and renderer.initWithState_
to consume VizConfig. Keep renderer.py ARKIT_FULL module-level (test
uses MetalRenderer.__new__ without calling initWithState_).

HAND_SWAP_LR default changed False (was True): Vision chirality
validated correct live 2026-07-02; swap knob now defaults off.
2026-07-02 11:21:55 +02:00
L'électron rare 5d425b1fbd fix(pose): plumb swap into hand features slots
CI build oscope-of / build-check (push) Has been cancelled
Review C1 (Critical): /pose/hands per-voice L/R mods routed without
the HAND_SWAP_LR swap while pinch/strike used it — the two paths
contradicted each other on an inverted-chirality source. step() now
takes swap, the publisher passes its env-read value, two tests pin
the contract. Also hoists the route_hands imports (M5) and fixes the
stale slot comment (M1).
2026-07-02 11:09:06 +02:00
L'électron rare 32cdfa0d3e fix(pose): swap chirality only, default on
CI build oscope-of / build-check (push) Has been cancelled
User-confirmed live: ARBodyTracker Vision chirality arrives inverted
vs the mirrored view. HAND_SWAP_LR now inverts the chirality mapping
inside route_hands (cx fallback untouched, it is already screen
relative) and defaults to 1. Also: HandPersistenceGate greedy-match
limitation documented + malformed-entry guard (review follow-ups).
2026-07-02 11:00:25 +02:00
L'électron rare e20a4d130a docs: hand router env vars 2026-07-02 10:55:38 +02:00
L'électron rare 366d916e9d refactor(pose): detectors consume routed slots
Convert all four consumers to accept pre-routed [L|None, R|None]:
- finger_strike: remove order_hands_by_cx + _slot_hands; step()
  iterates enumerate(slotted), None slot resets state / synthesizes
  release edges in-place.
- hand_features: step(hands, chirality=None) delegates to route_hands;
  speed buffer indexed by slot, not cx order.
- action_head_pub: _pick_hands returns chirality; _emit_fingers/
  _emit_pinch call route_hands with HAND_SWAP_LR/HAND_NEAR_MIN;
  _emit_hands plumbs chirality to HandFeatureExtractor.
- renderer: panels block replaced with route_hands call; persistence
  gate + hand_plausible mask applied before routing so a ghost hand
  cannot steal a slot; HAND_SWAP_LR read at init.
Test updates: [hand, None] slot lists in test_finger_strike;
_hand() fixture in test_action_head_finger_emit gains realistic size
so near_min=0.10 gate passes.
2026-07-02 10:55:32 +02:00
L'électron rare 9ee7990dcd feat(pose): hand router chirality + near gate
New module hand_slots.route_hands():
- Chirality path: iPhone ARBodyTracker Vision 0/1 -> L/R slots,
  alignment preserved before invalid-hand filtering.
- Fallback: ascending cx sort (mirror-aware).
- swap=True: swaps the two slots at the very end (safety knob,
  HAND_SWAP_LR=1, if a source inverts Vision chirality).
- near_min gate: drops slots below wrist->middle-MCP distance.
18 new tests covering all branches.
2026-07-02 10:55:21 +02:00
L'électron rare 8bc7acbd14 fix(pose): fresh arkit joint arrays per frame
Extract apply_skeleton_joints() as a pure module-level function that
copies prev_arr (or zeros) and writes a new array, never mutating the
old one.  The TAG_SKELETON handler now stores the new array under the
state lock, so readers that captured a reference to the previous array
(multi.py shallow-copy + ArkitFuse.apply outside the lock) always see
a complete, consistent snapshot — eliminating the B4 tearing race.

6 new tests cover: shape, identity (is not), value preservation for
invalid joints, value application for valid joints, wrong-shape reset,
and no-mutation of prev_arr.
2026-07-02 10:41:41 +02:00
L'électron rare 0a4f51ebfb test(viz): pin arkit max-len behavior change
Review follow-up: make the v2 filter's rejection of legacy 0.707
diagonal fixtures explicit instead of silently retuned, and document
the close-up caveat on max_len (ARKIT_BONE_MAX raises it live).
2026-07-02 10:37:02 +02:00
L'électron rare 039abaa4a8 docs: hand filter env vars
CI build oscope-of / build-check (push) Has been cancelled
Add HAND_CONF_MIN, HAND_PERSIST_FRAMES, ARKIT_BONE_MAX to the
Environment variables table in the root CLAUDE.md.
2026-07-02 10:28:23 +02:00
L'électron rare 5bfc63f86d feat(viz): hand persistence gate + conf knobs
HandPersistenceGate (min_frames=3, radius=0.15) suppresses ghost
hands that flash for fewer than min_frames consecutive frames.
Gate is called once per frame in the renderer on persons_hands;
result filters both the video overlay loop and the side panels.

hand_plausible now rejects wrists outside [-0.1, 1.1] (out-of-frame
ghost anchor check).  panel_segments gains a conf_min param.

Env knobs read at renderer init (single parse, no per-frame cost):
  HAND_CONF_MIN (default 0.45, raised from 0.3)
  HAND_PERSIST_FRAMES (default 3)
  ARKIT_BONE_MAX (default 0.5, passed to arkit_segments)

New tests: 12 gate tests + 4 wrist-bounds tests.
2026-07-02 10:27:58 +02:00
L'électron rare ac6768d8a8 fix(viz): arkit bone bounds + length filter
arkit_segments now accepts max_len and bounds params.
Bones whose endpoints fall outside [-bounds, 1+bounds] or whose
length exceeds max_len are skipped (tracking-loss garbage).
Defaults: max_len=0.5, bounds=0.1.  Renderer will pass
max_len=ARKIT_BONE_MAX explicitly (Part 3).

Tests updated to use realistic bone lengths; new tests added
for the out-of-bounds and overlong-bone cases.
2026-07-02 10:27:49 +02:00
L'électron rare 287c4977a9 fix(viz): install key monitors at app launch
NSEvent keyboard monitors were placed at the END of
_start_pose_worker, unreachable: every backend path returns
early (lines 369/384/399/413/440) so monitors were never
installed. ESC, space (concertNext) and keymaps were silently
dead in all real concert paths.

Move the local + global monitor installation into
applicationDidFinishLaunching_, after window focus setup,
non-headless path only. The --multi-hmr headless early-return
(line 95) is unaffected.
2026-07-02 10:15:19 +02:00
L'électron rare b8ae1a668c fix(pose): read real smplx person dataclass
_read_sources used .get("pid")/.get("v3d") (dict API) but
multi_hmr_worker emits SMPLXPerson dataclasses (field: vertices_3d).
AttributeError was swallowed by LOG.exception at 30 Hz.

- Dual-read via isinstance check: dataclass path uses p.pid /
  p.vertices_3d / p.expression; dict path keeps p.get() unchanged
- 3 new tests: real SMPLXPerson emits OSC, expression field accepted,
  legacy dict format still works
2026-07-02 10:14:17 +02:00
L'électron rare 48b04084fa fix(viz): gate arkit overlay on 2d freshness
persons_arkit_2d_t was written by IphoneUSBSource but never
read. A USB drop left the frozen skeleton on screen indefinitely.

- Add arkit_2d_fresh(ts_by_pid, now, max_age=1.0) pure helper
  in hand_display.py (8 TDD tests, all green)
- renderer._update_skeleton: use_arkit now requires at least one
  pid younger than 1 s (replaces bool check)
- Per-pid freshness gate in draw loop skips stale pids
2026-07-02 10:13:05 +02:00
L'électron rare 06b868e377 docs(viz): refactor audit ref (chantiers 1-6)
CI build oscope-of / build-check (push) Has been cancelled
2026-07-02 10:07:00 +02:00
L'électron rare 0a2865e136 feat(viz): left/right hand front-view panels
CI build oscope-of / build-check (push) Has been cancelled
Add panel_rect/panel_segments/panel_frame helpers to hand_display.py
(PANEL_SIDE=0.30, PANEL_MARGIN=0.02, PANEL_INNER=0.10). Segments use
uniform pixel-space scale so the hand aspect ratio is preserved
regardless of view aspect. mirror=True flips X within the panel.

In renderer._update_skeleton add push_panel (no mirror flip) and a
panels block before the segs==0 epilogue: routes persons_hands to
left/right panel via chirality list when aligned, screen-cx fallback
otherwise. Draws frame (pid 7) + wireframe (pid 5 left / 6 right).
Runs on every path including use_arkit. Guards: _mp_bones None, empty
hands, misaligned chirality, height==0.

26 tests pass (10 new for panel helpers).
2026-07-02 09:56:08 +02:00
L'électron rare 3a3e6b113f feat(viz): hud debug panel off by default
The AV-Live data-only text HUD hid the lower-left corner of the
performance display and would collide with the new left-hand panel.
VIZ_HUD=1 re-enables it; the H key toggle guards the absent view.
2026-07-02 09:47:54 +02:00
L'électron rare ef719e76f3 feat(viz): filter hand overlay artifacts
Add hand_display.py with hand_plausible / segment_ok / hand_size pure
helpers; wire into both skeleton hands loops (plausibility gate +
per-segment confidence+spike filter + real min(A.c,B.c) push); remove
filled mesh triangles for hands from _update_mesh (HAND_TRIANGLES block
deleted).
2026-07-02 09:42:51 +02:00
L'électron rare 60640a881e feat(pose): decode hand confidence + chirality
_decode_hands now returns (hands, chirality) tuple instead of a bare
list. Landmark .z keeps the raw wire value; .c = clamp(z, 0, 1).
Chirality byte (1=right, else 0=left) is decoded per hand and stored
in State.persons_hands_chirality under the same lock as
persons_hands_iphone.
2026-07-02 09:37:24 +02:00
L'électron rare 29c66c7151 docs(viz): hand display cleanup spec + plan 2026-07-02 09:33:34 +02:00
L'électron rare 1f19a5db96 feat(pose): pinch gate env vars + docs
CI build oscope-of / build-check (push) Has been cancelled
2026-07-02 09:18:14 +02:00
L'électron rare ea42ba4bb3 feat(pose): pinch engage debounce 2026-07-02 09:14:18 +02:00
L'électron rare 021c53f078 feat(pose): pinch open-hand extension gate 2026-07-02 09:10:06 +02:00
L'électron rare 50bc6f5d71 docs(pose): pinch gate implementation plan 2026-07-02 09:05:26 +02:00
L'électron rare 1510a37059 docs(pose): pinch open-hand gate + debounce spec 2026-07-02 09:00:56 +02:00
clement 095fd51812 Merge branch 'feat/disable-matrix-pinch'
CI build oscope-of / build-check (push) Has been cancelled
2026-06-30 22:51:39 +02:00
clement 1491aef30e feat(matrix): disable finger pinch by default 2026-06-30 22:49:41 +02:00
clement 24ce960992 fix(iphone): mirror skeleton overlay to video 2026-06-30 22:40:31 +02:00
clement 08b7c72e49 feat(iphone): render full arkit body skeleton 2026-06-30 22:14:35 +02:00
clement 86c071d0c9 feat(iphone): arkit skeleton segment builder 2026-06-30 22:09:10 +02:00
clement 04eb182fab feat(iphone): store topology + 2d validity
Add arkit_joint_names, arkit_parents, persons_arkit_2d_valid
to State. Wire TAG_TOPOLOGY=7 dispatch in IphoneUSBSource:
decode_topology stored under lock; TAG_SKELETON2D branch now
also stores per-joint validity bool array (91,) alongside pts.
2026-06-30 22:05:32 +02:00
clement b00b6dfbda fix(iphone): topology decode return type + test 2026-06-30 22:02:35 +02:00
clement fbc8b55044 feat(iphone): python topology codec 2026-06-30 21:58:42 +02:00
clement 67f347e705 feat(iphone): emit skeleton topology on connect 2026-06-30 21:58:35 +02:00
clement 6f941c52cb feat(wire): topology payload + tag 2026-06-30 21:49:37 +02:00
clement b2dab64aae fix(iphone): sendable hand buffer handoff 2026-06-30 21:47:29 +02:00
clement bb90820988 docs(iphone): phase 1 faithful skeleton plan 2026-06-30 21:45:31 +02:00
clement 5adb8abdc8 docs(iphone): SOTA 2026 refonte design spec 2026-06-30 21:33:45 +02:00
L'électron rare 40c21a246c perf(iphone): drop face vision + RealityKit view
CI build oscope-of / build-check (push) Has been cancelled
ARKit-only-body mode no longer consumes face: the Mac dropped face
entirely (face=0, /pose/mouth gone), so VNDetectFaceLandmarksRequest
and the .face USB frames were pure wasted per-frame compute. Drop the
face request and its send.

The on-device ARView (RealityKit) rendered a black background behind
the SwiftUI skeleton overlay -- it never showed the camera -- so it
only spun up the RealityKit AR render pipeline (the engine:rematerial
console spam) to draw black. Remove the ARView; the screen stays
black + skeleton overlay, identical UX, minus the GPU/thermal cost.

Video (HEVC) and hands are kept: the Mac uses the iPhone video as the
visualizer backdrop and Vision hands drive pinch/fist gestures.
2026-06-30 20:31:10 +02:00
L'électron rare 2b7b4a81c6 docs(data-only-viz): arkit body source mode
CI build oscope-of / build-check (push) Has been cancelled
2026-06-30 20:14:17 +02:00
L'électron rare ef99c32cac fix(iphone): explicit SIMD2<Float> for skeleton2D
CI build oscope-of / build-check (push) Has been cancelled
2026-06-30 19:42:22 +02:00
L'électron rare 27d5ce9d98 feat(iphone): send 2D projected skeleton
Add camera: ARCamera param to publishUSB and thread frame.camera
through from the session delegate. After the existing .skeleton
send, project all 91 joints to viewport 2D, normalize to [0,1],
and send as Skeleton2DPayload / FrameTag.skeleton2D (tag 6).
2026-06-30 19:41:02 +02:00
L'électron rare 33de64f599 chore(launcher): drop arkit_fuse, arkit is source 2026-06-30 19:32:04 +02:00
L'électron rare 5a9a6338a0 feat(pose): arkit body, skip mediapipe pose+face
Under --iphone-usb, skip all three MediaPipe detectors
(pose, face, hand) and rebuild the MP-33 body from
state.persons_arkit_{2d,joints} via new arkit_body_2d /
arkit_body_3d builders. Add head anchor (ARKIT_HEAD_IDX=51,
needs live verify) as MP33 slot 0. Webcam path unchanged.
2026-06-30 19:29:20 +02:00
L'électron rare c49de5c500 feat(pose): decode iphone 2D skeleton
Add TAG_SKELETON2D=6, SKEL2D_BYTES=819, decode_skeleton2D() to
iphone_usb_bridge.py; handler in IphoneUSBSource writing
persons_arkit_2d / persons_arkit_2d_t to state; new state fields
in State dataclass. TDD: test_arkit_body.py (round-trip + wrong
length → None). Import check verified.
2026-06-30 19:19:54 +02:00
L'électron rare 660d493ad6 feat(wire): skeleton2D frame payload
Add FrameTag.skeleton2D (raw 6) and Skeleton2DPayload for
2D-projected ARKit joints: 91×SIMD2<Float> + 91 valid bytes,
big-endian wire format mirroring SkeletonPayload style.

Note: raw value is 6 (not 5 as drafted) because face=5 is taken.
2026-06-30 19:15:16 +02:00
L'électron rare bd9f9fca29 docs: arkit-only body position plan 2026-06-30 18:47:10 +02:00