Hands: ft-hands works out which side camera is which

ft-camd tells the side cameras apart by XRService's buffer allocation order,
which some XRService starts reverse; both of 2026-10-02's starts did, so the
cutouts missed the hands. HANDS_SWAP_SIDES=auto (the default) has ft-hands
vote from hands seen in both side cameras: the landmark rays meet in front
of both cameras only under the right naming. While undecided it probes the
exchanged naming with the landmark model. It decides in about 2 s of hands
(right on all 7 recordings replayed), swaps the views in place, and publishes
sides.json. 0 and 1 still force it, with a warning when the hands disagree.

Recordings carry each part's naming and the session's decision; review,
export, validate and ft-handreplay put the names right, and takes.py sides
records a decision by hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DeeJanuzandClaude Opus 5.5 committed 2026-10-02 22:24:13 -06:00
1 parent 97b7fc837c
commit a533bb973a
19 files changed
+1403 -79

No files matched your search

+30
View File
@@ -15,6 +15,8 @@ checks:
every set's header is checked (camera names, sizes, record length) and counted against the
manifest; the pixels themselves aren't looked at;
- every .jsonl line parses, and take.json does;
- the side cameras (sides.py): the session's "sides" decision, and each take's files named
right by it (a warning when it wasn't decided: the maintainer's check tells then);
- the total size.
It runs on Linux and Windows, with Python 3.12 or later. It reports errors (don't upload or accept this) and warnings (look at it).
@@ -585,9 +587,37 @@ def check_manifest(m, report, root):
report.error(f"manifest: take {tid}'s file is {f!r}")
elif not isinstance(t.get("raw_bytes"), int):
report.error(f"manifest: take {tid} has a file but no raw_bytes")
check_sides(m, entries, report)
return entries
def check_sides(m, entries, report):
"""Which side camera is which (sides.py): session.sides.swapped is true, false or null, and
every take's files carry names_swapped equal to it (export renames them so)."""
session = m.get("session") if isinstance(m.get("session"), dict) else {}
s = session.get("sides")
if s is None:
report.warn("manifest: no session.sides (recorded before the side cameras were checked): "
"the maintainer's check tells which way round they are")
return
if not isinstance(s, dict) or s.get("swapped") not in (True, False, None):
report.error("manifest: session.sides isn't {\"swapped\": true|false|null, ...}")
return
swapped = s["swapped"]
if swapped is None:
report.warn("the side cameras' naming wasn't decided while recording (no live tracker): "
"the maintainer's check tells which way round they are")
for tid, t in entries.items():
ts = t.get("sides")
if ts is None:
continue
if not isinstance(ts, dict) or ts.get("names_swapped") not in (True, False, None):
report.error(f"manifest: take {tid}'s sides isn't {{\"names_swapped\": true|false|null, ...}}")
elif swapped is not None and ts["names_swapped"] != swapped:
report.error(f"take {tid}: its side cameras aren't named right (names_swapped "
f"{ts['names_swapped']}, the session's swapped {swapped}): export it again")
def summarize(m):
"""What a pull request's description says about the export: takes, minutes, lighting,
objects, controllers, the consent and tool versions."""