Files
L'électron rare beb94d2a4c feat(data-only-viz): action-head v3 hands+lips
Extends the action-head feature pipeline from v2 (302-D) to v3 (428-D).

- Replace placeholder SMPLX_FINGERTIP_VERTS with canonical vertex IDs
  from smplx.vertex_ids (lthumb/lindex/lmiddle/lring/lpinky, mirrored R)
- Add HANDS_KP_* constants (21 kp/hand, 42 total, 126-D flat block)
- FEATURE_DIM: 302 -> 428; hands_kp block inserted at [288:414]
- FeatureExtractor.from_buffer gains hands_kp param (42, 3),
  zero-padded when absent
- ActionHead.step gains hands_kp param, threads to from_buffer
- _read_sources returns 5-tuples with hands_kp42x3 per person
- MediaPipe FaceMesh inner-lip (idx 13/14) used for mouth_open;
  fallback to SMPL-X v3d lip vertices when face not available
- _build_hands_map and _build_face_mouth_map helpers added
- dataset.py: RawFrame/WindowRow/DatasetRow gain hands_kp fields
- train_action_head.py: reads hands_kp_stack per step, zeros if absent
- extract_j3d_offline.py: writes zero-filled hands_kp in jsonl output
- Tests: FEATURE_DIM 302->428, param bound 80k->100k, +4 new tests
2026-05-13 23:26:14 +02:00

142 lines
4.7 KiB
Python

"""Tests for dataset jsonl IO + sliding windows + split."""
from __future__ import annotations
import json
from pathlib import Path
import numpy as np
import pytest
def _make_session_jsonl(path: Path, n_frames: int = 64) -> None:
rng = np.random.default_rng(0)
with path.open("w") as f:
for t in range(n_frames):
row = {"ts": t / 30.0,
"session": "sess01",
"pid": 1,
"j3d": rng.normal(size=(32, 3)).tolist()}
f.write(json.dumps(row) + "\n")
def test_load_frames_jsonl(tmp_path: Path) -> None:
from data_only_viz.training.dataset import load_frames_jsonl
p = tmp_path / "raw.jsonl"
_make_session_jsonl(p)
frames = load_frames_jsonl(p)
assert len(frames) == 64
assert frames[0].j3d.shape == (32, 3)
assert frames[0].pid == 1
assert frames[0].session == "sess01"
def test_sliding_windows(tmp_path: Path) -> None:
from data_only_viz.training.dataset import (
load_frames_jsonl,
sliding_windows,
)
p = tmp_path / "raw.jsonl"
_make_session_jsonl(p, n_frames=64)
frames = load_frames_jsonl(p)
windows = list(sliding_windows(frames, window_len=16, stride=4))
assert len(windows) == 13
assert windows[0].j3d_stack.shape == (16, 32, 3)
assert windows[0].session == "sess01"
def test_write_and_load_dataset_jsonl(tmp_path: Path) -> None:
from data_only_viz.training.dataset import (
DatasetRow,
load_dataset_jsonl,
write_dataset_jsonl,
)
rng = np.random.default_rng(0)
rows = [
DatasetRow(
window_id=f"sess01_pid1_w{i:04d}",
label="debout" if i % 2 == 0 else "danse",
j3d_stack=rng.normal(size=(16, 32, 3)).astype(np.float32),
session="sess01",
pid_local=1,
auto_label_confidence=0.8,
manually_validated=False,
)
for i in range(5)
]
out = tmp_path / "ds.jsonl"
write_dataset_jsonl(rows, out)
loaded = load_dataset_jsonl(out)
assert len(loaded) == 5
assert loaded[0].label == "debout"
assert loaded[0].j3d_stack.shape == (16, 32, 3)
assert np.allclose(loaded[0].j3d_stack, rows[0].j3d_stack, atol=1e-6)
def test_write_and_load_dataset_jsonl_with_hands_kp(tmp_path: Path) -> None:
from data_only_viz.training.dataset import (
DatasetRow,
load_dataset_jsonl,
write_dataset_jsonl,
)
rng = np.random.default_rng(1)
hands_kp = rng.normal(size=(16, 42, 3)).astype(np.float32)
row = DatasetRow(
window_id="sess01_pid1_w0000",
label="danse",
j3d_stack=rng.normal(size=(16, 32, 3)).astype(np.float32),
session="sess01",
pid_local=1,
auto_label_confidence=0.9,
manually_validated=True,
hands_kp_stack=hands_kp,
)
out = tmp_path / "with_hands.jsonl"
write_dataset_jsonl([row], out)
loaded = load_dataset_jsonl(out)
assert loaded[0].hands_kp_stack is not None
assert loaded[0].hands_kp_stack.shape == (16, 42, 3)
assert np.allclose(loaded[0].hands_kp_stack, hands_kp, atol=1e-6)
def test_load_dataset_jsonl_without_hands_kp_is_ok(tmp_path: Path) -> None:
"""Legacy v2 rows without hands_kp field should load with hands_kp_stack=None."""
import json
from data_only_viz.training.dataset import load_dataset_jsonl
rng = np.random.default_rng(2)
row = {
"window_id": "sess01_pid1_w0000",
"label": "debout",
"j3d": rng.normal(size=(16, 32, 3)).tolist(),
"session": "sess01",
"pid_local": 1,
"auto_label_confidence": 0.8,
"manually_validated": False,
}
out = tmp_path / "legacy.jsonl"
out.write_text(json.dumps(row) + "\n")
loaded = load_dataset_jsonl(out)
assert len(loaded) == 1
assert loaded[0].hands_kp_stack is None
def test_split_by_session(tmp_path: Path) -> None:
from data_only_viz.training.dataset import DatasetRow, split_by_session
rng = np.random.default_rng(0)
rows = []
for sess in ("s01", "s02", "s03", "s04", "s05", "s06", "s07"):
rows.append(DatasetRow(
window_id=f"{sess}_w0", label="debout",
j3d_stack=rng.normal(size=(16, 32, 3)).astype(np.float32),
session=sess, pid_local=1, auto_label_confidence=0.7,
manually_validated=False,
))
train, val, test = split_by_session(rows, ratios=(0.7, 0.15, 0.15), seed=0)
all_sessions = {r.session for r in train + val + test}
assert all_sessions == {"s01","s02","s03","s04","s05","s06","s07"}
train_s = {r.session for r in train}
val_s = {r.session for r in val}
test_s = {r.session for r in test}
assert train_s.isdisjoint(val_s)
assert train_s.isdisjoint(test_s)
assert val_s.isdisjoint(test_s)