feat(data-only-viz): action-head review TUI

Add interactive console TUI for manual label review of
auto-labeled action datasets. Displays ASCII skeleton,
kinetics, and proposed label with confidence. User can
accept proposed label, choose manual override (1/2/3),
skip, or quit. Reads auto-labeled JSONL and writes
validated rows to reviewed dataset.
This commit is contained in:
L'électron rare
2026-05-13 22:34:26 +02:00
parent 0ecb2c3d3b
commit b53c748704
+116
View File
@@ -0,0 +1,116 @@
"""Manual label review TUI.
Reads an auto-labeled jsonl dataset, presents each window with:
- ASCII skeleton (front view) of last frame
- speed/accel/sym kinetics
- proposed label + confidence
Keys:
1 = debout, 2 = assise, 3 = danse
ENTER = accept proposed label
S = skip (label = None, will not be saved)
Q = quit and write what we have so far
Usage:
uv run python -m data_only_viz.training.review \\
--in ~/.cache/av-live-action/dataset/auto.jsonl \\
--out ~/.cache/av-live-action/dataset/reviewed.jsonl
"""
from __future__ import annotations
import argparse
import sys
import termios
import tty
from pathlib import Path
import numpy as np
from data_only_viz.action_head import LABELS
from data_only_viz.training.autolabel import autolabel_window
from data_only_viz.training.dataset import (
DatasetRow,
load_dataset_jsonl,
write_dataset_jsonl,
)
def _ascii_skeleton(j3d: np.ndarray, width: int = 40, height: int = 16) -> str:
pts = j3d[:, [0, 1]] # x, y
mn = pts.min(axis=0)
mx = pts.max(axis=0)
rng = np.maximum(mx - mn, 1e-3)
norm = (pts - mn) / rng
grid = [[" "] * width for _ in range(height)]
for x, y in norm:
col = int(x * (width - 1))
row = int((1 - y) * (height - 1))
grid[row][col] = "*"
return "\n".join("".join(row) for row in grid)
def _getch() -> str:
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
return sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
def review(in_path: Path, out_path: Path,
sample_validated_fraction: float = 0.2,
seed: int = 0) -> None:
from data_only_viz.action_head import FeatureExtractor
rows = load_dataset_jsonl(in_path)
rng = np.random.default_rng(seed)
kept: list[DatasetRow] = []
for i, r in enumerate(rows):
proposed, conf = autolabel_window(list(r.j3d_stack))
is_none = proposed is None
sampled = rng.random() < sample_validated_fraction
if not is_none and not sampled and r.manually_validated:
kept.append(r)
continue
print("\033[2J\033[H") # clear
print(f"[{i + 1}/{len(rows)}] {r.window_id} proposed={proposed} conf={conf:.2f}")
print(_ascii_skeleton(r.j3d_stack[-1]))
kin = FeatureExtractor.kinetics(list(r.j3d_stack))
print(f"speed={kin[0]:.3f} accel={kin[1]:.3f} sym={kin[2]:+.3f}")
print("keys: 1=debout 2=assise 3=danse ENTER=accept S=skip Q=quit")
k = _getch().lower()
if k == "q":
break
if k == "s":
continue
if k == "\r":
chosen = proposed
elif k in ("1", "2", "3"):
chosen = LABELS[int(k) - 1]
else:
continue
if chosen is None:
continue
kept.append(DatasetRow(
window_id=r.window_id, label=chosen, j3d_stack=r.j3d_stack,
session=r.session, pid_local=r.pid_local,
auto_label_confidence=conf, manually_validated=True,
))
out_path.parent.mkdir(parents=True, exist_ok=True)
write_dataset_jsonl(kept, out_path)
print(f"\nwrote {len(kept)} rows to {out_path}")
def _cli() -> None:
p = argparse.ArgumentParser()
p.add_argument("--in", dest="in_path", required=True, type=Path)
p.add_argument("--out", dest="out_path", required=True, type=Path)
p.add_argument("--sample-fraction", type=float, default=0.2)
args = p.parse_args()
review(args.in_path, args.out_path,
sample_validated_fraction=args.sample_fraction)
if __name__ == "__main__":
_cli()