mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 01:00:06 +02:00
hand recorder export: no controller poses without controllers, nothing in deleted ranges
When the checklist says no controllers, exported poses.jsonl has left and right null and feedback lines carry no controller state: controllers left switched on still get tracked (one wandered 2 m in a real session) and would read as the hands' ground truth. Poses and live-tracker feedback inside deleted ranges are left out too, as the images there are. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8321a99121
commit
1eb120e6b1
3 files changed
+76
-2
No files matched your search
@@ -311,6 +311,7 @@ On 2026-10-02 a whole session showed "I can't see your hands": after the headset
|
||||
- `calibration.json` and `device.json`, when the session has them.
|
||||
- Per take: `prompts.jsonl`, `poses.jsonl`, `take.json`, and `sets.bin.zst` (sets in deleted ranges removed, then zstd -10 with 2 threads).
|
||||
- The side cameras are named right in every exported set when the session's `sides.swapped` is known: parts that need it get slam_left and slam_right (and their `_dk`) exchanged in the set headers as they're compressed. The exported take.json says `"parts": {"sets.bin": {"names_swapped": <swapped>}}`, and the manifest's take entry `"sides": {"names_swapped", "renamed_sets"}`. Unknown, the names stay as recorded.
|
||||
- `poses.jsonl` and `prompts.jsonl` without what's in the deleted ranges: no poses and no live-tracker `feedback` there (the prompt timeline stays). With the checklist's controllers at `none`, the controllers' poses are null and `feedback` has no `controller` (`takes.export_jsonl`): controllers left switched on still get tracked, and their poses would pass for the hands' ground truth.
|
||||
- `SHA256SUMS`.
|
||||
|
||||
Compression runs at nice 19. While it runs with the headset worn, the window notes that VR may stutter a little (exports are done in the headset). Worn is judged the way `frame-job` does: `vrcompositor` runs and a `/sys/class/backlight/*/brightness` reads over 0 (SteamVR turns the panel off 5 s after the headset comes off). CPU work while in VR causes stutter. The proximity sensor is no use here: it read 9-43 with the headset sitting unworn.
|
||||
|
||||
+37
-2
@@ -126,6 +126,40 @@ def in_ranges(t, ranges):
|
||||
return any(a <= t <= b for a, b in ranges)
|
||||
|
||||
|
||||
def export_jsonl(src, dst, ranges, keep_controllers):
|
||||
"""Copy a take's poses.jsonl or prompts.jsonl for export:
|
||||
- poses in the deleted ranges are left out, and the live tracker's feedback there (the
|
||||
prompt timeline stays: it says what was asked, and shows nothing);
|
||||
- without controllers (the checklist's "none"), the controllers' poses are null and feedback
|
||||
carries no controller state: controllers lying about still get logged, and those poses
|
||||
would read as hands' ground truth.
|
||||
Lines that don't parse are copied as they are (validate.py reports them)."""
|
||||
poses = os.path.basename(src) == "poses.jsonl"
|
||||
with open(src, encoding="utf-8") as f, open(dst, "w", encoding="utf-8", newline="\n") as out:
|
||||
for line in f:
|
||||
try:
|
||||
obj = json.loads(line)
|
||||
except ValueError:
|
||||
out.write(line)
|
||||
continue
|
||||
if not isinstance(obj, dict):
|
||||
out.write(line)
|
||||
continue
|
||||
t = obj.get("t")
|
||||
gone = isinstance(t, int) and in_ranges(t, ranges)
|
||||
if poses:
|
||||
if gone:
|
||||
continue
|
||||
if not keep_controllers:
|
||||
obj["left"] = obj["right"] = None
|
||||
elif obj.get("event") == "feedback":
|
||||
if gone:
|
||||
continue
|
||||
if not keep_controllers:
|
||||
obj.pop("controller", None)
|
||||
out.write(json.dumps(obj, separators=(", ", ": ")) + "\n")
|
||||
|
||||
|
||||
def merge_ranges(ranges):
|
||||
"""Sorted, with overlapping or touching ranges joined."""
|
||||
out = []
|
||||
@@ -472,17 +506,18 @@ class Store:
|
||||
total = sum(t["bytes"] for t in takes) or 1
|
||||
done = 0
|
||||
take_entries = []
|
||||
keep_controllers = (meta.get("checklist") or {}).get("controllers") == "straps"
|
||||
for t in takes:
|
||||
if cancelled():
|
||||
raise Cancelled()
|
||||
tdir = self.take_dir(session, t["id"])
|
||||
out = os.path.join(work, "takes", t["id"])
|
||||
os.makedirs(out)
|
||||
ranges = t["ranges"]
|
||||
for name in ("prompts.jsonl", "poses.jsonl"):
|
||||
if os.path.isfile(os.path.join(tdir, name)):
|
||||
shutil.copyfile(os.path.join(tdir, name), os.path.join(out, name))
|
||||
export_jsonl(os.path.join(tdir, name), os.path.join(out, name), ranges, keep_controllers)
|
||||
index = take_index(tdir)
|
||||
ranges = t["ranges"]
|
||||
keep = [i for i in range(len(index)) if not in_ranges(index.time_ns(i), ranges)]
|
||||
# one file, named right where the decision is known (sides.py): its names_swapped is
|
||||
# then the session's swapped; unknown, it's the parts' own (None if they differ)
|
||||
|
||||
@@ -281,6 +281,44 @@ class ValidateTest(unittest.TestCase):
|
||||
self.assertNotEqual(os.system(f"{sys.executable} {validate.__file__} {d} >/dev/null"), 0)
|
||||
|
||||
|
||||
class ExportJsonlTest(unittest.TestCase):
|
||||
"""takes.export_jsonl: deleted ranges and the controllers when the checklist says none."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.mkdtemp(prefix="handrec-jsonl-test-")
|
||||
ctrl = {"m": [0.0] * 12, "r": 200, "ok": True}
|
||||
with open(os.path.join(self.tmp, "poses.jsonl"), "w") as f:
|
||||
for t in range(10):
|
||||
f.write(json.dumps({"t": t, "hmd": ctrl, "left": ctrl, "right": ctrl}) + "\n")
|
||||
with open(os.path.join(self.tmp, "prompts.jsonl"), "w") as f:
|
||||
for e in ({"t": 1, "event": "prompt", "id": "a"}, {"t": 4, "event": "feedback", "left": True},
|
||||
{"t": 7, "event": "feedback", "left": True, "controller": {"left": "ok"}}):
|
||||
f.write(json.dumps(e) + "\n")
|
||||
os.makedirs(os.path.join(self.tmp, "out"))
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.tmp, ignore_errors=True)
|
||||
|
||||
def run_export(self, name, keep):
|
||||
out = os.path.join(self.tmp, "out", name)
|
||||
takes.export_jsonl(os.path.join(self.tmp, name), out, [[3, 5]], keep)
|
||||
with open(out) as f:
|
||||
return [json.loads(line) for line in f]
|
||||
|
||||
def test_no_controllers(self):
|
||||
poses = self.run_export("poses.jsonl", False)
|
||||
self.assertEqual([p["t"] for p in poses], [0, 1, 2, 6, 7, 8, 9]) # 3-5 deleted
|
||||
self.assertTrue(all(p["left"] is None and p["right"] is None and p["hmd"] for p in poses))
|
||||
prompts = self.run_export("prompts.jsonl", False)
|
||||
self.assertEqual([e["t"] for e in prompts], [1, 7]) # the feedback at 4 was in a deleted range
|
||||
self.assertNotIn("controller", prompts[1])
|
||||
|
||||
def test_with_controllers(self):
|
||||
poses = self.run_export("poses.jsonl", True)
|
||||
self.assertTrue(all(p["left"] and p["right"] for p in poses))
|
||||
self.assertIn("controller", self.run_export("prompts.jsonl", True)[1])
|
||||
|
||||
|
||||
class SessionFilesTest(unittest.TestCase):
|
||||
"""session.py's device.json, and session ids with a suffix."""
|
||||
|
||||
|
||||
Reference in new issue
Block a user