Drop empty demos by default in merge_demos.py#728
Conversation
There was a problem hiding this comment.
Code Review Summary
This PR improves the robustness of merge_demos.py by automatically filtering out empty demos and detecting intra-file schema inconsistencies. The changes are well-structured, and the test coverage is comprehensive.
What Works Well
- Clear separation between detection (in
_inspect_file) and filtering (in_merge) - Good defensive handling for edge cases (all-empty inputs, single-file sanitization)
- Excellent test coverage for the new functionality
- Documentation updates are thorough and clear
Summary
Overall a solid PR. The minor suggestions below are mostly about clarity and edge-case logging. None block merge.
|
|
||
| # "Empty" = unusable for replay / training. Either there is no actions dataset at | ||
| # all, or it has zero timesteps. record_demos.py shouldn't normally export these, | ||
| # but partial recordings and interrupted sessions can produce them. |
There was a problem hiding this comment.
Suggestion (minor): The intra-file schema check builds a fingerprint for every non-first demo, which is O(keys × demos). For files with many demos (>100) this could add noticeable overhead. Consider short-circuiting after the first mismatch is found since one is enough to raise an error:
if name != demo_names[0] and not intra_file_schema_mismatches:
demo_fp = _build_schema_fingerprint(demo)
if demo_fp != schema_fingerprint:
intra_file_schema_mismatches.append(name)Alternatively, if you want to report all mismatches, this is fine as-is, but perhaps add a comment noting the intentional choice.
| untracked_step_demos: list[str] = [] | ||
| empty_demo_names: list[str] = [] | ||
| intra_file_schema_mismatches: list[str] = [] | ||
| for name in demo_names: |
There was a problem hiding this comment.
Nit: When "actions" is missing entirely, actions_len becomes 0, which is correct. However, the empty-demo detection below distinguishes between "no actions dataset" and "zero-length actions." Consider logging which case was hit for debugging purposes (e.g., missing actions dataset vs actions.shape[0] == 0), since these can indicate different recorder failure modes.
| report.schema_status = "MISMATCH" | ||
| report.errors.append( | ||
| f"{i.path}: {len(actionable)} demo(s) ({shown}{suffix}) have a key/shape/dtype" | ||
| " set that differs from demo_0 in the same file (e.g. a missing camera" |
There was a problem hiding this comment.
Observation: The error message suggests "drop them upstream or re-record," but this PR now auto-drops empty demos. Consider clarifying that non-empty demos with schema mismatches require manual intervention, while empty mismatches are auto-handled.
| with h5py.File(path, "r+") as f: | ||
| demo = f["data"][demo_name] | ||
| del demo["actions"] | ||
| demo.create_dataset( |
There was a problem hiding this comment.
Test gap (minor): Consider adding a test case for a demo with actions present but containing only NaN or Inf values. While technically non-empty (shape > 0), such demos may also be unusable for training. This is outside the current PR scope but worth noting for future hardening.
Greptile SummaryThis PR adds automatic empty-demo filtering to
Confidence Score: 3/5Safe to merge for the common case, but one edge-case will incorrectly block a valid merge when the very first demo in a file has its The empty-filtering logic works correctly when empty demos carry a zero-length
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Parse args & validate paths] --> B[_inspect_file for each input]
B --> C{File readable & has demo_* groups?}
C -- No --> ERR1[Return exit code 2]
C -- Yes --> D[Build schema fingerprint from demo_0\nLoop over all demos:\n- count steps\n- detect empty demos\n- detect intra-file schema mismatches]
D --> E[_validate_compatibility]
E --> F{format_version / cross-file schema / intra-file mismatches?}
F -- Hard errors --> ERR2[Print summary + error, return 1]
F -- OK / warnings only --> G{All demos empty?}
G -- Yes --> ERR3[Print summary + ERROR every demo empty, return 1]
G -- No --> H{--dry_run?}
H -- Yes --> I[Print summary, return 0/1]
H -- No --> J[os.makedirs output dir]
J --> K[_merge to .tmp file\nSkip demos in empty_demo_names\nRenumber survivors contiguously\nRecompute data.attrs.total]
K --> L{Exception?}
L -- Yes --> ERR4[Cleanup .tmp, return 1]
L -- No --> M[os.replace .tmp to output]
M --> N[_print_summary with dropped list]
N --> O[Return 0]
Reviews (1): Last reviewed commit: "Adding logic to drop empty teleop record..." | Re-trigger Greptile |
| # Sample the file-level reference schema from the first demo. record_demos.py writes | ||
| # a uniform recorder stack within a single file, so demo_0 is a reasonable reference; | ||
| # the per-demo loop below catches the rare case where a single demo in the file | ||
| # diverges (e.g. partial recording, manual edit). | ||
| schema_fingerprint = _build_schema_fingerprint(data_group[demo_names[0]]) |
There was a problem hiding this comment.
False intra-file schema error when
demo_0 is the empty demo (missing actions key)
The reference fingerprint is computed from demo_names[0] (line 156) before the loop that classifies empty demos. If demo_0 has no actions key at all (not merely a zero-length actions array, but the key is entirely absent — e.g. from a very early interrupted recording), schema_fingerprint will not contain actions. Every subsequent valid demo does have actions, so it is flagged as a schema mismatch. Those valid demos land in intra_file_schema_mismatches but are not in empty_set, making them "actionable". The merge aborts with a false error naming the good demos as the problem while the actual culprit (demo_0) is never called out.
The existing test suite only exercises the zero-length-actions path via _truncate_demo_actions (which keeps the actions key with shape (0, action_dim) so shape[1:] matches), so this scenario has no coverage. A recording session where the very first demo never wrote a single step is a realistic edge case for interrupted teleop sessions.
Summary
Drop empty demos by default in merge_demos.py
Detailed description
actionsdataset oractions.shape[0] == 0) are now skipped automatically during the merge. They breakreplay_demos.pyand the LeRobot conversion downstream, so there is no reason to keep them in a training-ready dataset. Dropped demos are listed by source file in the summary, and survivors renumber contiguously.