Instructions to use morinoppp/comos_predict_2B_GR1_action_cond with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Cosmos
How to use morinoppp/comos_predict_2B_GR1_action_cond with Cosmos:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
Upload folder using huggingface_hub
Browse files- .gitattributes +6 -0
- README.md +137 -3
- examples/episode_000000.mp4 +3 -0
- examples/episode_000008.mp4 +3 -0
- examples/episode_000087.mp4 +3 -0
- examples/gt/episode_000000.mp4 +3 -0
- examples/gt/episode_000008.mp4 +3 -0
- examples/gt/episode_000087.mp4 +3 -0
- inference/inference_gr00t.py +389 -0
- inference/inference_pipeline.py +436 -0
- model_ema_bf16.pt +3 -0
- scripts/convert_distcp_to_pt.py +134 -0
- scripts/eval_gr1_robot.sh +245 -0
- scripts/prepare_gr1_eval_data.py +127 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,9 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
examples/episode_000000.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
examples/episode_000008.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
examples/episode_000087.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
examples/gt/episode_000000.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
examples/gt/episode_000008.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
examples/gt/episode_000087.mp4 filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -1,3 +1,137 @@
|
|
| 1 |
-
---
|
| 2 |
-
license: apache-2.0
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
tags:
|
| 4 |
+
- robotics
|
| 5 |
+
- world-model
|
| 6 |
+
- video-prediction
|
| 7 |
+
- action-conditioned
|
| 8 |
+
- nvidia
|
| 9 |
+
- cosmos
|
| 10 |
+
base_model: nvidia/Cosmos-Predict2.5-2B
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# Cosmos-Predict2.5-2B GR1 Action-Conditioned World Model
|
| 14 |
+
|
| 15 |
+
Action-conditioned video prediction model fine-tuned from [nvidia/Cosmos-Predict2.5-2B](https://huggingface.co/nvidia/Cosmos-Predict2.5-2B) on GR1 dual-arm robot teleoperation data.
|
| 16 |
+
|
| 17 |
+
Given an initial observation frame and a sequence of robot actions, this model predicts future video frames that depict the robot executing those actions.
|
| 18 |
+
|
| 19 |
+
## Model Details
|
| 20 |
+
|
| 21 |
+
- **Base model:** Cosmos-Predict2.5-2B (video2world pre-trained)
|
| 22 |
+
- **Fine-tuning data:** GR1 dual-arm robot teleoperation (LeRobot format)
|
| 23 |
+
- **Action dimension:** 29 (left_arm:7 + left_hand:6 + right_arm:7 + right_hand:6 + waist:3)
|
| 24 |
+
- **Video frames:** 13 frames per chunk (1 conditional + 12 predicted)
|
| 25 |
+
- **Temporal compression:** 4x (13 image frames = 4 latent frames)
|
| 26 |
+
- **Resolution:** 480x832
|
| 27 |
+
- **Checkpoint format:** EMA weights in bf16 (`model_ema_bf16.pt`, ~4GB)
|
| 28 |
+
|
| 29 |
+
## Training Configuration
|
| 30 |
+
|
| 31 |
+
| Parameter | Value |
|
| 32 |
+
|-----------|-------|
|
| 33 |
+
| GPUs | 4x H200 (140GB) |
|
| 34 |
+
| Batch size | 4 per GPU (global=16) |
|
| 35 |
+
| Learning rate | 8e-5 |
|
| 36 |
+
| Max iterations | 4,000 |
|
| 37 |
+
| Save interval | 2,000 |
|
| 38 |
+
| Optimizer | AdamW (cosine schedule) |
|
| 39 |
+
| Precision | bf16 |
|
| 40 |
+
| Episodes per task | 3 (uniform sampling) |
|
| 41 |
+
| VAE | Wan2.1 |
|
| 42 |
+
| Text encoder | Cosmos-Reason1-7B |
|
| 43 |
+
| Tokenizer | Qwen2.5-VL-7B-Instruct |
|
| 44 |
+
|
| 45 |
+
## Inference
|
| 46 |
+
|
| 47 |
+
### Prerequisites
|
| 48 |
+
|
| 49 |
+
This model requires the [Cosmos-Predict2.5](https://github.com/NVIDIA/Cosmos-Predict2.5) codebase and the following dependencies:
|
| 50 |
+
|
| 51 |
+
- Wan2.1 VAE: `Wan2.1_VAE.pth`
|
| 52 |
+
- Text encoder: Cosmos-Reason1-7B
|
| 53 |
+
- Tokenizer: Qwen2.5-VL-7B-Instruct
|
| 54 |
+
|
| 55 |
+
### Quick Start
|
| 56 |
+
|
| 57 |
+
```bash
|
| 58 |
+
python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
|
| 59 |
+
--experiment=cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes_release_oss \
|
| 60 |
+
--ckpt_path=/path/to/model_ema_bf16.pt \
|
| 61 |
+
--input_video_root=/path/to/eval_data \
|
| 62 |
+
--save_root=/path/to/output \
|
| 63 |
+
--resolution 480,832 \
|
| 64 |
+
--guidance 0 \
|
| 65 |
+
--chunk_size 12 \
|
| 66 |
+
--fps_downsample_ratio 2 \
|
| 67 |
+
--save_fps 10 \
|
| 68 |
+
--vae_path /path/to/Wan2.1_VAE.pth \
|
| 69 |
+
--text_encoder_path /path/to/Cosmos-Reason1-7B \
|
| 70 |
+
--qwen_path /path/to/Qwen2.5-VL-7B-Instruct
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
### Input Data Format
|
| 74 |
+
|
| 75 |
+
The inference script expects a directory containing paired files:
|
| 76 |
+
- `episode_XXXXXX.mp4` - Input video (first frame used as conditional)
|
| 77 |
+
- `episode_XXXXXX_actions.npy` - Action array of shape `(T, 29)` in numpy format
|
| 78 |
+
|
| 79 |
+
### Eval Data Preparation
|
| 80 |
+
|
| 81 |
+
Use `scripts/prepare_gr1_eval_data.py` to extract eval episodes from a LeRobot dataset:
|
| 82 |
+
|
| 83 |
+
```bash
|
| 84 |
+
python scripts/prepare_gr1_eval_data.py \
|
| 85 |
+
--dataset-path /path/to/GR1_robot \
|
| 86 |
+
--output-dir /path/to/eval_data \
|
| 87 |
+
--num-episodes 100
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
## Examples
|
| 91 |
+
|
| 92 |
+
The `examples/` directory contains sample predictions alongside ground truth videos:
|
| 93 |
+
|
| 94 |
+
| Episode | Prediction | Ground Truth |
|
| 95 |
+
|---------|-----------|--------------|
|
| 96 |
+
| 000000 | [episode_000000.mp4](examples/episode_000000.mp4) | [episode_000000.mp4](examples/gt/episode_000000.mp4) |
|
| 97 |
+
| 000008 | [episode_000008.mp4](examples/episode_000008.mp4) | [episode_000008.mp4](examples/gt/episode_000008.mp4) |
|
| 98 |
+
| 000087 | [episode_000087.mp4](examples/episode_000087.mp4) | [episode_000087.mp4](examples/gt/episode_000087.mp4) |
|
| 99 |
+
|
| 100 |
+
## File Structure
|
| 101 |
+
|
| 102 |
+
```
|
| 103 |
+
.
|
| 104 |
+
├── model_ema_bf16.pt # Model weights (EMA, bf16)
|
| 105 |
+
├── inference/
|
| 106 |
+
│ ├── inference_gr00t.py # Main inference script
|
| 107 |
+
│ └── inference_pipeline.py # Inference pipeline (ActionVideo2WorldInference)
|
| 108 |
+
├── scripts/
|
| 109 |
+
│ ├── eval_gr1_robot.sh # One-command eval script
|
| 110 |
+
│ ├── prepare_gr1_eval_data.py # Eval data preparation
|
| 111 |
+
│ └── convert_distcp_to_pt.py # DCP -> PT checkpoint converter
|
| 112 |
+
└── examples/
|
| 113 |
+
├── episode_000000.mp4 # Predicted videos
|
| 114 |
+
├── episode_000008.mp4
|
| 115 |
+
├── episode_000087.mp4
|
| 116 |
+
└── gt/ # Ground truth videos
|
| 117 |
+
├── episode_000000.mp4
|
| 118 |
+
├── episode_000008.mp4
|
| 119 |
+
└── episode_000087.mp4
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
## Limitations
|
| 123 |
+
|
| 124 |
+
- Trained only on GR1 dual-arm robot data; may not generalize to other embodiments
|
| 125 |
+
- Prediction quality degrades for long-horizon generation (many chunks)
|
| 126 |
+
- Episodes with fewer than 12 actions cannot be processed (chunk_size=12)
|
| 127 |
+
- No classifier-free guidance (guidance=0) works best for this model
|
| 128 |
+
|
| 129 |
+
## License
|
| 130 |
+
|
| 131 |
+
Apache-2.0 (same as base Cosmos-Predict2.5)
|
| 132 |
+
|
| 133 |
+
## Acknowledgements
|
| 134 |
+
|
| 135 |
+
- Base model: [NVIDIA Cosmos-Predict2.5](https://github.com/NVIDIA/Cosmos-Predict2.5)
|
| 136 |
+
- Training data: GR1 teleoperation dataset (LeRobot format)
|
| 137 |
+
- VAE: [Wan2.1](https://huggingface.co/Wan-AI/Wan2.1-T2V-14B)
|
examples/episode_000000.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a5f14c2d6e41c7eeae80f354d5b094ab7eccbf32620bd45225959f7ab05ff277
|
| 3 |
+
size 199034
|
examples/episode_000008.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6c1b85ee2929a2616e7f2cd45bb3667b70dc4e4382270c4164d3c1074f121439
|
| 3 |
+
size 394188
|
examples/episode_000087.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1da2555b494710a119c08760aebbf744e4b281874e01a215a66d19590e44e2d1
|
| 3 |
+
size 623808
|
examples/gt/episode_000000.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bd87b160860d6fdd47846eb9839af86d43684c59e640be88d1df1e120a200066
|
| 3 |
+
size 530414
|
examples/gt/episode_000008.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9ac15a966e56f983a29037b57efc5c8f5ad5ec2a026e5e4d07f7b3471aad407c
|
| 3 |
+
size 540443
|
examples/gt/episode_000087.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ec40150f03290095fca08842f89ae4cd18c6b7651053bdfa24f6a83517285aef
|
| 3 |
+
size 1202359
|
inference/inference_gr00t.py
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# ---------------------------------- benchmark ----------------------------------
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
CUDA_VISIBLE_DEVICES=0 PYTHONPATH=. python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
|
| 23 |
+
--experiment=cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame \
|
| 24 |
+
--ckpt_path s3://bucket/cosmos_predict2_action_conditioned/action_conditional/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame/checkpoints/iter_000014000 \
|
| 25 |
+
--input_video_root results/gr00t_gr1/gt \
|
| 26 |
+
--save_root results/gr00t_gr1/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame-14k\
|
| 27 |
+
--resolution 480,832 --guidance 0 --chunk_size 12
|
| 28 |
+
|
| 29 |
+
CUDA_VISIBLE_DEVICES=7 PYTHONPATH=. python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
|
| 30 |
+
--experiment=cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame \
|
| 31 |
+
--ckpt_path s3://bucket/cosmos_predict2_action_conditioned/action_conditional/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame/checkpoints/iter_000020000 \
|
| 32 |
+
--input_video_root results/gr00t_gr1/gt \
|
| 33 |
+
--save_root results/gr00t_gr1/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame-20k\
|
| 34 |
+
--resolution 480,832 --guidance 0 --chunk_size 12 --start 80 --end 100
|
| 35 |
+
|
| 36 |
+
CUDA_VISIBLE_DEVICES=1 PYTHONPATH=. python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
|
| 37 |
+
--experiment=cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame \
|
| 38 |
+
--ckpt_path s3://bucket/cosmos_predict2_action_conditioned/action_conditional/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame/checkpoints/iter_000028000 \
|
| 39 |
+
--input_video_root results/gr00t_gr1/gt \
|
| 40 |
+
--save_root results/gr00t_gr1/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame-28k\
|
| 41 |
+
--resolution 480,832 --guidance 0 --chunk_size 12 --start 0 --end 100
|
| 42 |
+
|
| 43 |
+
CUDA_VISIBLE_DEVICES=7 PYTHONPATH=. python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
|
| 44 |
+
--experiment=cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes \
|
| 45 |
+
--ckpt_path s3://bucket/cosmos_predict2_action_conditioned/action_conditional/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes/checkpoints/iter_000004000 \
|
| 46 |
+
--input_video_root results/gr00t_gr1/gt \
|
| 47 |
+
--save_root results/gr00t_gr1/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes-4k\
|
| 48 |
+
--resolution 480,832 --guidance 0 --chunk_size 12 --start 80 --end 100
|
| 49 |
+
|
| 50 |
+
CUDA_VISIBLE_DEVICES=0 PYTHONPATH=. python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
|
| 51 |
+
--experiment=cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_49frame_full \
|
| 52 |
+
--ckpt_path s3://bucket/cosmos_predict2_action_conditioned/action_conditional/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_49frame_full/checkpoints/iter_000004000 \
|
| 53 |
+
--input_video_root results/gr00t_gr1/gt \
|
| 54 |
+
--save_root results/gr00t_gr1/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_49frame_full-6k\
|
| 55 |
+
--resolution 480,832 --guidance 0 --chunk_size 48 --start 80 --end 100
|
| 56 |
+
|
| 57 |
+
CUDA_VISIBLE_DEVICES=0 PYTHONPATH=. python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
|
| 58 |
+
--experiment=cosmos_predict2p5_2B_conditioned_posttrained_rl_merged_action_gr00t_gr1_customized_13frame_full \
|
| 59 |
+
--ckpt_path s3://bucket/cosmos_predict2_action_conditioned/action_conditional/cosmos_predict2p5_2B_conditioned_posttrained_rl_merged_action_gr00t_gr1_customized_13frame_full/checkpoints/iter_000004000 \
|
| 60 |
+
--input_video_root results/gr00t_gr1/gt \
|
| 61 |
+
--save_root results/gr00t_gr1/cosmos_predict2p5_2B_conditioned_posttrained_rl_merged_action_gr00t_gr1_customized_13frame_full-4k\
|
| 62 |
+
--resolution 480,832 --guidance 0 --chunk_size 12 --start 0 --end 100
|
| 63 |
+
|
| 64 |
+
CUDA_VISIBLE_DEVICES=1 PYTHONPATH=. python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
|
| 65 |
+
--experiment=cosmos_predict2p5_2B_conditioned_posttrained_rl_merged_action_gr00t_gr1_customized_13frame_full \
|
| 66 |
+
--ckpt_path s3://bucket/cosmos_predict2_action_conditioned/action_conditional/cosmos_predict2p5_2B_conditioned_posttrained_rl_merged_action_gr00t_gr1_customized_13frame_full/checkpoints/iter_000008000 \
|
| 67 |
+
--input_video_root results/gr00t_gr1/gt \
|
| 68 |
+
--save_root results/gr00t_gr1/cosmos_predict2p5_2B_conditioned_posttrained_rl_merged_action_gr00t_gr1_customized_13frame_full-8k\
|
| 69 |
+
--resolution 480,832 --guidance 0 --chunk_size 12 --start 0 --end 100
|
| 70 |
+
|
| 71 |
+
CUDA_VISIBLE_DEVICES=6 PYTHONPATH=. python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
|
| 72 |
+
--experiment=cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_49frame_full \
|
| 73 |
+
--ckpt_path s3://bucket/cosmos_predict2_action_conditioned/action_conditional/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_49frame_full/checkpoints/iter_000010000 \
|
| 74 |
+
--input_video_root results/gr00t_gr1/gt \
|
| 75 |
+
--save_root results/gr00t_gr1/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_49frame_full-10k\
|
| 76 |
+
--resolution 480,832 --guidance 0 --chunk_size 48 --start 90 --end 100
|
| 77 |
+
|
| 78 |
+
CUDA_VISIBLE_DEVICES=6 PYTHONPATH=. python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
|
| 79 |
+
--experiment=cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_73frame_full \
|
| 80 |
+
--ckpt_path s3://bucket/cosmos_predict2_action_conditioned/action_conditional/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_73frame_full/checkpoints/iter_000006000 \
|
| 81 |
+
--input_video_root results/gr00t_gr1/gt \
|
| 82 |
+
--save_root results/gr00t_gr1/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_73frame_full-6k\
|
| 83 |
+
--resolution 480,832 --guidance 0 --chunk_size 72 --start 70 --end 80
|
| 84 |
+
|
| 85 |
+
CUDA_VISIBLE_DEVICES=5 PYTHONPATH=. python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
|
| 86 |
+
--experiment=cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes \
|
| 87 |
+
--ckpt_path s3://bucket/cosmos_predict2_action_conditioned/action_conditional/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes/checkpoints/iter_000008000 \
|
| 88 |
+
--input_video_root results/gr00t_gr1/gt \
|
| 89 |
+
--save_root results/gr00t_gr1/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes-8k\
|
| 90 |
+
--resolution 480,832 --guidance 0 --chunk_size 12 --start 75 --end 100
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
CUDA_VISIBLE_DEVICES=6 PYTHONPATH=. python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
|
| 94 |
+
--experiment=cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes \
|
| 95 |
+
--ckpt_path s3://bucket/cosmos_predict2_action_conditioned/action_conditional/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes/checkpoints/iter_000010000 \
|
| 96 |
+
--input_video_root results/gr00t_gr1/gt \
|
| 97 |
+
--save_root results/gr00t_gr1/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes-10k\
|
| 98 |
+
--resolution 480,832 --guidance 0 --chunk_size 12 --start 90 --end 100
|
| 99 |
+
|
| 100 |
+
CUDA_VISIBLE_DEVICES=7 PYTHONPATH=. python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
|
| 101 |
+
--experiment=cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes \
|
| 102 |
+
--ckpt_path s3://bucket/cosmos_predict2_action_conditioned/action_conditional/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes/checkpoints/iter_000014000 \
|
| 103 |
+
--input_video_root results/gr00t_gr1/gt \
|
| 104 |
+
--save_root results/gr00t_gr1/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes-14k\
|
| 105 |
+
--resolution 480,832 --guidance 0 --chunk_size 12 --start 90 --end 100
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
CUDA_VISIBLE_DEVICES=7 PYTHONPATH=. python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
|
| 109 |
+
--experiment=cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes \
|
| 110 |
+
--ckpt_path s3://bucket/cosmos_predict2_action_conditioned/action_conditional/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes/checkpoints/iter_000016000 \
|
| 111 |
+
--input_video_root results/gr00t_gr1/gt \
|
| 112 |
+
--save_root results/gr00t_gr1/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes-16k\
|
| 113 |
+
--resolution 480,832 --guidance 0 --chunk_size 12 --start 0 --end 100
|
| 114 |
+
"""
|
| 115 |
+
|
| 116 |
+
import argparse
|
| 117 |
+
import os
|
| 118 |
+
from glob import glob
|
| 119 |
+
|
| 120 |
+
import mediapy
|
| 121 |
+
import numpy as np
|
| 122 |
+
import torch
|
| 123 |
+
from loguru import logger
|
| 124 |
+
|
| 125 |
+
from cosmos_predict2._src.imaginaire.utils import distributed
|
| 126 |
+
from cosmos_predict2._src.predict2.action.inference.inference_pipeline import (
|
| 127 |
+
_DEFAULT_NEGATIVE_PROMPT,
|
| 128 |
+
ActionVideo2WorldInference,
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
_IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", "webp"]
|
| 132 |
+
_VIDEO_EXTENSIONS = [".mp4"]
|
| 133 |
+
|
| 134 |
+
_ACTION_SCALER = 20.0
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def parse_arguments() -> argparse.Namespace:
|
| 138 |
+
"""Parses command-line arguments for the Video2World inference script."""
|
| 139 |
+
parser = argparse.ArgumentParser(description="Image2World/Video2World inference script")
|
| 140 |
+
parser.add_argument("--experiment", type=str, required=True, help="Experiment config")
|
| 141 |
+
parser.add_argument("--chunk_size", type=int, default=12, help="Chunk size for action conditioning")
|
| 142 |
+
parser.add_argument(
|
| 143 |
+
"--num_chunks", type=int, default=12, help="Number of chunks to generate (-1 for all available chunks)"
|
| 144 |
+
)
|
| 145 |
+
parser.add_argument("--guidance", type=int, default=7, help="Guidance value")
|
| 146 |
+
parser.add_argument("--seed", type=int, default=1, help="Guidance value")
|
| 147 |
+
parser.add_argument(
|
| 148 |
+
"--ckpt_path",
|
| 149 |
+
type=str,
|
| 150 |
+
default="",
|
| 151 |
+
help="Path to the checkpoint. If not provided, will use the one specify in the config",
|
| 152 |
+
)
|
| 153 |
+
parser.add_argument("--s3_cred", type=str, default="credentials/s3_checkpoint.secret")
|
| 154 |
+
parser.add_argument(
|
| 155 |
+
"--resolution",
|
| 156 |
+
type=str,
|
| 157 |
+
default="none",
|
| 158 |
+
help="Resolution of the video (H,W). Be default it will use model trained resolution. 9:16",
|
| 159 |
+
)
|
| 160 |
+
parser.add_argument("--input_video_root", type=str, default="bridge/annotation/test_100", help="Action root")
|
| 161 |
+
parser.add_argument("--save_root", type=str, default="results/image2world", help="Save root")
|
| 162 |
+
|
| 163 |
+
# for pi dataset
|
| 164 |
+
parser.add_argument("--camera_id", type=str, default="base", help="Camera id")
|
| 165 |
+
parser.add_argument("--start", type=int, default=0)
|
| 166 |
+
parser.add_argument("--end", type=int, default=100)
|
| 167 |
+
parser.add_argument("--fps_downsample_ratio", type=int, default=1)
|
| 168 |
+
parser.add_argument("--gripper_scale", type=float, default=1.0)
|
| 169 |
+
parser.add_argument("--gripper_key", type=str, default="continuous_gripper_state", help="Gripper key")
|
| 170 |
+
parser.add_argument("--state_key", type=str, default="state", help="State key")
|
| 171 |
+
|
| 172 |
+
parser.add_argument("--reverse", action="store_true", help="Reverse the video")
|
| 173 |
+
parser.add_argument("--single_chunk", action="store_true", help="Single chunk")
|
| 174 |
+
parser.add_argument("--start_frame_idx", type=int, default=0, help="Start frame index")
|
| 175 |
+
parser.add_argument("--save_fps", type=int, default=10, help="Save fps")
|
| 176 |
+
|
| 177 |
+
parser.add_argument(
|
| 178 |
+
"--negative_prompt",
|
| 179 |
+
type=str,
|
| 180 |
+
default=_DEFAULT_NEGATIVE_PROMPT,
|
| 181 |
+
help="Custom negative prompt for classifier-free guidance. If not specified, uses default embeddings from S3.",
|
| 182 |
+
)
|
| 183 |
+
parser.add_argument(
|
| 184 |
+
"--num_latent_conditional_frames",
|
| 185 |
+
type=int,
|
| 186 |
+
default=1,
|
| 187 |
+
help="Number of latent conditional frames (0, 1 or 2). For images, both values work by duplicating frames. For videos, uses the first N frames.",
|
| 188 |
+
)
|
| 189 |
+
# Context parallel arguments
|
| 190 |
+
parser.add_argument(
|
| 191 |
+
"--context_parallel_size",
|
| 192 |
+
type=int,
|
| 193 |
+
default=1,
|
| 194 |
+
help="Context parallel size (number of GPUs to split context over). Set to 8 for 8 GPUs",
|
| 195 |
+
)
|
| 196 |
+
# Local model path overrides (avoid downloading from HuggingFace/S3)
|
| 197 |
+
parser.add_argument("--vae_path", type=str, default="", help="Local path to Wan2.1 VAE .pth file")
|
| 198 |
+
parser.add_argument("--text_encoder_path", type=str, default="", help="Local path to Cosmos-Reason1-7B directory")
|
| 199 |
+
parser.add_argument("--qwen_path", type=str, default="", help="Local path to Qwen2.5-VL-7B-Instruct directory")
|
| 200 |
+
parser.add_argument("--experiment_opts", type=str, nargs="*", default=[], help="Additional Hydra override strings")
|
| 201 |
+
return parser.parse_args()
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def get_action_sequence_from_states(
|
| 205 |
+
data,
|
| 206 |
+
fps_downsample_ratio=1,
|
| 207 |
+
use_quat=False,
|
| 208 |
+
state_key="state",
|
| 209 |
+
gripper_scale=1.0,
|
| 210 |
+
gripper_key="continuous_gripper_state",
|
| 211 |
+
):
|
| 212 |
+
"""
|
| 213 |
+
Get the action sequence from the states.
|
| 214 |
+
"""
|
| 215 |
+
|
| 216 |
+
actions = np.array(data["action"])[::fps_downsample_ratio][:-1]
|
| 217 |
+
return actions
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def get_video_id(img_path: str):
|
| 221 |
+
"""Extract video ID from image path by removing directory and extension."""
|
| 222 |
+
return img_path.split("/")[-1].split(".")[0]
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def main():
|
| 226 |
+
torch.enable_grad(False) # Disable gradient calculations for inference
|
| 227 |
+
args = parse_arguments()
|
| 228 |
+
|
| 229 |
+
# Validate num_latent_conditional_frames at the very beginning
|
| 230 |
+
if args.num_latent_conditional_frames not in [0, 1, 2]:
|
| 231 |
+
raise ValueError(
|
| 232 |
+
f"num_latent_conditional_frames must be 0, 1 or 2, but got {args.num_latent_conditional_frames}"
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
# Determine supported extensions based on num_latent_conditional_frames
|
| 236 |
+
if args.num_latent_conditional_frames > 1:
|
| 237 |
+
supported_extensions = _VIDEO_EXTENSIONS
|
| 238 |
+
# Check if input folder contains any videos
|
| 239 |
+
has_videos = False
|
| 240 |
+
for file_name in os.listdir(args.input_root):
|
| 241 |
+
file_ext = os.path.splitext(file_name)[1].lower()
|
| 242 |
+
if file_ext in _VIDEO_EXTENSIONS:
|
| 243 |
+
has_videos = True
|
| 244 |
+
break
|
| 245 |
+
|
| 246 |
+
if not has_videos:
|
| 247 |
+
raise ValueError(
|
| 248 |
+
f"num_latent_conditional_frames={args.num_latent_conditional_frames} > 1 requires video inputs, "
|
| 249 |
+
f"but no videos found in {args.input_root}. Found extensions: "
|
| 250 |
+
f"{set(os.path.splitext(f)[1].lower() for f in os.listdir(args.input_root) if os.path.splitext(f)[1])}"
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
logger.info(f"Using video-only mode with {args.num_latent_conditional_frames} conditional frames")
|
| 254 |
+
elif args.num_latent_conditional_frames == 1:
|
| 255 |
+
supported_extensions = _IMAGE_EXTENSIONS + _VIDEO_EXTENSIONS
|
| 256 |
+
logger.info(f"Using image+video mode with {args.num_latent_conditional_frames} conditional frame")
|
| 257 |
+
|
| 258 |
+
# Initialize the inference handler with context parallel support
|
| 259 |
+
# Build experiment_opts from local path overrides
|
| 260 |
+
experiment_opts = list(args.experiment_opts)
|
| 261 |
+
if args.vae_path:
|
| 262 |
+
experiment_opts.append(f"++model.config.tokenizer.vae_pth={args.vae_path}")
|
| 263 |
+
logger.info(f"Using local VAE: {args.vae_path}")
|
| 264 |
+
if args.text_encoder_path:
|
| 265 |
+
experiment_opts.append(f"++model.config.text_encoder_config.ckpt_path={args.text_encoder_path}")
|
| 266 |
+
logger.info(f"Using local text encoder: {args.text_encoder_path}")
|
| 267 |
+
if args.qwen_path:
|
| 268 |
+
experiment_opts.append(f"++model.config.text_encoder_config.model_config.tokenizer.tokenizer_type={args.qwen_path}")
|
| 269 |
+
logger.info(f"Using local Qwen tokenizer: {args.qwen_path}")
|
| 270 |
+
|
| 271 |
+
video2world_cli = ActionVideo2WorldInference(
|
| 272 |
+
args.experiment,
|
| 273 |
+
args.ckpt_path,
|
| 274 |
+
args.s3_cred,
|
| 275 |
+
context_parallel_size=args.context_parallel_size,
|
| 276 |
+
experiment_opts=experiment_opts if experiment_opts else None,
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
mem_bytes = torch.cuda.memory_allocated(device=torch.device("cuda" if torch.cuda.is_available() else "cpu"))
|
| 280 |
+
logger.info(f"GPU memory usage after model dcp.load: {mem_bytes / (1024**3):.2f} GB")
|
| 281 |
+
|
| 282 |
+
# get input video and annotation path
|
| 283 |
+
input_video_path = os.path.join(args.input_video_root)
|
| 284 |
+
|
| 285 |
+
# Only process files on rank 0 if using distributed processing
|
| 286 |
+
rank0 = True
|
| 287 |
+
if args.context_parallel_size > 1:
|
| 288 |
+
rank0 = distributed.get_rank() == 0
|
| 289 |
+
|
| 290 |
+
# pdb.set_trace()
|
| 291 |
+
video_list = glob(os.path.join(input_video_path, "*.mp4"))
|
| 292 |
+
input_json_list = [video_path.replace(".mp4", "_actions.npy") for video_path in video_list]
|
| 293 |
+
|
| 294 |
+
# Ensure save directory exists
|
| 295 |
+
os.makedirs(args.save_root, exist_ok=True)
|
| 296 |
+
|
| 297 |
+
# Process each file in the input directory
|
| 298 |
+
for annotation_path, video_path in zip(input_json_list[args.start : args.end], video_list[args.start : args.end]):
|
| 299 |
+
actions = np.load(annotation_path)
|
| 300 |
+
|
| 301 |
+
# Convert camera_id to integer if it's a string and can be converted to an integer
|
| 302 |
+
|
| 303 |
+
actions = actions[: len(actions)]
|
| 304 |
+
video_array = mediapy.read_video(video_path)
|
| 305 |
+
|
| 306 |
+
# Resize video_array with arg.resolution if specified
|
| 307 |
+
if args.resolution != "none":
|
| 308 |
+
try:
|
| 309 |
+
h, w = map(int, args.resolution.split(","))
|
| 310 |
+
video_array = np.stack([mediapy.resize_image(frame, (h, w)) for frame in video_array], axis=0)
|
| 311 |
+
except Exception as e:
|
| 312 |
+
logger.warning(f"Failed to resize video to {args.resolution}: {e}")
|
| 313 |
+
|
| 314 |
+
img_array = video_array[args.start_frame_idx]
|
| 315 |
+
# img_name = annotation_path.split("/")[-1].split(".")[0]
|
| 316 |
+
img_name = video_path.split("/")[-1].split(".")[0]
|
| 317 |
+
|
| 318 |
+
frames = [img_array]
|
| 319 |
+
chunk_video = []
|
| 320 |
+
video_array = video_array[:: args.fps_downsample_ratio]
|
| 321 |
+
|
| 322 |
+
video_name = f"{args.save_root}/{img_name.replace('.jpg', '.mp4')}"
|
| 323 |
+
chunk_video_name = f"{args.save_root}/{img_name + '.mp4'}"
|
| 324 |
+
logger.info(f"Saving video to {video_name}")
|
| 325 |
+
if os.path.exists(chunk_video_name):
|
| 326 |
+
logger.info(f"Video already exists: {chunk_video_name}")
|
| 327 |
+
continue
|
| 328 |
+
|
| 329 |
+
# Calculate the maximum number of chunks to generate
|
| 330 |
+
max_chunks = len(actions) // args.chunk_size
|
| 331 |
+
if args.num_chunks > 0:
|
| 332 |
+
max_chunks = min(max_chunks, args.num_chunks)
|
| 333 |
+
|
| 334 |
+
logger.info(f"Generating {max_chunks} chunks (chunk_size={args.chunk_size}, total_actions={len(actions)})")
|
| 335 |
+
|
| 336 |
+
chunk_count = 0
|
| 337 |
+
for i in range(args.start_frame_idx, len(actions), args.chunk_size):
|
| 338 |
+
if actions[i : i + args.chunk_size].shape[0] != args.chunk_size:
|
| 339 |
+
break
|
| 340 |
+
|
| 341 |
+
# Check if we've reached the desired number of chunks
|
| 342 |
+
if args.num_chunks > 0 and chunk_count >= args.num_chunks:
|
| 343 |
+
logger.info(f"Reached target number of chunks ({args.num_chunks}), stopping generation")
|
| 344 |
+
break
|
| 345 |
+
|
| 346 |
+
logger.info(f"Generating chunk {chunk_count + 1}/{max_chunks}")
|
| 347 |
+
next_img_array, video_clamped = video2world_cli.step_inference(
|
| 348 |
+
img_array=img_array,
|
| 349 |
+
action=actions[i : i + args.chunk_size],
|
| 350 |
+
guidance=args.guidance,
|
| 351 |
+
seed=i,
|
| 352 |
+
)
|
| 353 |
+
frames.append(next_img_array)
|
| 354 |
+
img_array = next_img_array
|
| 355 |
+
chunk_video.append(video_clamped)
|
| 356 |
+
chunk_count += 1
|
| 357 |
+
|
| 358 |
+
if args.single_chunk:
|
| 359 |
+
break
|
| 360 |
+
|
| 361 |
+
if not chunk_video:
|
| 362 |
+
logger.warning(f"Skipping {img_name}: not enough actions for a full chunk ({len(actions)} < {args.chunk_size})")
|
| 363 |
+
continue
|
| 364 |
+
|
| 365 |
+
chunk_list = [chunk_video[0]] + [chunk_video[i][: args.chunk_size] for i in range(1, len(chunk_video))]
|
| 366 |
+
chunk_video = np.concatenate(chunk_list, axis=0)
|
| 367 |
+
if args.single_chunk:
|
| 368 |
+
chunk_video_name = f"{args.save_root}/{img_name + '_single_chunk.mp4'}"
|
| 369 |
+
else:
|
| 370 |
+
# chunk_video_name = f"{args.save_root}/{img_name + '_chunk.mp4'}"
|
| 371 |
+
chunk_video_name = f"{args.save_root}/{img_name + '.mp4'}"
|
| 372 |
+
mediapy.write_video(chunk_video_name, chunk_video, fps=args.save_fps)
|
| 373 |
+
|
| 374 |
+
# concat_video = np.concatenate([chunk_video, video_array[: chunk_video.shape[0]]], axis=2)
|
| 375 |
+
# concat_video_name = f"{args.save_root}/{img_name + '_concat.mp4'}"
|
| 376 |
+
# mediapy.write_video(concat_video_name, concat_video, fps=args.save_fps)
|
| 377 |
+
|
| 378 |
+
logger.info(f"Saved video to {chunk_video_name}")
|
| 379 |
+
|
| 380 |
+
# Synchronize all processes before cleanup
|
| 381 |
+
if args.context_parallel_size > 1:
|
| 382 |
+
torch.distributed.barrier()
|
| 383 |
+
|
| 384 |
+
# Clean up distributed resources
|
| 385 |
+
video2world_cli.cleanup()
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
if __name__ == "__main__":
|
| 389 |
+
main()
|
inference/inference_pipeline.py
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
import numpy as np
|
| 18 |
+
import torch
|
| 19 |
+
import torch.distributed as dist
|
| 20 |
+
import torchvision
|
| 21 |
+
from loguru import logger
|
| 22 |
+
from megatron.core import parallel_state
|
| 23 |
+
|
| 24 |
+
from cosmos_predict2._src.imaginaire.utils import distributed
|
| 25 |
+
from cosmos_predict2._src.interactive.utils.model_loader import (
|
| 26 |
+
load_model_from_checkpoint as load_distilled_model_from_checkpoint,
|
| 27 |
+
)
|
| 28 |
+
from cosmos_predict2._src.predict2.inference.get_t5_emb import get_text_embedding
|
| 29 |
+
from cosmos_predict2._src.predict2.utils.model_loader import load_model_from_checkpoint
|
| 30 |
+
|
| 31 |
+
_DEFAULT_NEGATIVE_PROMPT = "The video captures a series of frames showing ugly scenes, static with no motion, motion blur, over-saturation, shaky footage, low resolution, grainy texture, pixelated images, poorly lit areas, underexposed and overexposed scenes, poor color balance, washed out colors, choppy sequences, jerky movements, low frame rate, artifacting, color banding, unnatural transitions, outdated special effects, fake elements, unconvincing visuals, poorly edited content, jump cuts, visual noise, and flickering. Overall, the video is of poor quality."
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class ActionVideo2WorldInference:
|
| 35 |
+
"""
|
| 36 |
+
Handles the Video2World inference process, including model loading, data preparation,
|
| 37 |
+
and video generation from an image/video and text prompt. Now supports context parallelism
|
| 38 |
+
and distilled model inference.
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
def __init__(
|
| 42 |
+
self,
|
| 43 |
+
experiment_name: str,
|
| 44 |
+
ckpt_path: str,
|
| 45 |
+
s3_credential_path: str,
|
| 46 |
+
context_parallel_size: int = 1,
|
| 47 |
+
distilled: bool = False,
|
| 48 |
+
num_steps: int = 4,
|
| 49 |
+
experiment_opts: list[str] | None = None,
|
| 50 |
+
):
|
| 51 |
+
"""
|
| 52 |
+
Initializes the Video2WorldInference class.
|
| 53 |
+
|
| 54 |
+
Loads the diffusion model and its configuration based on the provided
|
| 55 |
+
experiment name and checkpoint path. Sets up distributed processing if needed.
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
experiment_name (str): Name of the experiment configuration.
|
| 59 |
+
ckpt_path (str): Path to the model checkpoint (local or S3).
|
| 60 |
+
s3_credential_path (str): Path to S3 credentials file (if loading from S3).
|
| 61 |
+
context_parallel_size (int): Number of GPUs for context parallelism.
|
| 62 |
+
distilled (bool): Whether to load a distilled model (DMD2).
|
| 63 |
+
num_steps (int): Number of diffusion steps for inference (default 4 for distilled models).
|
| 64 |
+
experiment_opts (list[str] | None): Hydra-style override strings (e.g. "++model.config.tokenizer.vae_pth=/path").
|
| 65 |
+
"""
|
| 66 |
+
self.experiment_name = experiment_name
|
| 67 |
+
self.ckpt_path = ckpt_path
|
| 68 |
+
self.s3_credential_path = s3_credential_path
|
| 69 |
+
self.context_parallel_size = context_parallel_size
|
| 70 |
+
self.process_group = None
|
| 71 |
+
self.distilled = distilled
|
| 72 |
+
self.num_steps = num_steps
|
| 73 |
+
self._experiment_opts = experiment_opts
|
| 74 |
+
|
| 75 |
+
# Initialize distributed processing if context parallel size > 1
|
| 76 |
+
if self.context_parallel_size > 1:
|
| 77 |
+
self._init_distributed()
|
| 78 |
+
|
| 79 |
+
# Choose the appropriate config file and loader based on whether we're loading a distilled model
|
| 80 |
+
if self.distilled:
|
| 81 |
+
config_file = "cosmos_predict2/_src/interactive/configs/registry_predict2p5.py"
|
| 82 |
+
logger.info(f"Loading distilled model with config: {config_file}")
|
| 83 |
+
# Use the cosmos3 loader for distilled models (DMD2)
|
| 84 |
+
model, config = load_distilled_model_from_checkpoint(
|
| 85 |
+
experiment_name=self.experiment_name,
|
| 86 |
+
s3_checkpoint_dir=self.ckpt_path,
|
| 87 |
+
config_file=config_file,
|
| 88 |
+
load_ema_to_reg=True,
|
| 89 |
+
)
|
| 90 |
+
else:
|
| 91 |
+
config_file = "cosmos_predict2/_src/predict2/action/configs/action_conditioned/config.py"
|
| 92 |
+
# Load the model and config using predict2 loader
|
| 93 |
+
model, config = load_model_from_checkpoint(
|
| 94 |
+
experiment_name=self.experiment_name,
|
| 95 |
+
s3_checkpoint_dir=self.ckpt_path,
|
| 96 |
+
config_file=config_file,
|
| 97 |
+
load_ema_to_reg=True,
|
| 98 |
+
experiment_opts=getattr(self, "_experiment_opts", None),
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
# For distilled models, set net_fake_score to None (not needed for inference)
|
| 102 |
+
if self.distilled and hasattr(model, "net_fake_score"):
|
| 103 |
+
logger.info("Setting net_fake_score to None for distilled model inference")
|
| 104 |
+
model.net_fake_score = None
|
| 105 |
+
|
| 106 |
+
# Enable context parallel on the model if using context parallelism
|
| 107 |
+
if self.context_parallel_size > 1:
|
| 108 |
+
model.net.enable_context_parallel(self.process_group)
|
| 109 |
+
|
| 110 |
+
self.model = model
|
| 111 |
+
self.config = config
|
| 112 |
+
self.batch_size = 1
|
| 113 |
+
self.neg_t5_embeddings = None
|
| 114 |
+
|
| 115 |
+
def _init_distributed(self):
|
| 116 |
+
"""Initialize distributed processing for context parallelism."""
|
| 117 |
+
|
| 118 |
+
# Initialize distributed environment
|
| 119 |
+
distributed.init()
|
| 120 |
+
|
| 121 |
+
# Initialize model parallel states
|
| 122 |
+
parallel_state.initialize_model_parallel(
|
| 123 |
+
context_parallel_size=self.context_parallel_size,
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
# Get the process group for context parallel
|
| 127 |
+
self.process_group = parallel_state.get_context_parallel_group()
|
| 128 |
+
|
| 129 |
+
logger.info(f"Initialized context parallel with size {self.context_parallel_size}")
|
| 130 |
+
logger.info(f"Current rank: {distributed.get_rank()}, World size: {distributed.get_world_size()}")
|
| 131 |
+
|
| 132 |
+
def _get_data_batch_input(
|
| 133 |
+
self,
|
| 134 |
+
video: torch.Tensor,
|
| 135 |
+
prompt: str,
|
| 136 |
+
num_conditional_frames: int = 1,
|
| 137 |
+
negative_prompt: str = _DEFAULT_NEGATIVE_PROMPT,
|
| 138 |
+
use_neg_prompt: bool = True,
|
| 139 |
+
):
|
| 140 |
+
"""
|
| 141 |
+
Prepares the input data batch for the diffusion model.
|
| 142 |
+
|
| 143 |
+
Constructs a dictionary containing the video tensor, text embeddings,
|
| 144 |
+
and other necessary metadata required by the model's forward pass.
|
| 145 |
+
Optionally includes negative text embeddings.
|
| 146 |
+
|
| 147 |
+
Args:
|
| 148 |
+
video (torch.Tensor): The input video tensor (B, C, T, H, W).
|
| 149 |
+
prompt (str): The text prompt for conditioning.
|
| 150 |
+
num_conditional_frames (int): Number of conditional frames to use.
|
| 151 |
+
negative_prompt (str, optional): Custom negative prompt.
|
| 152 |
+
use_neg_prompt (bool, optional): Whether to include negative prompt embeddings. Defaults to True.
|
| 153 |
+
|
| 154 |
+
Returns:
|
| 155 |
+
dict: A dictionary containing the prepared data batch, moved to the correct device and dtype.
|
| 156 |
+
"""
|
| 157 |
+
B, C, T, H, W = video.shape
|
| 158 |
+
|
| 159 |
+
data_batch = {
|
| 160 |
+
"dataset_name": "video_data",
|
| 161 |
+
"video": video,
|
| 162 |
+
"fps": torch.randint(16, 32, (self.batch_size,)).float(), # Random FPS (might be used by model)
|
| 163 |
+
"padding_mask": torch.zeros(self.batch_size, 1, H, W), # Padding mask (assumed no padding here)
|
| 164 |
+
"num_conditional_frames": num_conditional_frames, # Specify number of conditional frames
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
if use_neg_prompt:
|
| 168 |
+
assert negative_prompt is not None, "Negative prompt is required when use_neg_prompt is True"
|
| 169 |
+
|
| 170 |
+
# Compute text embeddings
|
| 171 |
+
if self.model.text_encoder is not None:
|
| 172 |
+
data_batch["ai_caption"] = [prompt]
|
| 173 |
+
data_batch["t5_text_embeddings"] = self.model.text_encoder.compute_text_embeddings_online(
|
| 174 |
+
data_batch={"ai_caption": [prompt], "images": None},
|
| 175 |
+
input_caption_key="ai_caption",
|
| 176 |
+
)
|
| 177 |
+
if use_neg_prompt:
|
| 178 |
+
data_batch["neg_t5_text_embeddings"] = self.model.text_encoder.compute_text_embeddings_online(
|
| 179 |
+
data_batch={"ai_caption": [negative_prompt], "images": None},
|
| 180 |
+
input_caption_key="ai_caption",
|
| 181 |
+
)
|
| 182 |
+
else:
|
| 183 |
+
data_batch["t5_text_embeddings"] = get_text_embedding(prompt)
|
| 184 |
+
if use_neg_prompt:
|
| 185 |
+
data_batch["neg_t5_text_embeddings"] = get_text_embedding(negative_prompt)
|
| 186 |
+
|
| 187 |
+
# Move tensors to GPU and convert to bfloat16 if they are floating point
|
| 188 |
+
for k, v in data_batch.items():
|
| 189 |
+
if isinstance(v, torch.Tensor) and torch.is_floating_point(data_batch[k]):
|
| 190 |
+
data_batch[k] = v.cuda().to(dtype=torch.bfloat16)
|
| 191 |
+
|
| 192 |
+
return data_batch
|
| 193 |
+
|
| 194 |
+
def step_inference_with_latents(
|
| 195 |
+
self,
|
| 196 |
+
img_array: np.ndarray,
|
| 197 |
+
action: np.ndarray = None,
|
| 198 |
+
guidance: int = 3,
|
| 199 |
+
seed: int = 1,
|
| 200 |
+
num_latent_conditional_frames: int = 1,
|
| 201 |
+
query_steps: list[int] = None,
|
| 202 |
+
):
|
| 203 |
+
"""
|
| 204 |
+
Runs a single inference step to generate the next video frame and the full video given an input image and action.
|
| 205 |
+
Returns intermediate latents for analysis.
|
| 206 |
+
|
| 207 |
+
Note: For distilled models, this method falls back to standard inference without latent collection
|
| 208 |
+
as distilled models use a different sampling process.
|
| 209 |
+
"""
|
| 210 |
+
|
| 211 |
+
num_video_frames = action.shape[0] + 1
|
| 212 |
+
|
| 213 |
+
img_tensor = torchvision.transforms.functional.to_tensor(img_array).unsqueeze(0)
|
| 214 |
+
vid_input = torch.cat([img_tensor, torch.zeros_like(img_tensor).repeat(num_video_frames - 1, 1, 1, 1)], dim=0)
|
| 215 |
+
vid_input = (vid_input * 255.0).to(torch.uint8) # Convert to uint8 range if needed (might depend on model)
|
| 216 |
+
vid_input = vid_input.unsqueeze(0).permute(0, 2, 1, 3, 4) # Add batch dim B=1 and permute
|
| 217 |
+
|
| 218 |
+
# Prepare the data batch with text embeddings
|
| 219 |
+
data_batch = self._get_data_batch_input(
|
| 220 |
+
vid_input,
|
| 221 |
+
prompt="",
|
| 222 |
+
num_conditional_frames=num_latent_conditional_frames,
|
| 223 |
+
negative_prompt="",
|
| 224 |
+
use_neg_prompt=False,
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
data_batch["action"] = torch.from_numpy(action).cuda().to(dtype=torch.bfloat16)[None, ...]
|
| 228 |
+
|
| 229 |
+
mem_bytes = torch.cuda.memory_allocated(device=torch.device("cuda" if torch.cuda.is_available() else "cpu"))
|
| 230 |
+
logger.info(f"GPU memory usage after getting data_batch: {mem_bytes / (1024**3):.2f} GB")
|
| 231 |
+
|
| 232 |
+
# Generate latent samples using the diffusion model
|
| 233 |
+
if self.distilled:
|
| 234 |
+
# Distilled model inference: use generate_samples_from_batch (no latent collection)
|
| 235 |
+
logger.info(f"Running distilled inference with {self.num_steps} steps (latent collection not supported)")
|
| 236 |
+
sample = self.model.generate_samples_from_batch(
|
| 237 |
+
data_batch,
|
| 238 |
+
n_sample=1,
|
| 239 |
+
seed=seed,
|
| 240 |
+
num_steps=self.num_steps,
|
| 241 |
+
)
|
| 242 |
+
latents_to_save = None # Distilled models don't support latent collection
|
| 243 |
+
else:
|
| 244 |
+
# Teacher model inference with latent collection
|
| 245 |
+
sample, latents_to_save = self.model.generate_samples_with_latents_from_batch(
|
| 246 |
+
data_batch,
|
| 247 |
+
n_sample=1, # Generate one sample
|
| 248 |
+
guidance=guidance,
|
| 249 |
+
seed=seed, # Fixed seed for reproducibility
|
| 250 |
+
is_negative_prompt=True, # Use classifier-free guidance
|
| 251 |
+
query_steps=query_steps,
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
# Decode the latent sample into a video tensor
|
| 255 |
+
video = self.model.decode(sample)
|
| 256 |
+
|
| 257 |
+
video_normalized = (video - (-1)) / (1 - (-1))
|
| 258 |
+
video_clamped = (torch.clamp(video_normalized[0], 0, 1) * 255).to(torch.uint8).permute(1, 2, 3, 0).cpu().numpy()
|
| 259 |
+
next_frame = torch.clamp(video_normalized[0, :, -1, :, :], 0, 1)
|
| 260 |
+
next_frame = (next_frame * 255).to(torch.uint8).permute(1, 2, 0).cpu().numpy()
|
| 261 |
+
return next_frame, video_clamped, latents_to_save
|
| 262 |
+
|
| 263 |
+
def step_inference(
|
| 264 |
+
self,
|
| 265 |
+
img_array: np.ndarray,
|
| 266 |
+
action: np.ndarray = None,
|
| 267 |
+
guidance: int = 3,
|
| 268 |
+
seed: int = 1,
|
| 269 |
+
num_latent_conditional_frames: int = 1,
|
| 270 |
+
):
|
| 271 |
+
"""
|
| 272 |
+
Runs a single inference step to generate the next video frame and the full video given an input image and action.
|
| 273 |
+
|
| 274 |
+
Args:
|
| 275 |
+
img_array (np.ndarray): Input image as a numpy array (H, W, C), typically the first frame.
|
| 276 |
+
action (np.ndarray, optional): Action vector to condition the model. Should be shape (action_dim,) or (chunk_size, action_dim).
|
| 277 |
+
guidance (int, optional): Guidance scale for classifier-free guidance. Default is 3.
|
| 278 |
+
seed (int, optional): Random seed for reproducibility. Default is 1.
|
| 279 |
+
num_latent_conditional_frames (int, optional): Number of conditional frames to use for the model. Default is 1.
|
| 280 |
+
|
| 281 |
+
Returns:
|
| 282 |
+
next_frame (np.ndarray): The next predicted frame as a numpy array (H, W, C), uint8.
|
| 283 |
+
video_clamped (np.ndarray): The generated video as a numpy array (T, H, W, C), uint8.
|
| 284 |
+
"""
|
| 285 |
+
num_video_frames = action.shape[0] + 1
|
| 286 |
+
|
| 287 |
+
img_tensor = torchvision.transforms.functional.to_tensor(img_array).unsqueeze(0) # (1, H, W, C)
|
| 288 |
+
vid_input = torch.cat([img_tensor, torch.zeros_like(img_tensor).repeat(num_video_frames - 1, 1, 1, 1)], dim=0)
|
| 289 |
+
vid_input = (vid_input * 255.0).to(torch.uint8) # Convert to uint8 range if needed (might depend on model)
|
| 290 |
+
vid_input = vid_input.unsqueeze(0).permute(0, 2, 1, 3, 4) # Add batch dim B=1 and permute
|
| 291 |
+
|
| 292 |
+
# Prepare the data batch with text embeddings
|
| 293 |
+
data_batch = self._get_data_batch_input(
|
| 294 |
+
vid_input,
|
| 295 |
+
prompt="",
|
| 296 |
+
num_conditional_frames=num_latent_conditional_frames,
|
| 297 |
+
negative_prompt="",
|
| 298 |
+
use_neg_prompt=False,
|
| 299 |
+
)
|
| 300 |
+
|
| 301 |
+
data_batch["action"] = torch.from_numpy(action).cuda().to(dtype=torch.bfloat16)[None, ...]
|
| 302 |
+
|
| 303 |
+
mem_bytes = torch.cuda.memory_allocated(device=torch.device("cuda" if torch.cuda.is_available() else "cpu"))
|
| 304 |
+
logger.info(f"GPU memory usage after getting data_batch: {mem_bytes / (1024**3):.2f} GB")
|
| 305 |
+
|
| 306 |
+
# Generate latent samples using the diffusion model
|
| 307 |
+
# For distilled models, use num_steps; for teacher models, use guidance with more steps
|
| 308 |
+
if self.distilled:
|
| 309 |
+
# Distilled model inference: use fewer steps, no guidance needed
|
| 310 |
+
logger.info(f"Running distilled inference with {self.num_steps} steps")
|
| 311 |
+
sample = self.model.generate_samples_from_batch(
|
| 312 |
+
data_batch,
|
| 313 |
+
n_sample=1,
|
| 314 |
+
seed=seed,
|
| 315 |
+
num_steps=self.num_steps,
|
| 316 |
+
)
|
| 317 |
+
else:
|
| 318 |
+
# Teacher model inference: use guidance with standard sampling
|
| 319 |
+
sample = self.model.generate_samples_from_batch(
|
| 320 |
+
data_batch,
|
| 321 |
+
n_sample=1, # Generate one sample
|
| 322 |
+
guidance=guidance,
|
| 323 |
+
seed=seed, # Fixed seed for reproducibility
|
| 324 |
+
is_negative_prompt=True, # Use classifier-free guidance
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
# Decode the latent sample into a video tensor
|
| 328 |
+
video = self.model.decode(sample)
|
| 329 |
+
|
| 330 |
+
video_normalized = (video - (-1)) / (1 - (-1))
|
| 331 |
+
video_clamped = (torch.clamp(video_normalized[0], 0, 1) * 255).to(torch.uint8).permute(1, 2, 3, 0).cpu().numpy()
|
| 332 |
+
next_frame = torch.clamp(video_normalized[0, :, -1, :, :], 0, 1)
|
| 333 |
+
next_frame = (next_frame * 255).to(torch.uint8).permute(1, 2, 0).cpu().numpy()
|
| 334 |
+
return next_frame, video_clamped
|
| 335 |
+
|
| 336 |
+
def step_inference_multi_frame(
|
| 337 |
+
self,
|
| 338 |
+
video_array: np.ndarray,
|
| 339 |
+
action: np.ndarray = None,
|
| 340 |
+
guidance: int = 3,
|
| 341 |
+
seed: int = 1,
|
| 342 |
+
num_latent_conditional_frames: int = 2,
|
| 343 |
+
):
|
| 344 |
+
"""
|
| 345 |
+
Runs a single inference step to generate the next video frame and the full video given an input image and action.
|
| 346 |
+
|
| 347 |
+
Args:
|
| 348 |
+
video_array (np.ndarray): Input video as a numpy array (T, H, W, C).
|
| 349 |
+
action (np.ndarray, optional): Action vector to condition the model. Should be shape (action_dim,) or (chunk_size, action_dim).
|
| 350 |
+
|
| 351 |
+
guidance (int, optional): Guidance scale for classifier-free guidance. Default is 3.
|
| 352 |
+
seed (int, optional): Random seed for reproducibility. Default is 1.
|
| 353 |
+
num_latent_conditional_frames (int, optional): Number of conditional frames to use for the model. Default is 1.
|
| 354 |
+
|
| 355 |
+
Returns:
|
| 356 |
+
next_frame (np.ndarray): The next predicted frame as a numpy array (H, W, C), uint8.
|
| 357 |
+
video_clamped (np.ndarray): The generated video as a numpy array (T, H, W, C), uint8.
|
| 358 |
+
"""
|
| 359 |
+
num_video_frames = action.shape[0] + 1 + (num_latent_conditional_frames - 1) * 4
|
| 360 |
+
num_cond_image_frames = (num_latent_conditional_frames - 1) * 4 + 1
|
| 361 |
+
|
| 362 |
+
assert num_cond_image_frames == video_array.shape[0], (
|
| 363 |
+
"Number of conditional frames is not equal to the number of frames in the video"
|
| 364 |
+
)
|
| 365 |
+
assert action.shape[0] == num_video_frames - num_cond_image_frames, (
|
| 366 |
+
"Number of action frames is not equal to the number of frames in the video"
|
| 367 |
+
)
|
| 368 |
+
|
| 369 |
+
video_tensor = torch.stack(
|
| 370 |
+
[torchvision.transforms.functional.to_tensor(v) for v in video_array]
|
| 371 |
+
) # (T, C, H, W)
|
| 372 |
+
vid_input = torch.cat(
|
| 373 |
+
[
|
| 374 |
+
video_tensor,
|
| 375 |
+
torch.zeros_like(video_tensor[0][None, ...]).repeat(num_video_frames - num_cond_image_frames, 1, 1, 1),
|
| 376 |
+
],
|
| 377 |
+
dim=0,
|
| 378 |
+
)
|
| 379 |
+
vid_input = (vid_input * 255.0).to(torch.uint8) # Convert to uint8 range if needed (might depend on model)
|
| 380 |
+
vid_input = vid_input.unsqueeze(0).permute(0, 2, 1, 3, 4) # Add batch dim B=1 and permute
|
| 381 |
+
|
| 382 |
+
# Prepare the data batch with text embeddings
|
| 383 |
+
data_batch = self._get_data_batch_input(
|
| 384 |
+
vid_input,
|
| 385 |
+
prompt="",
|
| 386 |
+
num_conditional_frames=num_latent_conditional_frames,
|
| 387 |
+
negative_prompt="",
|
| 388 |
+
use_neg_prompt=False,
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
zero_action = np.zeros(
|
| 392 |
+
(4 * (num_latent_conditional_frames - 1), action.shape[1])
|
| 393 |
+
) # (4 * (num_latent_conditional_frames-1), action_dim)
|
| 394 |
+
action_padded = np.concatenate([zero_action, action], axis=0)
|
| 395 |
+
|
| 396 |
+
data_batch["action"] = torch.from_numpy(action_padded).cuda().to(dtype=torch.bfloat16)[None, ...]
|
| 397 |
+
|
| 398 |
+
mem_bytes = torch.cuda.memory_allocated(device=torch.device("cuda" if torch.cuda.is_available() else "cpu"))
|
| 399 |
+
logger.info(f"GPU memory usage after getting data_batch: {mem_bytes / (1024**3):.2f} GB")
|
| 400 |
+
|
| 401 |
+
# Generate latent samples using the diffusion model
|
| 402 |
+
# For distilled models, use num_steps; for teacher models, use guidance with more steps
|
| 403 |
+
if self.distilled:
|
| 404 |
+
# Distilled model inference: use fewer steps, no guidance needed
|
| 405 |
+
logger.info(f"Running distilled inference with {self.num_steps} steps")
|
| 406 |
+
sample = self.model.generate_samples_from_batch(
|
| 407 |
+
data_batch,
|
| 408 |
+
n_sample=1,
|
| 409 |
+
seed=seed,
|
| 410 |
+
num_steps=self.num_steps,
|
| 411 |
+
)
|
| 412 |
+
else:
|
| 413 |
+
# Teacher model inference: use guidance with standard sampling
|
| 414 |
+
sample = self.model.generate_samples_from_batch(
|
| 415 |
+
data_batch,
|
| 416 |
+
n_sample=1, # Generate one sample
|
| 417 |
+
guidance=guidance,
|
| 418 |
+
seed=seed, # Fixed seed for reproducibility
|
| 419 |
+
is_negative_prompt=True, # Use classifier-free guidance
|
| 420 |
+
)
|
| 421 |
+
|
| 422 |
+
# Decode the latent sample into a video tensor
|
| 423 |
+
video = self.model.decode(sample)
|
| 424 |
+
|
| 425 |
+
video_normalized = (video - (-1)) / (1 - (-1))
|
| 426 |
+
video_clamped = (torch.clamp(video_normalized[0], 0, 1) * 255).to(torch.uint8).permute(1, 2, 3, 0).cpu().numpy()
|
| 427 |
+
next_frame = torch.clamp(video_normalized[0, :, -1, :, :], 0, 1)
|
| 428 |
+
next_frame = (next_frame * 255).to(torch.uint8).permute(1, 2, 0).cpu().numpy()
|
| 429 |
+
return next_frame, video_clamped
|
| 430 |
+
|
| 431 |
+
def cleanup(self):
|
| 432 |
+
"""Clean up distributed resources."""
|
| 433 |
+
if self.context_parallel_size > 1:
|
| 434 |
+
if parallel_state.is_initialized():
|
| 435 |
+
parallel_state.destroy_model_parallel()
|
| 436 |
+
dist.destroy_process_group()
|
model_ema_bf16.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:94d4a1491a58ae392e4ef7059589f1295503063c9f5407091e2e66544f598e0a
|
| 3 |
+
size 4256645510
|
scripts/convert_distcp_to_pt.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env -S uv run --script
|
| 2 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 3 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
#
|
| 5 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 6 |
+
# you may not use this file except in compliance with the License.
|
| 7 |
+
# You may obtain a copy of the License at
|
| 8 |
+
#
|
| 9 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 10 |
+
#
|
| 11 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 12 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 13 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 14 |
+
# See the License for the specific language governing permissions and
|
| 15 |
+
# limitations under the License.
|
| 16 |
+
|
| 17 |
+
# /// script
|
| 18 |
+
# requires-python = ">=3.10"
|
| 19 |
+
# dependencies = [
|
| 20 |
+
# "numpy",
|
| 21 |
+
# "s5cmd",
|
| 22 |
+
# "torch",
|
| 23 |
+
# "tyro",
|
| 24 |
+
# ]
|
| 25 |
+
# [tool.uv.sources]
|
| 26 |
+
# torch = [{ index = "pytorch" }]
|
| 27 |
+
# [[tool.uv.index]]
|
| 28 |
+
# name = "pytorch"
|
| 29 |
+
# url = "https://download.pytorch.org/whl/cpu"
|
| 30 |
+
# explicit = true
|
| 31 |
+
# ///
|
| 32 |
+
|
| 33 |
+
"""Download distributed checkpoint from S3 and convert to pytorch checkpoint.
|
| 34 |
+
|
| 35 |
+
Usage:
|
| 36 |
+
|
| 37 |
+
```python
|
| 38 |
+
./scripts/convert_distcp_to_pt.py "s3://bucket/cosmos_predict2_multiview/cosmos2_mv/buttercup_predict2p5_2b_mv_7views_res720p_fps30_t8_from16kfps10mv_jointalpamayov2mads720pmulticaps29frames-0/checkpoints/iter_000028000" "checkpoints/buttercup_predict2p5_2b_mv_7views_res720p_fps30_t8_from16kfps10mv_jointalpamayov2mads720pmulticaps29frames-0_iter_000028000"
|
| 39 |
+
```
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
import shlex
|
| 43 |
+
import subprocess
|
| 44 |
+
from dataclasses import dataclass
|
| 45 |
+
from pathlib import Path
|
| 46 |
+
from typing import Any
|
| 47 |
+
|
| 48 |
+
import torch
|
| 49 |
+
import tyro
|
| 50 |
+
from torch.distributed.checkpoint.format_utils import dcp_to_torch_save
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@dataclass(frozen=True)
|
| 54 |
+
class Args:
|
| 55 |
+
input_dir: tyro.conf.Positional[str]
|
| 56 |
+
"""S3 URI of the checkpoint or path to the distcp directory."""
|
| 57 |
+
output_dir: tyro.conf.Positional[Path]
|
| 58 |
+
"""Output directory to save the converted checkpoints."""
|
| 59 |
+
|
| 60 |
+
ema: bool = True
|
| 61 |
+
"""Export EMA weights."""
|
| 62 |
+
|
| 63 |
+
s3_args: str | None = None
|
| 64 |
+
"""Additional arguments to pass to s5cmd."""
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def main():
|
| 68 |
+
args = tyro.cli(Args, description=__doc__)
|
| 69 |
+
|
| 70 |
+
pt_path = args.output_dir / "model.pt"
|
| 71 |
+
pt_path.unlink(missing_ok=True)
|
| 72 |
+
pt_ema_fp32_path = args.output_dir / "model_ema_fp32.pt"
|
| 73 |
+
pt_ema_fp32_path.unlink(missing_ok=True)
|
| 74 |
+
pt_ema_bf16_path = args.output_dir / "model_ema_bf16.pt"
|
| 75 |
+
pt_ema_bf16_path.unlink(missing_ok=True)
|
| 76 |
+
|
| 77 |
+
if args.input_dir.startswith("s3://"):
|
| 78 |
+
input_s3 = args.input_dir.rstrip("/")
|
| 79 |
+
input_s3 = input_s3.removesuffix("/model")
|
| 80 |
+
distcp_dir = args.output_dir / "model"
|
| 81 |
+
print(f"Downloading distcp to {distcp_dir}...")
|
| 82 |
+
# Create the directory if it doesn't exist
|
| 83 |
+
distcp_dir.mkdir(parents=True, exist_ok=True)
|
| 84 |
+
# Use sync only if directory exists and has files, otherwise use cp
|
| 85 |
+
cmd = ["s5cmd"]
|
| 86 |
+
if args.s3_args:
|
| 87 |
+
cmd.extend(shlex.split(args.s3_args))
|
| 88 |
+
if distcp_dir.exists() and any(distcp_dir.iterdir()):
|
| 89 |
+
cmd.extend(["sync", "--exit-on-error"])
|
| 90 |
+
else:
|
| 91 |
+
cmd.extend(["cp", "--show-progress"])
|
| 92 |
+
cmd.extend(
|
| 93 |
+
[
|
| 94 |
+
f"{input_s3}/model/*",
|
| 95 |
+
f"{distcp_dir}",
|
| 96 |
+
]
|
| 97 |
+
)
|
| 98 |
+
print(shlex.join(cmd))
|
| 99 |
+
subprocess.run(cmd, check=True)
|
| 100 |
+
print(f"Downloaded distcp to '{distcp_dir}'")
|
| 101 |
+
else:
|
| 102 |
+
distcp_dir = Path(args.input_dir)
|
| 103 |
+
|
| 104 |
+
# Convert distributed checkpoint to torch single checkpoint
|
| 105 |
+
dcp_to_torch_save(distcp_dir, pt_path)
|
| 106 |
+
print(f"Converted '{distcp_dir}' to '{pt_path}'")
|
| 107 |
+
|
| 108 |
+
if not args.ema:
|
| 109 |
+
return
|
| 110 |
+
|
| 111 |
+
# Drop Reg keys and save EMA weights only in fp32 precision
|
| 112 |
+
state_dict: dict[str, Any] = torch.load(pt_path, map_location="cpu", weights_only=False)
|
| 113 |
+
state_dict_ema_fp32: dict[str, Any] = {}
|
| 114 |
+
for key, value in state_dict.items():
|
| 115 |
+
if key.startswith("net_ema."):
|
| 116 |
+
key = key.replace("net_ema.", "net.")
|
| 117 |
+
state_dict_ema_fp32[key] = value
|
| 118 |
+
if not state_dict_ema_fp32:
|
| 119 |
+
raise ValueError("Model doesn't contain EMA weights")
|
| 120 |
+
torch.save(state_dict_ema_fp32, pt_ema_fp32_path)
|
| 121 |
+
print(f"Saved EMA fp32 weights from '{pt_path}' to '{pt_ema_fp32_path}'")
|
| 122 |
+
|
| 123 |
+
# Save EMA weights only in bf16 precision
|
| 124 |
+
state_dict_ema_bf16: dict[str, Any] = {}
|
| 125 |
+
for key, value in state_dict_ema_fp32.items():
|
| 126 |
+
if isinstance(value, torch.Tensor) and value.dtype == torch.float32:
|
| 127 |
+
value = value.bfloat16()
|
| 128 |
+
state_dict_ema_bf16[key] = value
|
| 129 |
+
torch.save(state_dict_ema_bf16, pt_ema_bf16_path)
|
| 130 |
+
print(f"fp32 -> bf16: '{pt_ema_fp32_path}' to '{pt_ema_bf16_path}'")
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
if __name__ == "__main__":
|
| 134 |
+
main()
|
scripts/eval_gr1_robot.sh
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# =============================================================================
|
| 3 |
+
# Cosmos-Predict2.5 Action-Conditioned Evaluation on GR1 Robot Data
|
| 4 |
+
# =============================================================================
|
| 5 |
+
#
|
| 6 |
+
# 一键评估:指定模型路径(DCP 目录或 _ema_bf16.pt),自动完成转换 + 推理。
|
| 7 |
+
#
|
| 8 |
+
# 使用方式 (在 GPU 机器上):
|
| 9 |
+
# bash scripts/eval_gr1_robot.sh /path/to/checkpoint
|
| 10 |
+
# bash scripts/eval_gr1_robot.sh /path/to/model_ema_bf16.pt
|
| 11 |
+
# bash scripts/eval_gr1_robot.sh /path/to/iter_000002000 # DCP 目录
|
| 12 |
+
# bash scripts/eval_gr1_robot.sh /path/to/iter_000002000 --debug
|
| 13 |
+
# bash scripts/eval_gr1_robot.sh /path/to/model_ema_bf16.pt --num-episodes 50
|
| 14 |
+
#
|
| 15 |
+
# 选项:
|
| 16 |
+
# --num-episodes N 评估 episode 数量 (default: 100)
|
| 17 |
+
# --output-dir PATH 输出目录
|
| 18 |
+
# --guidance N CFG guidance 值 (default: 0)
|
| 19 |
+
# --num-gpus N 使用 GPU 数量 (default: 1)
|
| 20 |
+
# --experiment NAME Hydra 实验名 (default: 自动推断)
|
| 21 |
+
# --debug 调试模式 (5 episodes, 1 GPU)
|
| 22 |
+
# -h, --help 显示帮助
|
| 23 |
+
# =============================================================================
|
| 24 |
+
|
| 25 |
+
set -euo pipefail
|
| 26 |
+
|
| 27 |
+
# ---------------------------------------------------------------------------
|
| 28 |
+
# 默认配置
|
| 29 |
+
# ---------------------------------------------------------------------------
|
| 30 |
+
NUM_EPISODES=100
|
| 31 |
+
GUIDANCE=0
|
| 32 |
+
OUTPUT_ROOT="/inspire/ssd/project/security-defense-and-attack/25015/world_model/output"
|
| 33 |
+
DATASET_PATH="/inspire/ssd/project/security-defense-and-attack/25015/world_model/data/PhysicalAI-Robotics-GR00T-Teleop-GR1/GR1_robot"
|
| 34 |
+
COSMOS_ROOT="/inspire/ssd/project/security-defense-and-attack/25015/cosmos-predict2.5"
|
| 35 |
+
NUM_GPUS=1
|
| 36 |
+
DEBUG=0
|
| 37 |
+
EXPERIMENT_NAME="cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes_release_oss"
|
| 38 |
+
CHECKPOINT_INPUT=""
|
| 39 |
+
|
| 40 |
+
# ---------------------------------------------------------------------------
|
| 41 |
+
# 解析参数 (第一个位置参数为 checkpoint 路径)
|
| 42 |
+
# ---------------------------------------------------------------------------
|
| 43 |
+
if [ $# -eq 0 ]; then
|
| 44 |
+
echo "用法: bash scripts/eval_gr1_robot.sh <checkpoint_path> [选项]"
|
| 45 |
+
echo ""
|
| 46 |
+
echo "checkpoint_path 可以是:"
|
| 47 |
+
echo " - DCP 目录 (如 .../checkpoints/iter_000002000)"
|
| 48 |
+
echo " - _ema_bf16.pt 文件 (如 .../iter_000002000/model_ema_bf16.pt)"
|
| 49 |
+
echo ""
|
| 50 |
+
sed -n '3,18p' "$0"
|
| 51 |
+
exit 1
|
| 52 |
+
fi
|
| 53 |
+
|
| 54 |
+
CHECKPOINT_INPUT="$1"
|
| 55 |
+
shift
|
| 56 |
+
|
| 57 |
+
while [[ $# -gt 0 ]]; do
|
| 58 |
+
case $1 in
|
| 59 |
+
--num-episodes) NUM_EPISODES="$2"; shift 2 ;;
|
| 60 |
+
--output-dir) OUTPUT_ROOT="$2"; shift 2 ;;
|
| 61 |
+
--guidance) GUIDANCE="$2"; shift 2 ;;
|
| 62 |
+
--num-gpus) NUM_GPUS="$2"; shift 2 ;;
|
| 63 |
+
--experiment) EXPERIMENT_NAME="$2"; shift 2 ;;
|
| 64 |
+
--debug) DEBUG=1; shift ;;
|
| 65 |
+
-h|--help)
|
| 66 |
+
sed -n '3,18p' "$0"
|
| 67 |
+
exit 0
|
| 68 |
+
;;
|
| 69 |
+
*) echo "Unknown option: $1"; exit 1 ;;
|
| 70 |
+
esac
|
| 71 |
+
done
|
| 72 |
+
|
| 73 |
+
# 调试模式覆盖
|
| 74 |
+
if [ "$DEBUG" -eq 1 ]; then
|
| 75 |
+
NUM_EPISODES=5
|
| 76 |
+
NUM_GPUS=1
|
| 77 |
+
echo "=== DEBUG MODE ==="
|
| 78 |
+
fi
|
| 79 |
+
|
| 80 |
+
# ---------------------------------------------------------------------------
|
| 81 |
+
# Step 1: 确定 _ema_bf16.pt 路径 (自动转换 DCP → PT)
|
| 82 |
+
# ---------------------------------------------------------------------------
|
| 83 |
+
if [ ! -e "$CHECKPOINT_INPUT" ]; then
|
| 84 |
+
echo "ERROR: 路径不存在: ${CHECKPOINT_INPUT}"
|
| 85 |
+
exit 1
|
| 86 |
+
fi
|
| 87 |
+
|
| 88 |
+
if [ -f "$CHECKPOINT_INPUT" ]; then
|
| 89 |
+
# 已经是 .pt 文件,直接使用
|
| 90 |
+
CHECKPOINT_PATH="$CHECKPOINT_INPUT"
|
| 91 |
+
echo "使用已有 PT checkpoint: ${CHECKPOINT_PATH}"
|
| 92 |
+
else
|
| 93 |
+
# 是目录,需要找到或转换 _ema_bf16.pt
|
| 94 |
+
CKPT_DIR="$CHECKPOINT_INPUT"
|
| 95 |
+
|
| 96 |
+
# 检查是否已有 _ema_bf16.pt
|
| 97 |
+
if [ -f "${CKPT_DIR}/model_ema_bf16.pt" ]; then
|
| 98 |
+
CHECKPOINT_PATH="${CKPT_DIR}/model_ema_bf16.pt"
|
| 99 |
+
echo "找到已有 PT checkpoint: ${CHECKPOINT_PATH}"
|
| 100 |
+
elif [ -d "${CKPT_DIR}/model" ]; then
|
| 101 |
+
# DCP 格式,需要转换
|
| 102 |
+
echo "DCP checkpoint,开始转换..."
|
| 103 |
+
echo " 输入: ${CKPT_DIR}/model/"
|
| 104 |
+
echo " 输出: ${CKPT_DIR}/model_ema_bf16.pt"
|
| 105 |
+
echo ""
|
| 106 |
+
|
| 107 |
+
cd "${COSMOS_ROOT}"
|
| 108 |
+
python scripts/convert_distcp_to_pt.py \
|
| 109 |
+
"${CKPT_DIR}/model" \
|
| 110 |
+
"${CKPT_DIR}"
|
| 111 |
+
|
| 112 |
+
if [ -f "${CKPT_DIR}/model_ema_bf16.pt" ]; then
|
| 113 |
+
CHECKPOINT_PATH="${CKPT_DIR}/model_ema_bf16.pt"
|
| 114 |
+
echo ""
|
| 115 |
+
echo "转换成功: ${CHECKPOINT_PATH}"
|
| 116 |
+
else
|
| 117 |
+
echo "ERROR: DCP → PT 转换失败"
|
| 118 |
+
exit 1
|
| 119 |
+
fi
|
| 120 |
+
else
|
| 121 |
+
echo "ERROR: 目录中既没有 model_ema_bf16.pt 也没有 model/ 子目录"
|
| 122 |
+
echo " 内容: $(ls "$CKPT_DIR")"
|
| 123 |
+
exit 1
|
| 124 |
+
fi
|
| 125 |
+
fi
|
| 126 |
+
|
| 127 |
+
# ---------------------------------------------------------------------------
|
| 128 |
+
# Step 2: 准备评估数据
|
| 129 |
+
# ---------------------------------------------------------------------------
|
| 130 |
+
EVAL_DATA_DIR="${OUTPUT_ROOT}/eval/gr1_eval_data"
|
| 131 |
+
|
| 132 |
+
if [ ! -d "${EVAL_DATA_DIR}" ] || [ -z "$(ls -A "${EVAL_DATA_DIR}" 2>/dev/null)" ]; then
|
| 133 |
+
echo ""
|
| 134 |
+
echo "准备评估数据..."
|
| 135 |
+
cd "${COSMOS_ROOT}"
|
| 136 |
+
python scripts/prepare_gr1_eval_data.py \
|
| 137 |
+
--dataset-path "${DATASET_PATH}" \
|
| 138 |
+
--output-dir "${EVAL_DATA_DIR}" \
|
| 139 |
+
--num-episodes "${NUM_EPISODES}"
|
| 140 |
+
else
|
| 141 |
+
echo "评估数据已存在: ${EVAL_DATA_DIR}"
|
| 142 |
+
fi
|
| 143 |
+
|
| 144 |
+
# ---------------------------------------------------------------------------
|
| 145 |
+
# Step 3: 设置环境变量
|
| 146 |
+
# ---------------------------------------------------------------------------
|
| 147 |
+
export IMAGINAIRE_OUTPUT_ROOT="${OUTPUT_ROOT}"
|
| 148 |
+
export HF_HOME="${HF_HOME:-${OUTPUT_ROOT}/hf_cache}"
|
| 149 |
+
|
| 150 |
+
# 设置 HF 离线模式,防止尝试下载
|
| 151 |
+
export HF_HUB_OFFLINE=1
|
| 152 |
+
export TRANSFORMERS_OFFLINE=1
|
| 153 |
+
|
| 154 |
+
# ---------------------------------------------------------------------------
|
| 155 |
+
# Step 4: 运行推理
|
| 156 |
+
# ---------------------------------------------------------------------------
|
| 157 |
+
cd "${COSMOS_ROOT}"
|
| 158 |
+
|
| 159 |
+
PREDICT_DIR="${OUTPUT_ROOT}/eval/gr1_predicted"
|
| 160 |
+
mkdir -p "${PREDICT_DIR}"
|
| 161 |
+
|
| 162 |
+
# 日志文件
|
| 163 |
+
LOG_DIR="${OUTPUT_ROOT}/logs"
|
| 164 |
+
mkdir -p "${LOG_DIR}"
|
| 165 |
+
LOG_FILE="${LOG_DIR}/eval_$(date +%Y%m%d_%H%M%S).log"
|
| 166 |
+
|
| 167 |
+
echo ""
|
| 168 |
+
echo "==================================================================="
|
| 169 |
+
echo "Cosmos-Predict2.5 Action-Conditioned Evaluation (GR1 Robot)"
|
| 170 |
+
echo "==================================================================="
|
| 171 |
+
echo " Checkpoint: ${CHECKPOINT_PATH}"
|
| 172 |
+
echo " Experiment: ${EXPERIMENT_NAME}"
|
| 173 |
+
echo " 评估数据: ${EVAL_DATA_DIR}"
|
| 174 |
+
echo " 预测输出: ${PREDICT_DIR}"
|
| 175 |
+
echo " GPU 数量: ${NUM_GPUS}"
|
| 176 |
+
echo " Episode 数: ${NUM_EPISODES}"
|
| 177 |
+
echo " Guidance: ${GUIDANCE}"
|
| 178 |
+
echo " 日志: ${LOG_FILE}"
|
| 179 |
+
echo "==================================================================="
|
| 180 |
+
|
| 181 |
+
# 统计输入文件数
|
| 182 |
+
NUM_MP4=$(ls "${EVAL_DATA_DIR}"/*.mp4 2>/dev/null | wc -l)
|
| 183 |
+
echo " 输入视频数: ${NUM_MP4}"
|
| 184 |
+
echo ""
|
| 185 |
+
|
| 186 |
+
# 设置 CUDA 可见设备
|
| 187 |
+
if [ "$NUM_GPUS" -eq 1 ]; then
|
| 188 |
+
CUDA_DEVICES="0"
|
| 189 |
+
else
|
| 190 |
+
CUDA_DEVICES=$(seq -s, 0 $((NUM_GPUS - 1)))
|
| 191 |
+
fi
|
| 192 |
+
|
| 193 |
+
# ---------------------------------------------------------------------------
|
| 194 |
+
# 本地模型路径 (与训练脚本一致,避免从 HF/S3 下载)
|
| 195 |
+
# ---------------------------------------------------------------------------
|
| 196 |
+
VAE_PATH="/inspire/ssd/project/security-defense-and-attack/25015/world_model/Wan2.1-VAE/Wan2.1_VAE.pth"
|
| 197 |
+
REASON1_PATH="/inspire/ssd/project/security-defense-and-attack/25015/world_model/Cosmos-Reason1-7B"
|
| 198 |
+
QWEN_PATH="/inspire/ssd/project/security-defense-and-attack/25015/models/Qwen2.5-VL-7B-Instruct"
|
| 199 |
+
|
| 200 |
+
# 构建推理命令的本地路径参数
|
| 201 |
+
LOCAL_PATH_ARGS=""
|
| 202 |
+
if [ -f "$VAE_PATH" ]; then
|
| 203 |
+
LOCAL_PATH_ARGS="${LOCAL_PATH_ARGS} --vae_path ${VAE_PATH}"
|
| 204 |
+
echo " 使用本地 VAE: ${VAE_PATH}"
|
| 205 |
+
else
|
| 206 |
+
echo " WARNING: 本地 VAE 不存在: ${VAE_PATH}"
|
| 207 |
+
fi
|
| 208 |
+
if [ -d "$REASON1_PATH" ]; then
|
| 209 |
+
LOCAL_PATH_ARGS="${LOCAL_PATH_ARGS} --text_encoder_path ${REASON1_PATH}"
|
| 210 |
+
echo " 使用本地 Cosmos-Reason1-7B: ${REASON1_PATH}"
|
| 211 |
+
else
|
| 212 |
+
echo " WARNING: 本地 Cosmos-Reason1-7B 不存在: ${REASON1_PATH}"
|
| 213 |
+
fi
|
| 214 |
+
if [ -d "$QWEN_PATH" ]; then
|
| 215 |
+
LOCAL_PATH_ARGS="${LOCAL_PATH_ARGS} --qwen_path ${QWEN_PATH}"
|
| 216 |
+
echo " 使用本地 Qwen2.5-VL-7B-Instruct: ${QWEN_PATH}"
|
| 217 |
+
else
|
| 218 |
+
echo " WARNING: 本地 Qwen2.5-VL-7B-Instruct 不存在: ${QWEN_PATH}"
|
| 219 |
+
fi
|
| 220 |
+
|
| 221 |
+
echo ""
|
| 222 |
+
echo "启动推理..."
|
| 223 |
+
|
| 224 |
+
CUDA_VISIBLE_DEVICES=${CUDA_DEVICES} PYTHONPATH=. python \
|
| 225 |
+
cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
|
| 226 |
+
--experiment="${EXPERIMENT_NAME}" \
|
| 227 |
+
--ckpt_path="${CHECKPOINT_PATH}" \
|
| 228 |
+
--input_video_root="${EVAL_DATA_DIR}" \
|
| 229 |
+
--save_root="${PREDICT_DIR}" \
|
| 230 |
+
--resolution 480,832 \
|
| 231 |
+
--guidance ${GUIDANCE} \
|
| 232 |
+
--chunk_size 12 \
|
| 233 |
+
--start 0 \
|
| 234 |
+
--end ${NUM_EPISODES} \
|
| 235 |
+
--fps_downsample_ratio 2 \
|
| 236 |
+
--save_fps 10 \
|
| 237 |
+
${LOCAL_PATH_ARGS} \
|
| 238 |
+
2>&1 | tee "${LOG_FILE}"
|
| 239 |
+
|
| 240 |
+
echo ""
|
| 241 |
+
echo "==================================================================="
|
| 242 |
+
echo "评估完成!"
|
| 243 |
+
echo " 预测视频: ${PREDICT_DIR}"
|
| 244 |
+
echo " 日志: ${LOG_FILE}"
|
| 245 |
+
echo "==================================================================="
|
scripts/prepare_gr1_eval_data.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Prepare GR1 robot evaluation data for Cosmos-Predict2.5 inference.
|
| 4 |
+
|
| 5 |
+
Extracts episodes from the LeRobot dataset and creates paired
|
| 6 |
+
episode_XXXXXX.mp4 + episode_XXXXXX_actions.npy
|
| 7 |
+
files in the format expected by inference_gr00t.py.
|
| 8 |
+
|
| 9 |
+
Usage:
|
| 10 |
+
python scripts/prepare_gr1_eval_data.py
|
| 11 |
+
python scripts/prepare_gr1_eval_data.py --num-episodes 50 --start-episode 100
|
| 12 |
+
python scripts/prepare_gr1_eval_data.py --output-dir /path/to/output
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import argparse
|
| 16 |
+
import json
|
| 17 |
+
import os
|
| 18 |
+
|
| 19 |
+
import numpy as np
|
| 20 |
+
import pyarrow.parquet as pq
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def main():
|
| 24 |
+
parser = argparse.ArgumentParser(description="Prepare GR1 eval data for inference")
|
| 25 |
+
parser.add_argument(
|
| 26 |
+
"--dataset-path",
|
| 27 |
+
default="/inspire/ssd/project/security-defense-and-attack/25015/world_model/data/PhysicalAI-Robotics-GR00T-Teleop-GR1/GR1_robot",
|
| 28 |
+
)
|
| 29 |
+
parser.add_argument("--output-dir", default=None, help="Output directory (default: {dataset_path}/../eval/gr1_eval_data)")
|
| 30 |
+
parser.add_argument("--num-episodes", type=int, default=100, help="Number of episodes to extract")
|
| 31 |
+
parser.add_argument("--start-episode", type=int, default=0, help="First episode index")
|
| 32 |
+
parser.add_argument("--fps-downsample-ratio", type=int, default=2, help="Downsample ratio (20fps -> 10fps)")
|
| 33 |
+
parser.add_argument("--action-dim", type=int, default=29, help="Action dimension (29 for GR1 selected keys)")
|
| 34 |
+
args = parser.parse_args()
|
| 35 |
+
|
| 36 |
+
dataset_path = args.dataset_path
|
| 37 |
+
output_dir = args.output_dir or os.path.join(os.path.dirname(dataset_path), "eval", "gr1_eval_data")
|
| 38 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 39 |
+
|
| 40 |
+
# GR1 action key selection: left_arm(0:7), left_hand(7:13), right_arm(22:29), right_hand(29:35), waist(41:44)
|
| 41 |
+
action_indices = list(range(0, 7)) + list(range(7, 13)) + list(range(22, 29)) + list(range(29, 35)) + list(range(41, 44))
|
| 42 |
+
|
| 43 |
+
# Read episodes metadata
|
| 44 |
+
episodes_path = os.path.join(dataset_path, "meta", "episodes.jsonl")
|
| 45 |
+
episodes = []
|
| 46 |
+
with open(episodes_path) as f:
|
| 47 |
+
for line in f:
|
| 48 |
+
if line.strip():
|
| 49 |
+
episodes.append(json.loads(line))
|
| 50 |
+
|
| 51 |
+
total_episodes = len(episodes)
|
| 52 |
+
end_episode = min(args.start_episode + args.num_episodes, total_episodes)
|
| 53 |
+
selected = episodes[args.start_episode : end_episode]
|
| 54 |
+
|
| 55 |
+
print(f"Dataset: {dataset_path}")
|
| 56 |
+
print(f"Total episodes: {total_episodes}")
|
| 57 |
+
print(f"Selected: episodes {args.start_episode} - {end_episode - 1} ({len(selected)} episodes)")
|
| 58 |
+
print(f"Output: {output_dir}")
|
| 59 |
+
print(f"Action dim: {args.action_dim} (from 44-dim raw, indices {action_indices})")
|
| 60 |
+
print(f"FPS downsample ratio: {args.fps_downsample_ratio}")
|
| 61 |
+
print()
|
| 62 |
+
|
| 63 |
+
# Read info.json for chunk size
|
| 64 |
+
with open(os.path.join(dataset_path, "meta", "info.json")) as f:
|
| 65 |
+
info = json.load(f)
|
| 66 |
+
chunk_size = info.get("chunks_size", 1000) # episodes per chunk
|
| 67 |
+
|
| 68 |
+
skipped = 0
|
| 69 |
+
for i, ep in enumerate(selected):
|
| 70 |
+
ep_idx = ep["episode_index"]
|
| 71 |
+
chunk_idx = ep_idx // chunk_size
|
| 72 |
+
|
| 73 |
+
# Video path
|
| 74 |
+
video_key = "observation.images.ego_view_freq20"
|
| 75 |
+
video_src = os.path.join(
|
| 76 |
+
dataset_path,
|
| 77 |
+
"videos",
|
| 78 |
+
f"chunk-{chunk_idx:03d}",
|
| 79 |
+
video_key,
|
| 80 |
+
f"episode_{ep_idx:06d}.mp4",
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
# Parquet path
|
| 84 |
+
parquet_path = os.path.join(
|
| 85 |
+
dataset_path,
|
| 86 |
+
"data",
|
| 87 |
+
f"chunk-{chunk_idx:03d}",
|
| 88 |
+
f"episode_{ep_idx:06d}.parquet",
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
if not os.path.exists(video_src):
|
| 92 |
+
print(f" [SKIP] ep {ep_idx}: video not found: {video_src}")
|
| 93 |
+
skipped += 1
|
| 94 |
+
continue
|
| 95 |
+
if not os.path.exists(parquet_path):
|
| 96 |
+
print(f" [SKIP] ep {ep_idx}: parquet not found: {parquet_path}")
|
| 97 |
+
skipped += 1
|
| 98 |
+
continue
|
| 99 |
+
|
| 100 |
+
# Symlink video
|
| 101 |
+
video_dst = os.path.join(output_dir, f"episode_{ep_idx:06d}.mp4")
|
| 102 |
+
if os.path.islink(video_dst) or os.path.exists(video_dst):
|
| 103 |
+
os.remove(video_dst)
|
| 104 |
+
os.symlink(os.path.abspath(video_src), video_dst)
|
| 105 |
+
|
| 106 |
+
# Extract actions from parquet
|
| 107 |
+
table = pq.read_table(parquet_path)
|
| 108 |
+
actions_raw = np.array(table.column("action").to_pylist()) # (T, 44)
|
| 109 |
+
|
| 110 |
+
# Select 29 dims and downsample
|
| 111 |
+
actions_selected = actions_raw[:, action_indices] # (T, 29)
|
| 112 |
+
actions_downsampled = actions_selected[:: args.fps_downsample_ratio][:-1] # drop last, matching inference_gr00t.py
|
| 113 |
+
|
| 114 |
+
# Save NPY
|
| 115 |
+
npy_path = os.path.join(output_dir, f"episode_{ep_idx:06d}_actions.npy")
|
| 116 |
+
np.save(npy_path, actions_downsampled)
|
| 117 |
+
|
| 118 |
+
if (i + 1) % 20 == 0 or i == 0:
|
| 119 |
+
print(f" [{i + 1}/{len(selected)}] ep {ep_idx}: {actions_downsampled.shape[0]} steps, {actions_downsampled.shape[1]} dims")
|
| 120 |
+
|
| 121 |
+
print()
|
| 122 |
+
print(f"Done. {len(selected) - skipped} episodes prepared, {skipped} skipped.")
|
| 123 |
+
print(f"Output directory: {output_dir}")
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
if __name__ == "__main__":
|
| 127 |
+
main()
|