Skip to content

[video_ingestion] OSRB compliance: dual-license LICENSE, NOTICE, SPDX…#91

Closed
nv-liuw wants to merge 238 commits into
mainfrom
liuw/osrb_compliance
Closed

[video_ingestion] OSRB compliance: dual-license LICENSE, NOTICE, SPDX…#91
nv-liuw wants to merge 238 commits into
mainfrom
liuw/osrb_compliance

Conversation

@nv-liuw

@nv-liuw nv-liuw commented May 27, 2026

Copy link
Copy Markdown
Collaborator

NVIDIA OSRB approval for the dual CC-BY-4.0 + Apache-2.0 release flagged four BEFORE-WE-CAN-CLOSE items. This commit addresses all of them on the video_ingestion_agent surface.

  1. License file (video_ingestion_agent/LICENSE) Replace the previous 15-line Apache-2.0 short notice with the full CC-BY-4.0-Apache2 Dual License text (~700 lines): NVIDIA preamble + verbatim Creative Commons Attribution 4.0 International + verbatim Apache License 2.0.

  2. Third-party notices (video_ingestion_agent/NOTICE) New file. Enumerates the 41 direct dependencies declared in pyproject.toml (top-level + every optional-dependencies extra). Each entry follows the NVIDIA modstar template: owner + SPDX license + license URL + package name/version + project URL. Versions sourced from uv.lock; licenses hand-curated against each project's upstream LICENSE file.

  3. SPDX headers normalized across the package (~125 files) Every code/config file under video_ingestion_agent/ now carries the 2-line dual header: # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: CC-BY-4.0 AND Apache-2.0 Previously the headers used the 3-line Apache-2.0-only template (107 files) or a few variant formats (Dockerfile, docs/conf.py, scripts/ run_osmo.py docstring variant — 9 files), and 9 files (osmo_workflows yamls and reconstruction_interface init.py files) had no header at all. Year normalized to 2026 throughout.

  4. Contribution policy (CONTRIBUTING.md, .github/pull_request_template.md, README.md) Add a Sign-off section to CONTRIBUTING.md referencing the Developer Certificate of Origin (https://developercertificate.org/) and requiring git commit -s on every commit. Add a top-level pull_request_template.md with a Checklist including the DCO reminder. Expand the README License section to mention the dual license + the NOTICE file, and add a brief Contributing section pointing at CONTRIBUTING.md.

xinghaozhu-nvidia and others added 30 commits November 4, 2025 15:47
…t and formats it with the ManoSharpaData schema and saves to file. Then second script loads the data file and runs retargeting filling the robot state related fields. These changes also add code that loads the TACO dataset. The new data loading flow works for both Arctic and Taco.
fix num_envs = 1

fix left hand init and reset

add box no collision
nv-liuw and others added 25 commits May 6, 2026 10:48
- Improve the landing page of the github page
- Update ci/cd
# v2d hand alignment + ego e2e pipeline

## Summary

Adds an end-to-end egocentric pipeline for reconstructing a hand and
held object from a single RGB video, along with the `v2d_hand_alignment`
module that depth-aligns DynHaMR hand poses to monocular depth and
computes a coherent world-frame scene.

---

## New modules

### `v2d_hand_alignment`
Full Docker module for aligning egocentric hand reconstructions against
monocular depth estimates.

- **`hand_object_alignment.py`** — `HandObjectAlignment` class: given
DynHaMR world results, per-frame depth images, and per-frame object
masks, computes the camera-space translation offset that minimises the
hand–depth discrepancy per frame. `compute_offset_reprojected` corrects
for intrinsics mismatch between DynHaMR and the depth source (MoGe/ViPE)
by reprojecting the shifted centroid into depth pixel space before
applying the offset.
- **`align_world_results.py`** — reads `world_results.npz`, runs
per-frame `compute_offset_reprojected`, stores `trans_aligned`
(depth-corrected translation) and `intrins_aligned` ([fx,fy,cx,cy] of
the depth used for alignment) alongside all original fields. Optionally
ingests FoundationPose object poses and stores them in both camera and
world frame.
- **`render_dynhamr_video.py`** — renders a 2×2 grid overlay video (RGB
+ hand mesh + object mesh + combined). Intrinsics are picked up
automatically from `intrins_aligned` in the `.npz` when present; an
explicit `--intrinsics_path` override is also supported.
- **`render_multiview_video.py`**, **`render_mano_params_video.py`**,
**`reproject_hand_mesh.py`**, **`smooth_hand_mesh.py`**,
**`recover_mano_params.py`** — supporting render and post-processing
steps.

### `v2d_depth_anything`
New Docker module wrapping Depth Anything V2 for monocular depth
estimation (image and video). Follows the same host/container split as
other `v2d_*` modules.

### `v2d_viz`
Lightweight 3D viewer package: a Python HTTP server (`run_3d_viewer.py`)
serving a Three.js web UI (`static/index.html`) for inspecting point
clouds and meshes interactively.

---

## New pipelines (`v2d_pipelines`)

### `run_v2d_ego_e2e.py`
Single-script end-to-end pipeline for ego hand + object reconstruction:

| Step | What runs |
|------|-----------|
| 1 | Ego hand reconstruction (ViPE + DynHaMR) |
| 2 | DynHaMR EXR depth → uint16 PNG + intrinsics |
| 3 | Frame extraction |
| 4 | MoGe monocular depth + intrinsics |
| 5 | Grounding DINO object detection (reference frame) |
| 6 | SAM2 object mask tracking |
| 7 | SAM3D textured mesh generation |
| 8 | FoundationPose scale estimation |
| 9 | FoundationPose 6-DOF tracking (with optional IoU-based
re-registration) |
| 10 | EKF pose smoothing |
| 11 | Hand/object depth alignment |
| 12–13 | Aligned + unaligned overlay video renders |

`--depth_source moge|vipe` selects which depth map feeds FoundationPose,
EKF, and hand alignment; both sources are always computed so results can
be compared side by side.

### `run_v2d_ego_video.py`
Lighter pipeline variant: runs only the hand reconstruction and depth
estimation steps (steps 1–4), without object tracking.

---

## Module enhancements

### `v2d_moge`
- `video_to_depth` now optionally exports a **pointmap** folder
(`--points_folder`): per-frame `(H, W, 3)` float32 `.npy` files with
metric 3D points, useful for SAM3D and depth alignment.
- Intrinsics stabilisation: `stabilize_intrinsics` smooths per-frame
focal lengths into a single stable estimate across the video.

### `v2d_sam3d`
- Accepts a pointmap input as an alternative depth source for mesh
generation.
- `run_image_to_mesh` exposes the pointmap path via both the lib and
docker wrappers.

### `v2d_foundation_pose`
- `run_video_to_poses` gains `--reregister_iou_thresh`: when set,
re-registers the object from scratch whenever the projected mask IoU
drops below the threshold (uses the new `track_one_with_recovery` path
in `FoundationPoseTracker`).

### `v2d_mesh`
- `Mesh.load` / `Mesh.save` extended; `mesh_transform.py` and
`mesh_render_image.py` updated to support the render pipeline.

### `v2d_depth`
- `align_depth_sequence.py` and `stabilize_intrinsics.py` added as
shared utilities.

---

## Documentation

- **`docs/ego_e2e_setup.md`** — minimal setup guide: prerequisites,
package install, Docker build, weight download, and how to run the
pipeline.

---

## Output schema (world_results_aligned.npz)

All original DynHaMR fields, plus:

| Field | Shape | Description |
|-------|-------|-------------|
| `trans_aligned` | `(B, T, 3)` | Depth-aligned translation (world
units) |
| `intrins_aligned` | `(4,)` | `[fx, fy, cx, cy]` of the depth source
used for alignment |
| `object_pose_cam` | `(T, 4, 4)` | Object-to-camera SE(3) (if
`--object_poses_dir`) |
| `object_pose_world` | `(T, 4, 4)` | Object-to-world SE(3) (if
`--object_poses_dir`) |

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- **Adaptive auto-curriculum** (`curriculum.py`): CV-gated first VOC
decay, WSR-retention gate for subsequent decays, custom VOC schedule
(1.0→0.75→…→0.0), force-timeout fallback; new `Sharpa-V2P-AutoCurr-v0`
env config and pool/priority support in OSMO configs
- **Eval pipeline overhaul** (`eval_callback.py`): two-pass eval per
checkpoint — Pass A (from-start, tc=0) measures full-trajectory
completion; Pass B (random reset) measures trajectory coverage; logs
`eval/completion_ratio_mean`, `eval/completion_ratio_std`,
`eval/full_completion_pct`, `eval/completion_ratio_mean_random`
- **Train/eval distribution gap fix**: `reset_to_first_frame_prob`
causes a fraction of training resets to go to tc=0, closing the gap
between training distribution and from-start eval
- **VOC-gated eval**: eval runs before each VOC decay and on a 1000-iter
fallback, providing signal timed to curriculum transitions
- **Bug fixes**: `custom_schedule` VOC not decaying per-env, eval video
now records from frame 0, post-warmup completion ratio denominator,
spurious training reward peaks after eval, inference mode not set at
step 0, VOC not restored correctly on resume

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a SOMA-X (NVlabs SOMA-MHR) -> G1 whole-body retargeter that mirrors
the NVHuman pipeline's motion_v1 output schema. The new path is
config-driven from day one: every robot-specific transform, IK weight,
posture-task gain, and foot-framing parameter is loaded from per-robot
JSON in `configs/<robot>/{frame_alignment,retargeter}.json` rather than
embedded in `params.py`, so adding a second humanoid is a JSON drop-in.

New modules
- `retarget/read_soma.py`: SOMA-X loader. Reads `soma_params.npz`,
applies SOMA's joint-orient + level-order FK, anchors the body to
frame-0 pelvis at the origin (`normalize=True`), and exposes per-frame
joints, world-rotation quats, and posed mesh vertices.
- `retarget/robot_config.py`: `RobotRetargetConfig` dataclass +
`load_robot_config(robot_name)`. Reads `frame_alignment.json` (world
axis swap + per-bone q_offset / t_offset) and `retargeter.json` (URDF,
IK map, foot framing, posture-task), composes pre-multiplied matrices,
validates the bundle.
- `retarget/whole_body_kinematics.py`: add
`ConfigDrivenWholeBodyKinematics` plus a posture-task hook
(`_extra_tasks`) so q0 + previous-q tracking can run alongside the frame
tasks.
- `retarget/ground_alignment.py`: generalize `ReferencePlane` to a
`(normal, offset)` pair; add `vertical_offset_to_plane` and a
reconstructed-plane loader so retargeting can honour the
reconstruction's `ground_plane.json` when present (falls back to
`ReferencePlane.horizontal(z=0.0)`).

New scripts
- `scripts/retarget/soma_to_g1.py` (1226 lines): SOMA -> G1 entrypoint.
Loads SOMA, applies CV->SOMA + first-frame anchor to the object
trajectory, runs the IK loop with optional posture-task regularization,
anchors per-frame ground plane, and writes a motion_v1 parquet under
`whole_body/soma/...`. Supports `--start-frame/--end-frame` slicing and
IK diagnostics.
- `scripts/retarget/soma_loader_probe.py`: read-only verification probe
for the SOMA loader; viser playback of body + object in source frame.
- `scripts/retarget/process_soma_sequence.sh`: interactive driver that
walks one sequence through the loader probe, retarget, support
reconstruction, and Isaac Lab replay stages. `--auto` runs the
non-visual stages hands-off.
- `scripts/setup_soma_assets.py`: one-shot bootstrap that fetches the
SOMA-MHR body model assets into `assets/body_models/soma/`.

Configs + docs
- `configs/g1/frame_alignment.json`: source-to-robot frame change for G1
(world axis swap + per-bone q_offsets / t_offsets). Per-bone q_offsets
seeded from NVIDIA/soma-retargeter and refined empirically.
- `configs/g1/retargeter.json`: G1 IK map, foot framing, posture-task
weights tuned for the snack-box / book sequences.
- `configs/README.md`: schema for both files, conventions (frames,
units, rotation composition), "add a new robot" walkthrough, three
legitimate sources for q_offsets (upstream copy / rest-pose derivation /
hand-tune in viewer), and a verification recipe.
- `retarget/scene_utils/scene_viewer_env_cfg.py`: seed
`env_cfg.scene.robot.init_state` from the saved frame-0 robot pose so
the viewer spawns the URDF where the retargeted motion expects it.
- `pyproject.toml`, `workflow/Dockerfile`: add the SOMA dep group +
body-model fetcher.

Tests
- `tests/test_reference_plane.py` (10 cases): horizontal equivalence,
inclined signed distance, vertical-offset conversion, normalization,
normal-z reorientation, near-vertical / zero-norm rejection, batch-shape
support, trailing-dim guard. All pass.

Bundled data
- `assets/human_motion_data/whole_body/soma/...`: one retargeted fixture
sequence + reconstructed support-surface USDA, for end-to-end regression
from this branch.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Touches modules that I don't own:
- (1506ab3) FrameSource/FrameWriter abstraction to support
reading/writing frames as images, video, or HDF5. In most places, this
is simply replacing code lines doing image read/write. For SAM2, I
modified the LazyFrameLoader.
- (3477889) Reorder contents of several Dockerfiles to use build cache
better (v2d_sam2, v2d_foundation_pose, and some mv-specific modules)
- (12b1498) Small fix in v2d_mesh to load GLB meshes with orientation

Other changes:
- Added OSMO workflow configuration and management system for dataset
sequences
- Add MHR->SOMA conversion in v2d_sam3d_body module, simplify output
format
- Add ground plane estimation
- Add symmetric object handling in MV FP tracking
- Add data exporting
- Improve detectron2 with ByteTracker
… to hand alignment (#62)

# Add v2d_anycalib + fisheye-aware hand alignment

## Summary

- New `v2d_anycalib` module: single-view camera calibration (intrinsics
+ distortion) and undistortion via AnyCalib, for image / image folder /
video inputs. Lets the e2e pipeline run on undistorted frames so
ViPE/MoGe/FoundationPose/DynHaMR don't fight the fisheye.
- Fixes for `v2d_hand_alignment` aligned/unaligned renders on fisheye
footage: intrinsics misuse, left-hand mirror, and missing per-vertex
size correction under depth-source intrinsics.

## Changes

**`v2d_anycalib`** — `lib/` calibration + undistortion, `docker/`
host-side wrappers (image, image folder, video, shell, weights
download), Dockerfile, pyproject, `v2d_common.datatypes` fields for
distortion + undistorted intrinsics.

**`v2d_hand_alignment`**
- `run_v2d_ego_e2e.render_unaligned` now passes `intrinsics_vipe` (raw
`trans/cam_R/cam_t` were optimized against ViPE's pinhole; depth-source
intrinsics break it).
- `align_world_results` mirrors `delta_world.x` for left hands — the
renderer flips x *after* adding `trans`, so the world-space delta must
match.
- `HandObjectAlignment.compute_alignment(side, t)` does one render pass
and returns `{offset, offset_reprojected, scale, n_pixels}`. Old methods
are thin shims.
- `align_world_results` aggregates per-frame scales into a constant
`hand_scale` (`n_pixels`-weighted median, rejecting <256-pixel or
out-of-`[0.2, 5.0]` frames) and saves it on
`world_results_aligned_*.npz`. MANO has no native scale knob, so a
post-MANO scalar is the right schema.
- `render_dynhamr_video` applies `hand_scale` around the per-frame
centroid only when `use_trans_aligned=True`; falls back to 1.0 when
absent (backward-compat).

**Misc** — `v2d_nlf` Docker build fix.

## Why

`hand_scale = (fx_vipe / fx_moge) × (z_moge / z_vipe)` resolves both the
ViPE-vs-MoGe focal mismatch (wrong silhouette size) and the
MoGe-vs-DynHaMR depth disagreement (centroid at wrong depth).
Approximately constant across time, so a single global value works.

## Test plan

- [ ] `run_v2d_ego_e2e` on raw fisheye and AnyCalib-undistorted inputs.
- [ ] `render_unaligned_moge.mp4` matches DynHaMR `*_src_cam.mp4` modulo
OneEuro smoothing (known).
- [ ] `render_aligned_moge.mp4` hand silhouette overlays image hand at
correct size.
- [ ] `world_results_aligned_*.npz` contains `hand_scale`; old npzs
without it still render.
- [ ] `mano.ipynb`: scaled hand sits on depth point cloud, not just
along its ray.
- [ ] AnyCalib end-to-end: per-frame `dz` smaller on undistorted stream
than on raw fisheye.
…uction (#60)

- Add reconstruction interface to integrate video agent with
reconstruction module
- Integrate the e2e hand reconstuction pipeline with webapp
- Add tests


Test plan:
- [x] Unit tests
- [x] Integrated tests with webapp:
https://drive.google.com/file/d/1h_8kcBLoSJEFKnjE_4ckBpCMdAvXkK4e/view?usp=drive_link

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
- Adds MANO→Dex3 retargeting for ARCTIC + TACO mirroring the sharpa
scripts on main: new `arctic_to_dex3.py` and `taco_to_dex3.py` use the
same loader output (`<dataset>_loaded/`), CLI surface, and per-frame IK
loop.
- Implements MANO source in `Dex3HandKinematics` (replaces the existing
`NotImplementedError` stub) and adds `setup_dex3_kinematics`,
`DEX3_TO_MANO_MAPPING` (palm + thumb/index/middle tips), `ManoDex3Data`
schema (BASE + MANO + DEX3 + OBJECT, 7 finger joints, 23 frames, 4
tasks).
- `dataset_registry.py`: refactors `retarget_script: str` →
`retarget_scripts: dict[str, str]` keyed by robot name. arctic and taco
gain `"dex3"` entries; sharpa entries unchanged.
- `run_retarget.py`: new `--robot {sharpa_wave,dex3}` flag dispatches to
the right per-robot retargeter.
- `vis_retargeted.py`: same `--robot` flag generalises the viewer.
Bundles the changes needed to launch whole-body ReconBody training on
the new soma TPV sequences (snack_box pick-and-place, dark_blue_book
pick-handover-place, blue_trash_can drag) and bring across the VOC and
metric infrastructure they depend on.

Feature work:
- MotionData.trim(start, end) at the schema layer; consumed by the
whole-body TrackingCommand loader and by load_replay_trajectory so
train, eval, and replay all subset motions consistently.
- TrackingCommandCfg gains motion_start_frame / motion_end_frame (int
sentinel for end so Hydra int overrides aren't rejected).
- scripts/replay_motion.py exposes --start_frame / --end_frame and
threads them through Trajectory and ContactOverlay.
- WholeBodyFixedTimestepVOCCurriculum + G1SonicReconBodyVOCCurriculumCfg
port the VOC scale schedule onto v2p_whole_body.
- TrackingCommand allocates and populates per-env tracking metrics
including virtual_object_controller_scale_factor.
- DualHandsObjectTrackingCommand logs the per-env VOC scale.
- motion_joint_pos_error_exp converted to ManagerTermBase so configs can
pass joint_names; G1SonicReconBodyRewardsCfg now scopes the joint-pos
reward to G1_SONIC_JOINT_NAMES.
- Strengthens resampling_time_range docstring to spell out how the (1e6,
1e6) sentinel disables IsaacLab's mid-episode resample, since the trim
feature relies on that path staying off.

Experiments + assets:
- New experiment configs recon_body_snack_box_pick_and_place_01 and
recon_body_dark_blue_book_pick_handover_place_02, registered in
experiments/registry.yaml.
- snack_box config drives motion_start_frame=250 / motion_end_frame=500
and reset_shoulder_spread=0.2 as a working example of the trim +
shoulder-spread reset combo.
- Adds soma sequence assets for snack_box_pick_and_place_01 and
blue_trash_can_drag_007 (mesh + URDF + parquet) plus the
reconstructed_stage USDA referenced by the snack_box scene.

Tests:
- v2p_whole_body command tests cover update_metrics shape contract and
the per-env VOC scale propagation.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Tooling (the bulk of this change):
- scripts/train_tpv_wholebody_workflow.sh: 729-line host-side driver for
the TPV ReconBody training pipeline. Provides an interactive `run` mode
that walks init -> replay -> set-frames -> submit with stage-skip
detection, and per-stage subcommands (`init`, `replay`, `set-frames`,
`submit`) for scripted reruns. Orchestrates Docker (`./workflow/run.sh
exec`) and OSMO submission from the host; aborts if invoked inside the
container.
- scripts/retarget/create_soma_task.py: 441-line scaffolder that writes
`experiments/<exp_id>/config.yaml` from the existing recon_body_*
template and idempotently registers the id in
`experiments/registry.yaml` under the whole-body section, preserving
comments/blank lines. Used by stage 6 of the workflow and runnable
standalone.
- experiments/run_experiment.py (+169), experiments/utils.py (+52),
scripts/rsl_rl/eval.py (+43): wiring needed by the workflow driver.

## Task config:
- v2p_whole_body/config/sonic/g1/g1_sonic_env_cfg.py: switch the timeout
DoneTerm from `il_mdp.time_out` to `timestep_termination` keyed on the
motion command, so episode length follows the per-motion frame range
instead of a fixed wall-clock budget.

## New experiments + assets:
- experiments/recon_body_corn_can_right_left_handover_01 and
experiments/recon_body_sit_skinny_wood_chair_01: config.yaml + registry
entries, retargeted G1 motion_v1 parquet, and textured object meshes
(.obj/.urdf/.mtl/.png) under the SOMA whole_body asset tree.

## Existing-config tweaks:
- recon_body_blue_trash_can_drag_007: motion frame range 0..250 ->
50..400.
- recon_body_snack_box_pick_and_place_01: motion frame range 250..500 ->
  300..-1, episode_length_s 5.0.


## Tests
- [x] Run the full data processing pipeline with
`process_soma_sequnces.sh`
- [x] Run the task initialization and training pipeline with
`train_tpv_wholebody_workflow.sh`
- [x] launched all five jobs in OSMO, [example
1](https://wandb.ai/nvidia-isaac/v2d-whole-body-tpv/runs/mxno4i5z?nw=nwuserhuihuaz),
[example
2](https://wandb.ai/nvidia-isaac/v2d-whole-body-tpv/runs/qtgv5it3?nw=nwuserhuihuaz)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add SECURITY.md file to repo for public-facing releases. Thanks!
…hot updates (#74)

Lower bound stays on 0.12.x: 0.13+ introduces a ~6x per-video slowdown
on Qwen3-VL multimodal prefill at TP=8 (verified by A/B on identical
HoT3D Aria 1080p workload, groot-h100-01).

Upper bound already dodged the 0.20+ Qwen3-VL deepstack-tokens
regression (`num_tokens > deepstack_input_embeds_num_tokens`); kept.

Update the screenshots of the v2d webapp

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Motionbricks planner migration into video_to_data + planning-time
validation

Replaces the legacy MFM-based planner with the new motionbricks backend
(stacked on the task-40 dex3-retargeting MR) and adds planning-time
validation so the output parquet is trainable end-to-end with no
post-hoc
patches.

## Summary

- Self-contained `torch.package` motionbricks backend bundled at
  `planner/motionbricks/assets/models/planner.pkg` (Git LFS). No gr00t
  runtime dependency.
- `g1_planner.py` rewired to the new backend; output is the unified
  `motion_v1` parquet schema, Hive-partitioned at
  `<output>/planner_processed/sequence_id=…/robot_name=…/*.parquet`.
- Support surface reconstruction split into `planner/support_recon.py`
  with a phantom-tool filter (a tool resting on the target no longer
  emits a support disk that intersects the target body in sim).
- New `planner/utils/` subpackage for shared helpers (coordinate
  transforms, motion loading, validation). Legacy MFM backend kept under
  `planner/mfm/` for reference and explicitly noqa-ed; everything else
  enforces ruff/mypy.

## Pipeline

V2P retargeted parquet
  → Step 1: nominal FK
  → Step 2: load + interpolate + (optional contact-driven trim)
→ Step 3–4: transform_reference (local frame fix → yaw → position align
→ workspace offset)
→ Step 5: build warmup trajectory (hold nominal → interp → hold start →
reference)
  → Step 6: motionbricks chunked autoregressive inference
→ Step 7: assemble full qpos (planner body + reference fingers +
optional fixed legs/root)
  → Step 8: save motion_v1 parquet + post-write invariant assertions
  → Step 8b: reconstruct support USDA with phantom-tool filter
  → Step 9: optional MuJoCo viewer

## What's new vs the legacy planner

### Planning-time validation (`planner/utils/validation.py`)

Two entry points called from `g1_planner.py`:

- **`warn_reference_issues(motion, ref_data, robot_type)`** — pre-plan.
  Warns when the input motion is missing required fields, has an
  off-FPS source, references asset paths that don't resolve locally, or
  has URDF `<mesh filename=>` deps not present in the workspace. These
  are upstream (retargeting / asset pipeline) responsibilities; the
  planner never auto-fixes them.
- **`assert_motion_parquet_invariants(parquet_path, robot_type)`** —
  post-plan, hard-fail. Verifies:
  - `object_root_position` length matches `object_body_position` and
    mirrors body 0 to within 1 mm.
  - `ee_link_names` matches the actual EE body in the data
    (`*_hand_palm_link` for dex3, `*_wrist_yaw_link` for sharpa).
  - `hand_frames_w[palm]` agrees with `ee_pose_w` per side (catches a
    skipped V2P→planner rigid transform on hand keypoints).
  - `robot_joint_names` contains the tracked finger joints (catches the
    case where the env's tracking_command would raise on
    `cfg.joint_names` resolution).
  - Every contact-field shape is `(2, T, N, 3)` with T matching the
    body trajectory.
  - Every `object_urdf_paths` / `object_mesh_paths` entry exists; every
    URDF mesh dependency exists.

### Output parquet contract enforced in `save_planner_parquet`

- `robot_joint_names` / `robot_joint_positions` cover every actuated
  joint (body + fingers) in MuJoCo joint order. The per-side
  `hand_finger_joints` / `hand_finger_joint_names` lists stay
  populated for callers that want the side-segregated view.
- `object_root_position` / `object_root_axis_angle` derived from body 0
  of the planner-frame object pose so the env's articulated scene init
  always lands where the trajectory starts.
- `ee_link_names` is robot-conditional (palm for dex3, wrist_yaw_link
  for default).
- `hand_frames_w` and the four contact-field groups are lifted into the
  planner frame via the same per-frame rigid transform that
  `transform_reference` applies to `ee_pose_w` / `object_body_position`,
  so every field of the output parquet lives in a single coherent
  frame.

### Support surface reconstruction

`planner/support_recon.py` runs after the parquet write. The new
phantom-tool filter drops a body's support disk if any other body sits
in its xy circle for ≥ 90 % of frames within Δz < 10 cm — catches the
case where a tool is "still" only because it's resting on the target.
Detection uses per-body trajectories from the parquet, no name-prefix
convention needed.

## Module layout

planner/
├── g1_planner.py             CLI orchestration; sole consumer of utils/
├── support_recon.py support reconstruction with phantom-tool filter
├── trajectory.py             warmup trajectory builder
├── visualization.py          MuJoCo viewer
├── motionbricks/             motionbricks model backend (torch.package)
├── mfm/                      legacy MFM backend (kept; noqa-ed)
└── utils/                    pure helpers, no planner state
├── transforms.py quat math + rigid primitives + transform_reference
pipeline
├── loader.py V2P motion resampling (linear + SLERP + masked contact
interp)
    └── validation.py         pre-plan warn + post-plan assert

`planner.utils.*` is consumed only by `g1_planner.py`; the orchestration
script is the only entry point in the package.

## Lint / typing

- File-level `# ruff: noqa: ANN001, ANN201, ANN202, ANN204, D102, D103,
  D107, D417` blocks removed from every active file. Annotations and
  docstrings added on the public surface.
- Suppression retained on `planner/mfm/*` only — the legacy backend
  will be removed; not worth churning.
- Local mypy / ruff pre-commit hooks pass; CI mypy fixes (`_dc._is_type`
  attr-defined ignores + a `previous_range` None-narrowing assertion)
  are in the final commit.

## Verification

Run end-to-end under the `v2p` conda env on two known-good references:

| Sequence | Robot | T frames | FPS | Wrist EE | Notes |
|---|---|---|---|---|---|
| `taco_skim_off__spoon__pan_20230926_011` | dex3 | 1980 | 150 | 78.3 mm
| phantom support on `tool` correctly filtered (tool sits on target 100
% of frames) |
| `dataset_s01_mixer_use_02` | dex3 | 2660 | 100 | 73.7 mm | no phantom
 support |

Both runs:
- Pre-plan: emit only `REFERENCE WARNING` lines for unresolvable
  container paths in the upstream parquet (planner's path remapper
  then re-roots them locally).
- Post-plan: `planner_validation: <hash>.parquet passed all
  invariants.` — zero diagnostics.
- Output parquets load cleanly via
  `motion_schema.reader.load_motion_data_parquet` and through
  `SceneConfig.from_motion_file(...)` for env spawn.

Run command (taco_skim example):

```bash
PYTHONPATH=robotic_grounding/source/robotic_grounding \
  MUJOCO_GL=egl python -m robotic_grounding.planner.g1_planner \
    --robot dex3 \
    --v2p_parquet <…>/assets/human_motion_data/taco/taco_dex3_processed \
    --v2p_robot_name dex3 \
    --v2p_sequence taco_skim_off__spoon__pan_20230926_011 \
    --target_fps 150 \
    --output <out_dir> \
    --no_viewer

Out of scope / known caveats

- A few task-48-era taco assets
(assets/urdfs/taco/{040,067}_rigid.urdf +
assets/meshes/taco/obj_{040,067}_visual.stl) are still untracked in
this worktree. They're not required for the sequences exercised
above; including them in this MR is left as a follow-up.
- planner/mfm/ is retained verbatim. It can be removed once the
motionbricks backend has soaked in training for a release cycle.
- Env-side G1SonicReconHandEnvCfg.episode_length_s and similar
consumer-side fixes live downstream of this MR; this PR's
invariants run on the planner output only.

Files changed (high level)

- Added: planner/motionbricks/, planner/utils/{__init__,transforms,loader,validation}.py,
planner/support_recon.py, planner/motionbricks/assets/models/planner.pkg (LFS).
- Modified: planner/g1_planner.py (orchestration + validation
wiring), planner/visualization.py (inertial on injected vis
bodies, mesh scale, warmup-hold), planner/README.md,
planner/trajectory.py (lint).
- Removed: planner/transforms.py, planner/frame_transforms.py,
planner/v2p_loader.py, planner/planner_validation.py (moved /
merged into planner/utils/).
)

Root-level dotfiles had accumulated rules that only applied to one
subproject (mainly robotic_grounding). Move them into each package's own
copies and add a CI workflow that prevents the drift from coming back.

Moves and content shifts:
- .pre-commit-config.yaml and .envrc -> robotic_grounding/. The
robotic_grounding CI was already keying its cache on
robotic_grounding/.pre-commit-config.yaml; this lines that up.
- Trim root .gitignore and .gitattributes to universal rules only; push
package-anchored rules (robotic_grounding/checkpoints/,
reconstruction/soma_data/, taco LFS paths, *.pkg, ...) into the matching
subproject .gitignore / .gitattributes.
- Drop CLAUDE.md ignore rules from root and robotic_grounding gitignores
so per-package agent guidance is committable; add
video_ingestion_agent/CLAUDE.md which had been silently ignored.

New CI: .github/workflows/monorepo_hygiene.yml runs two jobs on changes
to root config:
- check_root_files: fails if root .gitignore / .gitattributes contain
patterns anchored to a subproject path, or if .pre-commit-config.yaml /
.envrc exist at root.
- agent_context_check: fails if the root .gitignore ignores CLAUDE.md or
AGENTS.md (per-package gitignores are out of scope by design).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#76)

atomic-clip segmentation + auto-discovered penetration filter
## Summary

New hand-reconstruction stack, end-to-end pipeline rework, and Apache
2.0 LICENSEs for the authored modules.

**New Docker modules**
- `v2d_mediapipe` — single-image hand bbox + handedness (CPU).
- `v2d_hamer` — per-frame MANO from SAM2 hand masks + overlay renderer +
shared `align_hands`.
- `v2d_wilor` — hand bbox + MANO from image / image-list / video / SAM2
masks.
- `v2d_droid_slam` — monocular SLAM → per-frame camera poses (+ optional
keyframe depth / PLY). Accepts single or per-frame intrinsics; optional
metric-depth folder for scale alignment.
- `v2d_gsplat_refinement` — 2DGS / hand-Gaussian refinement, with
face-tied hand mode, bg init, and a visualizer.

**Pipeline**
- `run_v2d_ego_e2e.py` rewritten to use the hamer-style per-frame
cam-frame alignment (`dynhamr_to_hamer_tracks` → `align_hands`). Robust
to bad ViPE intrinsics / camera poses because the alignment never
touches inter-frame extrinsics.
- Same renderer drives both the aligned and unaligned comparison videos
via a new `use_pre_dz_cam_t` flag.
- SAM3D mesh generation is seedable (`--seed`) for reproducibility.
- MoGe accepts an `--input_intrinsics` focal-length prior.

**Bug fix**
- SAM2 multi-frame prompts now run under `torch.autocast(bfloat16)` —
without it, the cross-attention dtype mismatch broke any prompt at
`frame_idx > 0`.

**Cleanup**
- Removed legacy `v2d_hand_alignment` modules superseded by the
hamer-style path: `align_world_results.py`,
`run_align_world_results.py`, `render_dynhamr_video.py`,
`run_render_dynhamr_video.py`.

**Licensing (commit `d323b3c0`)**
Adds 31 Apache 2.0 LICENSE files: umbrella `reconstruction/LICENSE` +
one per module for every module whose code is entirely authored (Docker
wrappers, lib wrappers, shared packages).

Intentionally omitted pending legal review of in-tree upstream code:
- `v2d_foundation_pose` — `lib/FoundationPose/` is in-tree (NVIDIA Sec
3.3).
- `v2d_nlf` — `lib/lib_smpl/smplpytorch/` is in-tree (GPL-3).
- `v2d_sam3d_body` — `lib/sam_3d_body/` is in-tree (Meta SAM License).

For these three, the upstream LICENSE files at the in-tree subdirectory
level already cover the vendored code; the wrapper-level
derivative-vs-independent call is deferred. `v2d_raft` is also
unlicensed — empty placeholder.

## Test plan
- [ ] `./scripts/build_containers.sh` succeeds for all listed modules
- [ ] `python modules/v2d_pipelines/run_v2d_ego_e2e.py …` produces both
`render_aligned_{ds}.mp4` and `render_unaligned_{ds}.mp4` from a single
`align_hands` pass
- [ ] `--seed 0` yields a deterministic SAM3D mesh; `--seed -1` produces
fresh meshes each run
- [ ] SAM2 mask tracking succeeds with a multi-frame prompt JSON
(regression test for the bfloat16 fix)
- [ ] HaMeR / WiLoR module CLIs still run as standalone wrappers
- [ ] DROID-SLAM `run_video_to_slam` produces poses + optional depth on
a sample clip
- [ ] Confirm with legal: deferred LICENSE on `v2d_foundation_pose`,
`v2d_nlf`, `v2d_sam3d_body` is the right call before release
…build scripts (#84)

## Summary

Unblocks the EVT-02 ego e2e walkthrough by fixing two QA-reported setup
bugs and tightening the install/build story.

- **bug 6197910** — `scripts/install_packages.sh` had drifted out of
sync with the modules tree; missing entries (`v2d_depth`,
`v2d_hand_alignment/docker`, etc.) caused `ModuleNotFoundError`
mid-pipeline. Script now installs every host-side package under
`modules/`. `v2d_pipelines/pyproject.toml` also now declares its direct
host-side imports (`trimesh`, `OpenEXR`, `numpy`) so no ad-hoc `pip
install` is needed.
- **bug 6203170** — MANO assets now live at
`data/weights/hand/models/MANO_RIGHT.pkl` (manotorch's required
convention). `v2d_ego_hand_reconstruction/docker/run_reconstruction.py`
reads from the nested path and copies into the vendored container's flat
location. Docs updated.
- **New focused scripts** — `scripts/install_ego_e2e_packages.sh` and
`scripts/build_ego_e2e_containers.sh` install/build only what
`run_v2d_ego_e2e.py` needs. `docs/ego_e2e_setup.md` links to both the
focused and the full scripts.

## Test plan

- [ ] Fresh `.venv` + `./scripts/install_ego_e2e_packages.sh` resolves
cleanly
- [ ] Fresh `.venv` + `./scripts/install_packages.sh` resolves cleanly
- [ ] `python -c "import v2d.pipelines.run_v2d_ego_e2e"` succeeds
without ad-hoc `pip install` steps
- [ ] `./scripts/build_ego_e2e_containers.sh` builds all required images
- [ ] End-to-end `run_v2d_ego_e2e.py` runs against `assets/airplane.mp4`
with `data/weights/hand/models/MANO_RIGHT.pkl` (no manotorch assertion)
- [ ] QA re-runs EVT-02 and confirms the bug 6197910 / 6203170
workarounds are no longer needed
#85)

## Summary
- `app.py:87-91` gates the cross-tab `reconstruct_btn.click()`
registration
    on `"reconstruction" in services`. That key is only set when
    `AppConfig.reconstruction` is a populated dict AND
    `ReconstructionService(...)` constructs without raising.
- The default config path — `AppConfig.from_project_configs()`, taken
when
    `scripts/run_webapp.py` is launched without `--config` — reads only
`retrieval.yaml` and `ingestion.yaml`. Neither has a `reconstruction:`
block. Result: the Reconstruct button appears on the page, gets enabled
after a successful query, but has **no click handler** — clicking is a
    silent no-op (QA-reported behavior).
- This patch adds an `elif` branch that wires a fallback handler firing
a
`gr.Warning(...)` toast explaining what to launch with. Happy path is
    unchanged.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#87)

## Summary
- `database_tab.load_database()` called `resolve_graph_db_path(db_dir)`
    **outside** its `try/except`. If that call raised — `Path(...)` on
    adversarial input, `OSError` / `PermissionError` on an unreadable
parent, etc. — the function exited without producing the 10-tuple the
    Gradio `.click()` binding expects, and the frontend sat in a loading
    spinner forever with no error surfaced. This violates the F.1 Error
    paths spec contract ("no silent hangs in any case") and is the
    symptom QA reported when typing a bad path into the Database
    directory input.
  - Fix: hoist the `try/except` to wrap the resolve call, and add
    `gr.Warning(...)` toasts on both failure paths so the user gets a
    visible signal regardless of which output widget is animating.
  - Also switched the exception logger to `logger.exception(...)` so the
    stack trace lands in stderr for future audits.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#86)

- `_read_worker_progress()` returns **deltas** since its previous call
(it
tracks consumed JSONL lines per worker via `progress_lines_read`). Both
call sites — the 2 s poll loop and the final read — were *assigning* the
tuple back into the outer totals, so once the JSONL was fully drained,
    the next call returned (0, 0, 0) and clobbered the prior counts.
- Result before this patch: a successful 2/2-video, 6-clip batch run
shows
`Videos processed: 0/0, Failed: 0, Total clips: 0` in the webapp, while
    `summary_worker_*.json` on disk records the real successful counts.
    ("Workers" and "Total time" were correct because they weren't routed
    through the buggy helper — exactly what QA flagged.)
- Fix: accumulate (`+=`) the delta tuple at both call sites; no change
to
    `_read_worker_progress()` itself.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#82)

Three independent bug fixes that surfaced while running the HoT3D
benchmark suite at scale; isolated here for fast review and merge.

1. Dockerfile: `uv sync` was missing `--extra local`. The ingestion
`__init__` chain eagerly imports `transformers` via `visual_extractor`,
which needs `accelerate`. Without `--extra local`, entity_extraction
crashes with `ImportError: accelerate` on EVERY clip in the image,
producing 0 entities written. This affected the `s3_ingest_v10b` batch
ingest (100% entity-extraction failure rate) before being diagnosed. The
local extra is light (transformers, accelerate, timm, bitsandbytes) and
does not conflict with the vLLM-backed code path.

2. entity_graph_nodes.py: frame_id values written into vector.db were
NOT video-unique. `video_processor.py` emits `frame_<idx>` and we passed
it through unchanged into add_frames_batch. The vector.db schema's
UNIQUE(frame_id) constraint combined with INSERT OR REPLACE caused
cross-video overwrites: in a multi-video ingest, only the last writer
for each `frame_<idx>` slot survived, leaving the table with ~150 rows
total instead of 30k+. This silently destroyed ~99.5% of frame
embeddings on the v10c batch ingest with no error surfaced. Fix:
namespace with `f"{video_id_str}/{frame_id}"` on write. After fix, v10d
ingest landed 21,539 frame embeddings vs the prior 154.

3. scripts/run_benchmark.py: parallel workers all defaulted to cuda:0
for SigLIP-2 embedding regardless of backend. The old guard skipped
CUDA_VISIBLE_DEVICES pinning for the vllm backend on the reasoning that
vLLM owns the GPU, but the per-worker SigLIP-2 embedding model still
runs in-process on the worker, so all N workers OOM'd on a shared cuda:0
(v10 ingest: 11/136 videos completed). Fix mirrors
run_batch_ingestion.py: unconditional pinning, honors upstream
CUDA_VISIBLE_DEVICES (so OSMO container device sets are respected),
modulos worker_id across visible devices. Also threads a `--worker-id`
arg so per-worker progress files don't collide and surfaces non-zero
worker exits for the caller.

Each fix was validated:
* Dockerfile: local docker smoke ingestion completed end-to-end with
entities + relationships populated.
* frame_id: v10d OSMO ingest emitted 21,539 namespaced frame IDs
matching `<video_id>/frame_<idx>` pattern.
* run_benchmark.py: v10d ingest hit 198/198 videos with no per-worker
OOM in the logs.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… headers, DCO sign-off

NVIDIA OSRB approval for the dual CC-BY-4.0 + Apache-2.0 release
flagged four BEFORE-WE-CAN-CLOSE items. This commit addresses all of
them on the video_ingestion_agent surface.

1. License file (`video_ingestion_agent/LICENSE`)
   Replace the previous 15-line Apache-2.0 short notice with the full
   CC-BY-4.0-Apache2 Dual License text (~700 lines): NVIDIA preamble +
   verbatim Creative Commons Attribution 4.0 International + verbatim
   Apache License 2.0.

2. Third-party notices (`video_ingestion_agent/NOTICE`)
   New file. Enumerates the 41 direct dependencies declared in
   pyproject.toml (top-level + every optional-dependencies extra). Each
   entry follows the NVIDIA modstar template: owner + SPDX license +
   license URL + package name/version + project URL. Versions sourced
   from uv.lock; licenses hand-curated against each project's upstream
   LICENSE file.

3. SPDX headers normalized across the package (~125 files)
   Every code/config file under video_ingestion_agent/ now carries the
   2-line dual header:
     # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
     # SPDX-License-Identifier: CC-BY-4.0 AND Apache-2.0
   Previously the headers used the 3-line Apache-2.0-only template (107
   files) or a few variant formats (Dockerfile, docs/conf.py, scripts/
   run_osmo.py docstring variant — 9 files), and 9 files (osmo_workflows
   yamls and reconstruction_interface __init__.py files) had no header
   at all. Year normalized to 2026 throughout.

4. Contribution policy (`CONTRIBUTING.md`, `.github/pull_request_template.md`,
   `README.md`)
   Add a Sign-off section to CONTRIBUTING.md referencing the Developer
   Certificate of Origin (https://developercertificate.org/) and
   requiring `git commit -s` on every commit. Add a top-level
   pull_request_template.md with a Checklist including the DCO
   reminder. Expand the README License section to mention the dual
   license + the NOTICE file, and add a brief Contributing section
   pointing at CONTRIBUTING.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Wei Liu <liuw@nvidia.com>
@nv-liuw
nv-liuw requested a review from jaybdub May 27, 2026 18:57
… Your Work" wording

Replace the prior compact Sign-off section with the canonical
"Signing Your Work" template — bullet-list rationale plus the full
verbatim Developer Certificate of Origin v1.1 text — so the policy is
self-contained in the repo rather than relying on the external link.

Update the anchor references in `.github/pull_request_template.md`
and `video_ingestion_agent/README.md` from the old
`#sign-off-developer-certificate-of-origin` anchor to the new
`#signing-your-work` anchor.

No functional change vs. the prior compact section — same DCO, same
git commit -s requirement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Wei Liu <liuw@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.