@@ -131,20 +131,26 @@ def test_load_dataset_spec_parses_preprocess(tmp_path: Path) -> None:
131131hash_salt: ""
132132preprocess:
133133 normalize_root_xy: true
134- ground_align: clip_min_foot
134+ ground_align: none
135135 min_frames: 10
136+ max_all_off_ground_s: 0.5
137+ off_ground_height: 0.08
136138sources:
137139 - name: clips
138140 type: npz
139141 input: { tmp_path / 'npz_source' }
142+ exclude_patterns: ["*obstacle*"]
140143""" ,
141144 encoding = "utf-8" ,
142145 )
143146
144147 spec = load_dataset_spec (spec_path )
145148 assert spec .preprocess .normalize_root_xy is True
146- assert spec .preprocess .ground_align == "clip_min_foot "
149+ assert spec .preprocess .ground_align == "none "
147150 assert spec .preprocess .min_frames == 10
151+ assert spec .preprocess .max_all_off_ground_s == 0.5
152+ assert spec .preprocess .off_ground_height == 0.08
153+ assert spec .sources [0 ].exclude_patterns == ("*obstacle*" ,)
148154
149155
150156def test_load_dataset_spec_parses_seed_filter_preset (tmp_path : Path ) -> None :
@@ -428,6 +434,71 @@ def test_collect_source_files_with_report_handles_single_file_source(tmp_path: P
428434 assert [item .rel_no_suffix .as_posix () for item in legacy_items ] == ["clip_a" ]
429435
430436
437+ def test_collect_source_files_with_report_applies_exclude_patterns (tmp_path : Path ) -> None :
438+ input_root = tmp_path / "lafan1"
439+ input_root .mkdir (parents = True , exist_ok = True )
440+ (input_root / "walk1.bvh" ).write_text ("placeholder" , encoding = "utf-8" )
441+ (input_root / "obstacle_run.bvh" ).write_text ("placeholder" , encoding = "utf-8" )
442+ nested = input_root / "subject1"
443+ nested .mkdir ()
444+ (nested / "obstacle_jump.bvh" ).write_text ("placeholder" , encoding = "utf-8" )
445+
446+ source = DatasetSourceSpec (
447+ name = "lafan1" ,
448+ type = "bvh" ,
449+ input = str (input_root ),
450+ bvh_format = "lafan1" ,
451+ exclude_patterns = ("*obstacle*" ,),
452+ )
453+
454+ items , _scan_root , report = dataset_builder ._collect_source_files_with_report (
455+ source ,
456+ quiet = True ,
457+ )
458+
459+ assert [item .rel_no_suffix .as_posix () for item in items ] == ["walk1" ]
460+ assert report ["scanned_files" ] == 3
461+ assert report ["path_rejected_files" ] == 2
462+ assert report ["kept_files" ] == 1
463+ assert report ["filtered_files" ] == 2
464+ assert report ["path_reject_reasons" ] == {"*obstacle*" : 2 }
465+
466+
467+ def test_collect_source_files_with_report_preserves_path_excludes_with_metadata (tmp_path : Path ) -> None :
468+ input_root = tmp_path / "seed_source" / "g1" / "csv"
469+ input_root .mkdir (parents = True , exist_ok = True )
470+ for name in ("walk_a.csv" , "walk_b.csv" , "obstacle_walk.csv" ):
471+ (input_root / name ).write_text ("placeholder" , encoding = "utf-8" )
472+
473+ metadata_csv = tmp_path / "metadata.csv"
474+ with metadata_csv .open ("w" , encoding = "utf-8" , newline = "" ) as handle :
475+ writer = csv .DictWriter (handle , fieldnames = ["move_g1_path" , "is_mirror" ])
476+ writer .writeheader ()
477+ for name in ("walk_a.csv" , "walk_b.csv" , "obstacle_walk.csv" ):
478+ writer .writerow ({"move_g1_path" : f"g1/csv/{ name } " , "is_mirror" : "False" })
479+
480+ source = DatasetSourceSpec (
481+ name = "seed" ,
482+ type = "seed_csv" ,
483+ input = str (input_root ),
484+ metadata_csv = str (metadata_csv ),
485+ filters = {"is_mirror" : [False ]},
486+ exclude_patterns = ("*obstacle*" ,),
487+ )
488+
489+ items , _scan_root , report = dataset_builder ._collect_source_files_with_report (
490+ source ,
491+ quiet = True ,
492+ )
493+
494+ assert [item .rel_no_suffix .as_posix () for item in items ] == ["walk_a" , "walk_b" ]
495+ assert report ["scanned_files" ] == 3
496+ assert report ["path_rejected_files" ] == 1
497+ assert report ["kept_files" ] == 2
498+ assert report ["filtered_files" ] == 1
499+ assert report ["path_reject_reasons" ] == {"*obstacle*" : 1 }
500+
501+
431502def test_build_dataset_from_spec_writes_shard_directories (tmp_path : Path ) -> None :
432503 npz_input = tmp_path / "npz_source"
433504 _write_npz_from_pkl (npz_input / "clip_a.npz" )
@@ -460,6 +531,35 @@ def test_build_dataset_from_spec_writes_shard_directories(tmp_path: Path) -> Non
460531 assert "clip_lengths" in train_data .files
461532
462533
534+ def test_collect_clip_rows_ignores_stale_excluded_cached_npz (tmp_path : Path ) -> None :
535+ npz_input = tmp_path / "npz_source"
536+ for name in ("keep_a.npz" , "keep_b.npz" , "obstacle_old.npz" ):
537+ _write_npz_from_pkl (npz_input / name )
538+
539+ spec = DatasetSpec (
540+ name = "demo_dataset" ,
541+ target_fps = 30 ,
542+ val_percent = 5 ,
543+ hash_salt = "" ,
544+ sources = [
545+ DatasetSourceSpec (
546+ name = "npz_src" ,
547+ type = "npz" ,
548+ input = str (npz_input ),
549+ exclude_patterns = ("*obstacle*" ,),
550+ )
551+ ],
552+ )
553+ paths = dataset_builder .resolve_dataset_paths (spec , output_root = tmp_path / "datasets" )
554+ source_dir = paths .clips_root / "npz_src"
555+ for name in ("keep_a.npz" , "keep_b.npz" , "obstacle_old.npz" ):
556+ _write_npz_from_pkl (source_dir / name )
557+
558+ rows = dataset_builder .collect_clip_rows (spec , paths = paths )
559+
560+ assert sorted (row .clip_id for row in rows ) == ["npz_src:keep_a" , "npz_src:keep_b" ]
561+
562+
463563def test_convert_source_to_npz_clips_applies_preprocess (tmp_path : Path ) -> None :
464564 npz_input = tmp_path / "npz_source"
465565 _write_npz_from_pkl (npz_input / "clip_a.npz" )
@@ -503,6 +603,66 @@ def test_convert_source_to_npz_clips_applies_preprocess(tmp_path: Path) -> None:
503603 assert np .isclose (float (np .min (foot_z )), 0.0 )
504604
505605
606+ def test_convert_source_to_npz_clips_skips_all_off_ground_clips_before_ground_align (tmp_path : Path ) -> None :
607+ npz_input = tmp_path / "npz_source"
608+ _write_npz_from_pkl (npz_input / "keep.npz" )
609+ _write_npz_from_pkl (npz_input / "float.npz" )
610+
611+ for name , floating in (("keep.npz" , False ), ("float.npz" , True )):
612+ path = npz_input / name
613+ clip = dict (np .load (path , allow_pickle = True ))
614+ body_names = [str (body_name ) for body_name in clip ["body_names" ].tolist ()]
615+ left_idx = body_names .index ("left_ankle_roll_link" )
616+ right_idx = body_names .index ("right_ankle_roll_link" )
617+ body_pos_w = np .asarray (clip ["body_pos_w" ]).copy ()
618+ if floating :
619+ body_pos_w [..., 2 ] = np .maximum (body_pos_w [..., 2 ], 0.3 )
620+ else :
621+ body_pos_w [:, left_idx , 2 ] = 0.0
622+ body_pos_w [:, right_idx , 2 ] = 0.3
623+ clip ["body_pos_w" ] = body_pos_w
624+ np .savez (path , ** clip )
625+
626+ source = DatasetSourceSpec (name = "npz_src" , type = "npz" , input = str (npz_input ))
627+ output_dir = tmp_path / "dataset" / "clips" / "npz_src"
628+ report = convert_source_to_npz_clips (
629+ source ,
630+ output_dir ,
631+ jobs = 1 ,
632+ preprocess = dataset_builder .DatasetPreprocessSpec (
633+ ground_align = "clip_min_foot" ,
634+ max_all_off_ground_s = 0.05 ,
635+ off_ground_height = 0.08 ,
636+ ),
637+ )
638+
639+ assert report ["clips" ] == 1
640+ assert (output_dir / "keep.npz" ).is_file ()
641+ assert not (output_dir / "float.npz" ).exists ()
642+ assert (output_dir / "float.npz.filtered.json" ).is_file ()
643+
644+ def _unexpected_run_conversion_tasks (* _args , ** _kwargs ):
645+ raise AssertionError ("filtered clip should be skipped by marker on incremental rebuild" )
646+
647+ monkeypatch = pytest .MonkeyPatch ()
648+ monkeypatch .setattr (dataset_builder , "run_conversion_tasks" , _unexpected_run_conversion_tasks )
649+ try :
650+ second_report = convert_source_to_npz_clips (
651+ source ,
652+ output_dir ,
653+ jobs = 1 ,
654+ preprocess = dataset_builder .DatasetPreprocessSpec (
655+ ground_align = "clip_min_foot" ,
656+ max_all_off_ground_s = 0.05 ,
657+ off_ground_height = 0.08 ,
658+ ),
659+ )
660+ finally :
661+ monkeypatch .undo ()
662+
663+ assert second_report ["clips" ] == 1
664+
665+
506666def test_merge_clip_dicts_rejects_inconsistent_body_names (tmp_path : Path ) -> None :
507667 clip_a = tmp_path / "clip_a.npz"
508668 clip_b = tmp_path / "clip_b.npz"
0 commit comments