xizaoqu commited on
Commit
622d9c9
·
1 Parent(s): a0e6519

update trainning pipeline and datasets

Browse files
README.md CHANGED
@@ -39,6 +39,7 @@ https://github.com/user-attachments/assets/fb8a32e2-9470-4819-a93d-c38caf76d72c
39
  conda create python=3.10 -n worldmem
40
  conda activate worldmem
41
  pip install -r requirements.txt
 
42
  ```
43
 
44
 
@@ -48,11 +49,81 @@ pip install -r requirements.txt
48
  python app.py
49
  ```
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  ## TODO
52
 
53
  - [x] Release inference models and weights;
54
- - [ ] Release training pipeline on Minecraft;
55
- - [ ] Release training data on Minecraft;
56
 
57
 
58
 
 
39
  conda create python=3.10 -n worldmem
40
  conda activate worldmem
41
  pip install -r requirements.txt
42
+ conda install -c conda-forge ffmpeg=4.3.2
43
  ```
44
 
45
 
 
49
  python app.py
50
  ```
51
 
52
+ ## Training and Inference
53
+
54
+ To enable cloud logging with [Weights & Biases (wandb)](https://wandb.ai/site), follow these steps:
55
+
56
+ 1. Sign up for a wandb account.
57
+ 2. Run the following command to log in:
58
+
59
+ ```bash
60
+ wandb login
61
+ ```
62
+
63
+ 3. Open `configurations/training.yaml` and set the `entity` and `project` field to your wandb username.
64
+
65
+ ---
66
+
67
+ ### Training
68
+
69
+ We observe that gradually increasing task difficulty improves performance. Thus, we adopt a multi-stage training strategy:
70
+
71
+ ```bash
72
+ sh train_stage_1.sh # Small range, no vertical turning
73
+ sh train_stage_2.sh # Large range, no vertical turning
74
+ sh train_stage_3.sh # Large range, with vertical turning
75
+ ```
76
+
77
+ To resume training from a previous checkpoint, configure the `resume` and `output_dir` variables in the corresponding `.sh` script.
78
+
79
+ ---
80
+
81
+ ### Inference
82
+
83
+ To run inference:
84
+
85
+ ```bash
86
+ sh infer.sh
87
+ ```
88
+
89
+ You can either **load the diffusion model and VAE separately**:
90
+
91
+ ```bash
92
+ +diffusion_model_path=yslan/worldmem_checkpoints/diffusion_only.ckpt \
93
+ +vae_path=yslan/worldmem_checkpoints/vae_only.ckpt \
94
+ +customized_load=true \
95
+ +seperate_load=true \
96
+ ```
97
+
98
+ Or **load a combined checkpoint**:
99
+
100
+ ```bash
101
+ +load=your_model_path \
102
+ +customized_load=true \
103
+ +seperate_load=false \
104
+ ```
105
+
106
+ ---
107
+
108
+ ## Dataset
109
+
110
+ Download the Minecraft dataset from [Hugging Face](https://huggingface.co/datasets/zeqixiao/worldmem_minecraft_dataset)
111
+
112
+ Place the dataset in the following directory structure:
113
+
114
+ ```
115
+ data/
116
+ └── minecraft/
117
+ ├── training/
118
+ └── validation/
119
+ ```
120
+
121
+
122
  ## TODO
123
 
124
  - [x] Release inference models and weights;
125
+ - [x] Release training pipeline on Minecraft;
126
+ - [x] Release training data on Minecraft;
127
 
128
 
129
 
app.py CHANGED
@@ -23,11 +23,23 @@ from huggingface_hub import hf_hub_download
23
  import tempfile
24
  import os
25
  import requests
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  torch.set_float32_matmul_precision("high")
28
 
29
  def load_custom_checkpoint(algo, checkpoint_path):
30
- try:
31
  hf_ckpt = str(checkpoint_path).split('/')
32
  repo_id = '/'.join(hf_ckpt[:2])
33
  file_name = '/'.join(hf_ckpt[2:])
@@ -35,9 +47,17 @@ def load_custom_checkpoint(algo, checkpoint_path):
35
  filename=file_name)
36
  ckpt = torch.load(model_path, map_location=torch.device('cpu'))
37
 
38
- algo.load_state_dict(ckpt['state_dict'], strict=True)
 
 
 
 
 
 
 
 
39
  print("Load: ", model_path)
40
- except:
41
  ckpt = torch.load(checkpoint_path, map_location=torch.device('cpu'))
42
 
43
  filtered_state_dict = {}
@@ -192,13 +212,21 @@ SUNFLOWERS_RAIN_IMAGE = "assets/rain_sunflower_plains.png"
192
  device = torch.device('cuda')
193
 
194
  def save_video(frames, path="output.mp4", fps=10):
 
195
  h, w, _ = frames[0].shape
196
- out = cv2.VideoWriter(path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w, h))
 
197
  for frame in frames:
198
  out.write(cv2.cvtColor(frame, cv2.COLOR_RGB2BGR))
199
  out.release()
200
 
201
- return path
 
 
 
 
 
 
202
 
203
  cfg = OmegaConf.load("configurations/huggingface.yaml")
204
  worldmem = WorldMemMinecraft(cfg)
 
23
  import tempfile
24
  import os
25
  import requests
26
+ from huggingface_hub import model_info
27
+
28
+
29
+ def is_huggingface_model(path: str) -> bool:
30
+ hf_ckpt = str(path).split('/')
31
+ repo_id = '/'.join(hf_ckpt[:2])
32
+ try:
33
+ model_info(repo_id)
34
+ return True
35
+ except:
36
+ return False
37
+
38
 
39
  torch.set_float32_matmul_precision("high")
40
 
41
  def load_custom_checkpoint(algo, checkpoint_path):
42
+ if is_huggingface_model(str(checkpoint_path)):
43
  hf_ckpt = str(checkpoint_path).split('/')
44
  repo_id = '/'.join(hf_ckpt[:2])
45
  file_name = '/'.join(hf_ckpt[2:])
 
47
  filename=file_name)
48
  ckpt = torch.load(model_path, map_location=torch.device('cpu'))
49
 
50
+ filtered_state_dict = {}
51
+ for k, v in ckpt['state_dict'].items():
52
+ if "frame_timestep_embedder" in k:
53
+ new_k = k.replace("frame_timestep_embedder", "timestamp_embedding")
54
+ filtered_state_dict[new_k] = v
55
+ else:
56
+ filtered_state_dict[k] = v
57
+
58
+ algo.load_state_dict(filtered_state_dict, strict=True)
59
  print("Load: ", model_path)
60
+ else:
61
  ckpt = torch.load(checkpoint_path, map_location=torch.device('cpu'))
62
 
63
  filtered_state_dict = {}
 
212
  device = torch.device('cuda')
213
 
214
  def save_video(frames, path="output.mp4", fps=10):
215
+ temp_path = path[:-4] + "_temp.mp4"
216
  h, w, _ = frames[0].shape
217
+
218
+ out = cv2.VideoWriter(temp_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w, h))
219
  for frame in frames:
220
  out.write(cv2.cvtColor(frame, cv2.COLOR_RGB2BGR))
221
  out.release()
222
 
223
+ ffmpeg_cmd = [
224
+ "ffmpeg", "-y", "-i", temp_path,
225
+ "-c:v", "libx264", "-crf", "23", "-preset", "medium",
226
+ path
227
+ ]
228
+ subprocess.run(ffmpeg_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
229
+ os.remove(temp_path)
230
 
231
  cfg = OmegaConf.load("configurations/huggingface.yaml")
232
  worldmem = WorldMemMinecraft(cfg)
configurations/algorithm/base_algo.yaml ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # This will be passed as the cfg to Algo.__init__(cfg) of your algorithm class
2
+
3
+ debug: ${debug} # inherited from configurations/config.yaml
configurations/algorithm/base_pytorch_algo.yaml ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ defaults:
2
+ - base_algo # inherits from configurations/algorithm/base_algo.yaml
3
+
4
+ lr: ${experiment.training.lr}
configurations/algorithm/df_base.yaml ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defaults:
2
+ - base_pytorch_algo
3
+
4
+ # dataset-dependent configurations
5
+ x_shape: ${dataset.observation_shape}
6
+ frame_stack: 1
7
+ frame_skip: 1
8
+ data_mean: ${dataset.data_mean}
9
+ data_std: ${dataset.data_std}
10
+ external_cond_dim: 0 #${dataset.action_dim}
11
+ context_frames: ${dataset.context_length}
12
+ # training hyperparameters
13
+ weight_decay: 1e-4
14
+ warmup_steps: 10000
15
+ optimizer_beta: [0.9, 0.999]
16
+ # diffusion-related
17
+ uncertainty_scale: 1
18
+ guidance_scale: 0.0
19
+ chunk_size: 1 # -1 for full trajectory diffusion, number to specify diffusion chunk size
20
+ scheduling_matrix: autoregressive
21
+ noise_level: random_all
22
+ causal: True
23
+
24
+ diffusion:
25
+ # training
26
+ objective: pred_x0
27
+ beta_schedule: cosine
28
+ schedule_fn_kwargs: {}
29
+ clip_noise: 20.0
30
+ use_snr: False
31
+ use_cum_snr: False
32
+ use_fused_snr: False
33
+ snr_clip: 5.0
34
+ cum_snr_decay: 0.98
35
+ timesteps: 1000
36
+ # sampling
37
+ sampling_timesteps: 50 # fixme, numer of diffusion steps, should be increased
38
+ ddim_sampling_eta: 1.0
39
+ stabilization_level: 10
40
+ # architecture
41
+ architecture:
42
+ network_size: 64
configurations/algorithm/df_video_worldmemminecraft.yaml ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defaults:
2
+ - df_base
3
+
4
+ n_frames: ${dataset.n_frames}
5
+ frame_skip: ${dataset.frame_skip}
6
+ metadata: ${dataset.metadata}
7
+
8
+ # training hyperparameters
9
+ weight_decay: 2e-3
10
+ warmup_steps: 1000
11
+ optimizer_beta: [0.9, 0.99]
12
+ action_cond_dim: 25
13
+ use_plucker: true
14
+
15
+ diffusion:
16
+ # training
17
+ beta_schedule: sigmoid
18
+ objective: pred_v
19
+ use_fused_snr: True
20
+ cum_snr_decay: 0.96
21
+ clip_noise: 20.
22
+ # sampling
23
+ sampling_timesteps: 20
24
+ ddim_sampling_eta: 0.0
25
+ stabilization_level: 15
26
+ # architecture
27
+ architecture:
28
+ network_size: 64
29
+ attn_heads: 4
30
+ attn_dim_head: 64
31
+ dim_mults: [1, 2, 4, 8]
32
+ resolution: ${dataset.resolution}
33
+ attn_resolutions: [16, 32, 64, 128]
34
+ use_init_temporal_attn: True
35
+ use_linear_attn: True
36
+ time_emb_type: rotary
37
+
38
+ metrics:
39
+ # - fvd
40
+ # - fid
41
+ # - lpips
42
+
43
+ _name: df_video_worldmemminecraft
configurations/dataset/base_dataset.yaml ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # This will be passed as the cfg to Dataset.__init__(cfg) of your dataset class
2
+
3
+ debug: ${debug} # inherited from configurations/config.yaml
configurations/dataset/base_video.yaml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defaults:
2
+ - base_dataset
3
+
4
+ metadata: "data/${dataset.name}/metadata.json"
5
+ data_mean: "data/${dataset.name}/data_mean.npy"
6
+ data_std: "data/${dataset.name}/data_std.npy"
7
+ save_dir: ???
8
+ n_frames: 32
9
+ context_length: 4
10
+ resolution: 128
11
+ observation_shape: [3, "${dataset.resolution}", "${dataset.resolution}"]
12
+ external_cond_dim: 0
13
+ validation_multiplier: 1
14
+ frame_skip: 1
configurations/dataset/video_minecraft.yaml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defaults:
2
+ - base_video
3
+
4
+ save_dir: data/minecraft_simple_backforward
5
+ n_frames: 16 # TODO: increase later
6
+ resolution: 128
7
+ data_mean: 0.5
8
+ data_std: 0.5
9
+ action_cond_dim: 25
10
+ context_length: 1
11
+ frame_skip: 1
12
+ validation_multiplier: 1
13
+
14
+ _name: video_minecraft_oasis
configurations/experiment/base_experiment.yaml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ debug: ${debug} # inherited from configurations/config.yaml
2
+ tasks: [main] # tasks to run sequantially, such as [training, test], useful when your project has multiple stages and you want to run only a subset of them.
configurations/experiment/base_pytorch.yaml ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # inherites from base_experiment.yaml
2
+ # most of the options have docs at https://lightning.ai/docs/pytorch/stable/common/trainer.html
3
+
4
+ defaults:
5
+ - base_experiment
6
+
7
+ tasks: [training] # tasks to run sequantially, change when your project has multiple stages and you want to run only a subset of them.
8
+ num_nodes: 1 # number of gpu servers used in large scale distributed training
9
+
10
+ training:
11
+ precision: 16-mixed # set float precision, 16-mixed is faster while 32 is more stable
12
+ compile: False # whether to compile the model with torch.compile
13
+ lr: 0.001 # learning rate
14
+ batch_size: 16 # training batch size; effective batch size is this number * gpu * nodes iff using distributed training
15
+ max_epochs: 1000 # set to -1 to train forever
16
+ max_steps: -1 # set to -1 to train forever, will override max_epochs
17
+ max_time: null # set to something like "00:12:00:00" to enable
18
+ data:
19
+ num_workers: 4 # number of CPU threads for data preprocessing.
20
+ shuffle: True # whether training data will be shuffled
21
+ optim:
22
+ accumulate_grad_batches: 1 # accumulate gradients for n batches before backprop
23
+ gradient_clip_val: 0 # clip gradients with norm above this value, set to 0 to disable
24
+ checkpointing:
25
+ # these are arguments to pytorch lightning's callback, `ModelCheckpoint` class
26
+ every_n_train_steps: 5000 # save a checkpoint every n train steps
27
+ every_n_epochs: null # mutually exclusive with ``every_n_train_steps`` and ``train_time_interval``
28
+ train_time_interval: null # in format of "00:12:00:00", mutually exclusive with ``every_n_train_steps`` and ``every_n_epochs``.
29
+ enable_version_counter: False # If this is ``False``, later checkpoint will be overwrite previous ones.
30
+
31
+ validation:
32
+ precision: 16-mixed
33
+ compile: False # whether to compile the model with torch.compile
34
+ batch_size: 16 # validation batch size per GPU; effective batch size is this number * gpu * nodes iff using distributed training
35
+ val_every_n_step: 2000 # controls how frequent do we run validation, can be float (fraction of epoches) or int (steps) or null (if val_every_n_epoch is set)
36
+ val_every_n_epoch: null # if you want to do validation every n epoches, requires val_every_n_step to be null.
37
+ limit_batch: null # if null, run through validation set. Otherwise limit the number of batches to use for validation.
38
+ inference_mode: True # whether to run validation in inference mode (enable_grad won't work!)
39
+ data:
40
+ num_workers: 4 # number of CPU threads for data preprocessing, for validation.
41
+ shuffle: False # whether validation data will be shuffled
42
+
43
+ test:
44
+ precision: 16-mixed
45
+ compile: False # whether to compile the model with torch.compile
46
+ batch_size: 4 # test batch size per GPU; effective batch size is this number * gpu * nodes iff using distributed training
47
+ limit_batch: null # if null, run through test set. Otherwise limit the number of batches to use for test.
48
+ data:
49
+ num_workers: 4 # number of CPU threads for data preprocessing, for test.
50
+ shuffle: False # whether test data will be shuffled
configurations/experiment/exp_video.yaml ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defaults:
2
+ - base_pytorch
3
+
4
+ tasks: [training]
5
+
6
+ training:
7
+ lr: 2e-5
8
+ precision: 16-mixed
9
+ batch_size: 4
10
+ max_epochs: -1
11
+ max_steps: 2000005
12
+ checkpointing:
13
+ every_n_train_steps: 2500
14
+ optim:
15
+ gradient_clip_val: 1.0
16
+
17
+ validation:
18
+ val_every_n_step: 2500
19
+ val_every_n_epoch: null
20
+ batch_size: 4
21
+ limit_batch: 1
22
+
23
+ test:
24
+ limit_batch: 1
25
+ batch_size: 1
26
+
27
+ logging:
28
+ metrics:
29
+ # - fvd
30
+ # - fid
31
+ # - lpips
configurations/huggingface.yaml CHANGED
@@ -54,7 +54,7 @@ noise_level: random_all
54
  causal: True
55
  x_shape: [3, 360, 640]
56
  context_frames: 1
57
- diffusion_path: /mnt/xiaozeqi/worldmem_github/diffusion_only_2025-04-06-16-24-11_epoch1step720000.ckpt
58
  vae_path: yslan/worldmem_checkpoints/vae_only.ckpt
59
  pose_predictor_path: yslan/worldmem_checkpoints/pose_prediction_model_only.ckpt
60
  next_frame_length: 1
 
54
  causal: True
55
  x_shape: [3, 360, 640]
56
  context_frames: 1
57
+ diffusion_path: yslan/worldmem_checkpoints/diffusion_only.ckpt
58
  vae_path: yslan/worldmem_checkpoints/vae_only.ckpt
59
  pose_predictor_path: yslan/worldmem_checkpoints/pose_prediction_model_only.ckpt
60
  next_frame_length: 1
configurations/training.yaml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # configuration parsing starts here
2
+ defaults:
3
+ - experiment: exp_video # experiment yaml file name in configurations/experiments folder [fixme]
4
+ - dataset: video_minecraft # dataset yaml file name in configurations/dataset folder [fixme]
5
+ - algorithm: df_video_worldmemminecraft # algorithm yaml file name in configurations/algorithm folder [fixme]
6
+ - cluster: null # optional, cluster yaml file name in configurations/cluster folder. Leave null for local compute
7
+
8
+ debug: false # global debug flag will be passed into configuration of experiment, dataset and algorithm
9
+
10
+ wandb:
11
+ entity: xizaoqu # wandb account name / organization name [fixme]
12
+ project: worldmem # wandb project name; if not provided, defaults to root folder name [fixme]
13
+ mode: online # set wandb logging to online, offline or dryrun
14
+
15
+ resume: null # wandb run id to resume logging and loading checkpoint from
16
+ load: null # wandb run id containing checkpoint or a path to a checkpoint file
datasets/README.md ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ The `datasets` folder is used to contain dataset code or environment code.
2
+ Don't store actual data like images here! For those, please use the `data` folder instead of `datasets`.
3
+
4
+ Create a folder to create your own pytorch dataset definition. Then, update the `__init__.py`
5
+ at every level to register all datasets.
6
+
7
+ Each dataset class takes in a DictConfig file `cfg` in its `__init__`, which allows you to pass in arguments via configuration file in `configurations/dataset` or [command line override](https://hydra.cc/docs/tutorials/basic/your_first_app/simple_cli/).
8
+
9
+ ---
10
+
11
+ This repo is forked from [Boyuan Chen](https://boyuan.space/)'s research template [repo](https://github.com/buoyancy99/research-template). By its MIT license, you must keep the above sentence in `README.md` and the `LICENSE` file to credit the author.
datasets/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from .video import MinecraftVideoDataset
datasets/video/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from .minecraft_video_dataset import MinecraftVideoDataset
datasets/video/base_video_dataset.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Sequence
2
+ import torch
3
+ import random
4
+ import os
5
+ import numpy as np
6
+ import cv2
7
+ from omegaconf import DictConfig
8
+ from torchvision import transforms
9
+ from pathlib import Path
10
+ from abc import abstractmethod, ABC
11
+ import json
12
+
13
+
14
+ class BaseVideoDataset(torch.utils.data.Dataset, ABC):
15
+ """
16
+ Base class for video datasets. Videos may be of variable length.
17
+
18
+ Folder structure of each dataset:
19
+ - [save_dir] (specified in config, e.g., data/phys101)
20
+ - /[split] (one per split)
21
+ - /data_folder_name (e.g., videos)
22
+ metadata.json
23
+ """
24
+
25
+ def __init__(self, cfg: DictConfig, split: str = "training"):
26
+ super().__init__()
27
+ self.cfg = cfg
28
+ self.split = split
29
+ self.resolution = cfg.resolution
30
+ self.external_cond_dim = cfg.external_cond_dim
31
+ self.n_frames = (
32
+ cfg.n_frames * cfg.frame_skip
33
+ if split == "training"
34
+ else cfg.n_frames * cfg.frame_skip * cfg.validation_multiplier
35
+ )
36
+ self.frame_skip = cfg.frame_skip
37
+ self.save_dir = Path(cfg.save_dir)
38
+ self.save_dir.mkdir(exist_ok=True, parents=True)
39
+ self.split_dir = self.save_dir / f"{split}"
40
+ self.data_paths = self.get_data_paths(self.split)
41
+
42
+ if self.split == 'training':
43
+ self.metadata = [1300] * len(self.data_paths) # total 1500 f, randomly sampled ~1300 clips
44
+ else:
45
+ self.metadata = [1] * len(self.data_paths) # total 1500 f
46
+
47
+ self.clips_per_video = np.clip(np.array(self.metadata) - self.n_frames + 1, a_min=1, a_max=None).astype(
48
+ np.int32
49
+ )
50
+ self.cum_clips_per_video = np.cumsum(self.clips_per_video)
51
+ self.transform = transforms.Resize((self.resolution, self.resolution), antialias=True)
52
+
53
+ # shuffle but keep the same order for each epoch, so validation sample is diverse yet deterministic
54
+ random.seed(0)
55
+ self.idx_remap = list(range(self.__len__()))
56
+ random.shuffle(self.idx_remap)
57
+
58
+ @abstractmethod
59
+ def download_dataset(self) -> Sequence[int]:
60
+ """
61
+ Download dataset from the internet and build it in save_dir
62
+
63
+ Returns a list of video lengths
64
+ """
65
+ raise NotImplementedError
66
+
67
+ @abstractmethod
68
+ def get_data_paths(self, split):
69
+ """Return a list of data paths (e.g. xxx.mp4) for a given split"""
70
+ raise NotImplementedError
71
+
72
+ def get_data_lengths(self, split):
73
+ """Return a list of num_frames for each data path (e.g. xxx.mp4) for a given split"""
74
+ lengths = []
75
+ for path in self.get_data_paths(split):
76
+ length = cv2.VideoCapture(str(path)).get(cv2.CAP_PROP_FRAME_COUNT)
77
+ lengths.append(length)
78
+ return lengths
79
+
80
+ def split_idx(self, idx):
81
+ video_idx = np.argmax(self.cum_clips_per_video > idx)
82
+ frame_idx = idx - np.pad(self.cum_clips_per_video, (1, 0))[video_idx]
83
+ return video_idx, frame_idx
84
+
85
+ @staticmethod
86
+ def load_video(path: Path):
87
+ """
88
+ Load video from a path
89
+ :param filename: path to the video
90
+ :return: video as a numpy array
91
+ """
92
+
93
+ cap = cv2.VideoCapture(str(path))
94
+
95
+ frames = []
96
+ while cap.isOpened():
97
+ ret, frame = cap.read()
98
+ if ret:
99
+ frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
100
+ frames.append(frame)
101
+ else:
102
+ break
103
+
104
+ cap.release()
105
+ frames = np.stack(frames, dtype=np.uint8)
106
+ return np.transpose(frames, (0, 3, 1, 2)) # (T, C, H, W)
107
+
108
+ @staticmethod
109
+ def load_image(filename: Path):
110
+ """
111
+ Load image from a path
112
+ :param filename: path to the image
113
+ :return: image as a numpy array
114
+ """
115
+ image = cv2.imread(str(filename))
116
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
117
+ return np.transpose(image, (2, 0, 1))
118
+
119
+ def __len__(self):
120
+ return self.clips_per_video.sum()
121
+
122
+ def __getitem__(self, idx):
123
+ idx = self.idx_remap[idx]
124
+ video_idx, frame_idx = self.split_idx(idx)
125
+ video_path = self.data_paths[video_idx]
126
+ video = self.load_video(video_path)[frame_idx : frame_idx + self.n_frames]
127
+
128
+ pad_len = self.n_frames - len(video)
129
+
130
+ nonterminal = np.ones(self.n_frames)
131
+ if len(video) < self.n_frames:
132
+ video = np.pad(video, ((0, pad_len), (0, 0), (0, 0), (0, 0)))
133
+ nonterminal[-pad_len:] = 0
134
+
135
+ video = torch.from_numpy(video / 256.0).float()
136
+ video = self.transform(video)
137
+
138
+ if self.external_cond_dim:
139
+ external_cond = np.load(
140
+ # pylint: disable=no-member
141
+ self.condition_dir
142
+ / f"{video_path.name.replace('.mp4', '.npy')}"
143
+ )
144
+ if len(external_cond) < self.n_frames:
145
+ external_cond = np.pad(external_cond, ((0, pad_len),))
146
+ external_cond = torch.from_numpy(external_cond).float()
147
+ return (
148
+ video[:: self.frame_skip],
149
+ external_cond[:: self.frame_skip],
150
+ nonterminal[:: self.frame_skip],
151
+ )
152
+ else:
153
+ return video[:: self.frame_skip], nonterminal[:: self.frame_skip]
datasets/video/minecraft_video_dataset.py ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import tarfile
4
+ import numpy as np
5
+ import torch
6
+ from typing import Sequence, Mapping
7
+ from omegaconf import DictConfig
8
+ from pytorchvideo.data.encoded_video import EncodedVideo
9
+ import random
10
+
11
+ from .base_video_dataset import BaseVideoDataset
12
+
13
+
14
+
15
+
16
+ ACTION_KEYS = [
17
+ "inventory",
18
+ "ESC",
19
+ "hotbar.1",
20
+ "hotbar.2",
21
+ "hotbar.3",
22
+ "hotbar.4",
23
+ "hotbar.5",
24
+ "hotbar.6",
25
+ "hotbar.7",
26
+ "hotbar.8",
27
+ "hotbar.9",
28
+ "forward",
29
+ "back",
30
+ "left",
31
+ "right",
32
+ "cameraY",
33
+ "cameraX",
34
+ "jump",
35
+ "sneak",
36
+ "sprint",
37
+ "swapHands",
38
+ "attack",
39
+ "use",
40
+ "pickItem",
41
+ "drop",
42
+ ]
43
+
44
+ def convert_action_space(actions):
45
+ vec_25 = torch.zeros(len(actions), len(ACTION_KEYS))
46
+ vec_25[actions[:,0]==1, 11] = 1
47
+ vec_25[actions[:,0]==2, 12] = 1
48
+ vec_25[actions[:,4]==11, 16] = -1
49
+ vec_25[actions[:,4]==13, 16] = 1
50
+ vec_25[actions[:,3]==11, 15] = -1
51
+ vec_25[actions[:,3]==13, 15] = 1
52
+ vec_25[actions[:,5]==6, 24] = 1
53
+ vec_25[actions[:,5]==1, 24] = 1
54
+ vec_25[actions[:,1]==1, 13] = 1
55
+ vec_25[actions[:,1]==2, 14] = 1
56
+ vec_25[actions[:,7]==1, 2] = 1
57
+ return vec_25
58
+
59
+ # Dataset class
60
+ class MinecraftVideoDataset(BaseVideoDataset):
61
+ """
62
+ Minecraft video dataset for training and validation.
63
+
64
+ Args:
65
+ cfg (DictConfig): Configuration object.
66
+ split (str): Dataset split ("training" or "validation").
67
+ """
68
+ def __init__(self, cfg: DictConfig, split: str = "training"):
69
+ if split == "test":
70
+ split = "validation"
71
+ self.wo_updown = getattr(cfg, "wo_updown", False)
72
+ super().__init__(cfg, split)
73
+ self.n_frames = cfg.n_frames_valid if split == "validation" and hasattr(cfg, "n_frames_valid") else cfg.n_frames
74
+ self.memory_condition_length = cfg.memory_condition_length
75
+ self.customized_validation = cfg.customized_validation
76
+ if split == "training":
77
+ self.angle_range = cfg.angle_range
78
+ self.pos_range = cfg.pos_range
79
+ self.add_timestamp_embedding = cfg.add_timestamp_embedding
80
+ self.training_dropout = 0.1
81
+ self.memory_condition_length = getattr(cfg, "memory_condition_length", False)
82
+ self.sample_more_event = getattr(cfg, "sample_more_event", False)
83
+ self.causal_frame = getattr(cfg, "causal_frame", False)
84
+
85
+ def get_data_paths(self, split: str):
86
+ """
87
+ Retrieve all video file paths for the given split.
88
+
89
+ Args:
90
+ split (str): Dataset split ("training" or "validation").
91
+
92
+ Returns:
93
+ List[Path]: List of video file paths.
94
+ """
95
+ data_dir = self.save_dir / split
96
+ paths = sorted(list(data_dir.glob("**/*.mp4")), key=lambda x: x.name)
97
+
98
+ if self.wo_updown:
99
+ # Filter out paths containing "w_updown"
100
+ paths = [p for p in paths if "w_updown" not in str(p)]
101
+
102
+ if split == "validation" and self.wo_updown:
103
+ paths = [p for p in paths if "w_updown" not in str(p)]
104
+ elif split == "validation":
105
+ paths = [p for p in paths if "w_updown" in str(p)]
106
+
107
+ if not paths:
108
+ sub_dirs = os.listdir(data_dir)
109
+ for sub_dir in sub_dirs:
110
+ sub_path = data_dir / sub_dir
111
+ paths += sorted(list(sub_path.glob("**/*.mp4")), key=lambda x: x.name)
112
+ return paths
113
+
114
+ def download_dataset(self):
115
+ pass
116
+
117
+ def __getitem__(self, idx: int):
118
+ """
119
+ Retrieve a single data sample by index.
120
+
121
+ Args:
122
+ idx (int): Index of the data sample.
123
+
124
+ Returns:
125
+ Tuple[torch.Tensor, torch.Tensor, np.ndarray, np.ndarray]: Video, actions, poses, and timestamps.
126
+ """
127
+ max_retries = 1000
128
+ for _ in range(max_retries):
129
+ try:
130
+ return self.load_data(idx)
131
+ except Exception as e:
132
+ print(f"Retrying due to error: {e}")
133
+ idx = (idx + 1) % len(self)
134
+
135
+ def load_data(self, idx):
136
+ # === 1. Remap index and skip first few frames ===
137
+ idx = self.idx_remap[idx]
138
+ file_idx, frame_idx = self.split_idx(idx)
139
+ frame_idx += 100 # initial few frames are low quality
140
+
141
+ # === 2. Load paths and data arrays ===
142
+ video_path = self.data_paths[file_idx]
143
+ action_path = video_path.with_suffix(".npz")
144
+ data = np.load(action_path)
145
+ actions_pool = convert_action_space(data["actions"])
146
+ poses_pool = data["poses"]
147
+
148
+ # Fix corrupted height (maybe) in the first frame
149
+ poses_pool[0, 1] = poses_pool[1, 1]
150
+ assert poses_pool[:, 1].ptp() < 2, f"Height variation too large: {poses_pool[:, 1].ptp()} - {video_path}"
151
+
152
+ # Pad poses if shorter than actions
153
+ if len(poses_pool) < len(actions_pool):
154
+ poses_pool = np.pad(poses_pool, ((1, 0), (0, 0)))
155
+
156
+ # === 3. Load video clip ===
157
+ video_raw = EncodedVideo.from_path(video_path, decode_audio=False)
158
+ fps = 10
159
+ clip = video_raw.get_clip(
160
+ start_sec=frame_idx / fps,
161
+ end_sec=(frame_idx + self.n_frames) / fps
162
+ )["video"]
163
+ video = clip.permute(1, 2, 3, 0).numpy()
164
+
165
+ actions = np.copy(actions_pool[frame_idx : frame_idx + self.n_frames])
166
+ poses = np.copy(poses_pool[frame_idx : frame_idx + self.n_frames])
167
+
168
+ # === 4. Normalize poses relative to current segment ===
169
+ def normalize_pose(pose, ref_pose):
170
+ pose[:, :3] -= ref_pose[:1, :3]
171
+ pose[:, -1] = -pose[:, -1]
172
+ pose[:, 3:] %= 360
173
+ return pose
174
+
175
+ poses_pool = normalize_pose(poses_pool, poses)
176
+ poses = normalize_pose(poses, poses)
177
+
178
+ assert len(video) >= self.n_frames, f"{video_path}"
179
+
180
+ # === 5. Sample memory frames for training ===
181
+ if self.split == "training" and self.memory_condition_length > 0:
182
+ use_memory = random.uniform(0, 1) > self.training_dropout
183
+
184
+ if use_memory:
185
+ # Compute pose distance between current and candidate frames
186
+ dis = np.abs(poses[:, None] - poses_pool[None, :])
187
+ dis[..., 3:][dis[..., 3:] > 180] = 360 - dis[..., 3:][dis[..., 3:] > 180]
188
+
189
+ spatial_match = (dis[..., :3] <= self.pos_range).sum(-1) >= 3
190
+ angular_match = (dis[..., 3:] <= self.angle_range).sum(-1) >= 2
191
+ not_exact_match = ((dis[..., :3] > 0).sum(-1) >= 1) | ((dis[..., 3:] > 0).sum(-1) >= 1)
192
+
193
+ valid_index = (spatial_match & angular_match & not_exact_match).sum(0)
194
+ valid_index[:100] = 0 # skip unstable early frames
195
+
196
+ # Exclude future if causality and timestamp are enabled
197
+ if self.add_timestamp_embedding and self.causal_frame and (actions_pool[:frame_idx, 24] == 1).sum() > 0:
198
+ valid_index[frame_idx:] = 0
199
+
200
+ # Select indices satisfying condition
201
+ mask = valid_index >= 1
202
+ mask[0] = False
203
+ candidate_indices = np.argwhere(mask)
204
+
205
+ # Backup candidates with weaker conditions
206
+ mask2 = valid_index >= 0
207
+ mask2[0] = False
208
+
209
+ count = min(self.memory_condition_length, candidate_indices.shape[0])
210
+ selected = candidate_indices[np.random.choice(candidate_indices.shape[0], count, replace=True)][:, 0]
211
+
212
+ if count < self.memory_condition_length:
213
+ extra = np.argwhere(mask2)
214
+ extra = extra[np.random.choice(extra.shape[0], self.memory_condition_length - count, replace=True)][:, 0]
215
+ selected = np.concatenate([selected, extra])
216
+
217
+ # Prioritize event-trigger frames if applicable
218
+ if self.sample_more_event and random.uniform(0, 1) > 0.3:
219
+ event_idx = torch.nonzero(actions_pool[:frame_idx, 24] == 1)[:, 0]
220
+ if len(event_idx) > self.memory_condition_length // 2:
221
+ event_idx = event_idx[-self.memory_condition_length // 2:]
222
+ if len(event_idx) > 0:
223
+ selected[-len(event_idx):] = event_idx + 4
224
+
225
+ else:
226
+ selected = np.full(self.memory_condition_length, random.randint(0, frame_idx))
227
+
228
+ # === 6. Retrieve video frames for selected memory indices ===
229
+ video_pool = []
230
+ for si in selected:
231
+ frame = video_raw.get_clip(start_sec=si / fps, end_sec=(si + 1) / fps)["video"][:, 0].permute(1, 2, 0)
232
+ video_pool.append(frame)
233
+
234
+ video = np.concatenate([video, np.stack(video_pool)], axis=0)
235
+ actions = np.concatenate([actions, actions_pool[selected]], axis=0)
236
+ poses = np.concatenate([poses, poses_pool[selected]], axis=0)
237
+ timestamp = np.concatenate([np.arange(frame_idx, frame_idx + self.n_frames), selected])
238
+ else:
239
+ timestamp = np.arange(self.n_frames)
240
+
241
+ # === 7. Convert video to torch format ===
242
+ video = torch.from_numpy(video / 255.0).float().permute(0, 3, 1, 2).contiguous()
243
+
244
+ # === 9. Return all items ===
245
+ return (
246
+ video[:: self.frame_skip],
247
+ actions[:: self.frame_skip],
248
+ poses[:: self.frame_skip],
249
+ timestamp
250
+ )
infer.sh ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ wandb enabled
2
+ python -m main +name=infer \
3
+ experiment.tasks=[validation] \
4
+ dataset.validation_multiplier=1 \
5
+ +diffusion_model_path=yslan/worldmem_checkpoints/diffusion_only.ckpt \
6
+ +vae_path=yslan/worldmem_checkpoints/vae_only.ckpt \
7
+ +load=/mnt/xiaozeqi/checkpoints/2025-03-31-22-24-06-epoch1step240000_2d_largerange.ckpt \
8
+ +customized_load=true \
9
+ +seperate_load=false \
10
+ dataset.n_frames=8 \
11
+ dataset.save_dir=data/minecraft \
12
+ +dataset.n_frames_valid=700 \
13
+ +dataset.memory_condition_length=8 \
14
+ +dataset.customized_validation=true \
15
+ +dataset.add_timestamp_embedding=true \
16
+ +algorithm.n_tokens=8 \
17
+ +algorithm.memory_condition_length=8 \
18
+ algorithm.context_frames=600 \
19
+ +algorithm.relative_embedding=true \
20
+ +algorithm.log_video=true \
21
+ +algorithm.add_timestamp_embedding=true \
22
+ algorithm.metrics=[lpips,psnr] \
train_stage_1.sh ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ wandb enabled
2
+
3
+ # set -e
4
+ python -m main +name=train \
5
+ +diffusion_model_path=your_diffusion_model_path \
6
+ +vae_path=your_vae_path \
7
+ +customized_load=true \
8
+ +seperate_load=true \
9
+ +zero_init_gate=true \
10
+ dataset.n_frames=8 \
11
+ dataset.save_dir=data/minecraft \
12
+ +dataset.n_frames_valid=700 \
13
+ +dataset.angle_range=110 \
14
+ +dataset.pos_range=2 \
15
+ +dataset.memory_condition_length=8 \
16
+ +dataset.customized_validation=true \
17
+ +dataset.add_timestamp_embedding=true \
18
+ +dataset.wo_updown=true \
19
+ +algorithm.n_tokens=8 \
20
+ +algorithm.memory_condition_length=8 \
21
+ algorithm.context_frames=600 \
22
+ +algorithm.relative_embedding=true \
23
+ +algorithm.log_video=true \
24
+ +algorithm.add_timestamp_embedding=true \
25
+ algorithm.metrics=[lpips,psnr] \
26
+ experiment.training.checkpointing.every_n_train_steps=2500 \
27
+ experiment.training.max_steps=120000
28
+
29
+
train_stage_2.sh ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ wandb enabled
2
+
3
+ # set -e
4
+ python -m main +name=train \
5
+ dataset.n_frames=8 \
6
+ dataset.save_dir=data/minecraft \
7
+ +dataset.n_frames_valid=700 \
8
+ +dataset.angle_range=110 \
9
+ +dataset.pos_range=8 \
10
+ +dataset.memory_condition_length=8 \
11
+ +dataset.customized_validation=true \
12
+ +dataset.add_timestamp_embedding=true \
13
+ +dataset.wo_updown=true \
14
+ +algorithm.n_tokens=8 \
15
+ +algorithm.memory_condition_length=8 \
16
+ algorithm.context_frames=600 \
17
+ +algorithm.relative_embedding=true \
18
+ +algorithm.log_video=true \
19
+ +algorithm.add_timestamp_embedding=true \
20
+ algorithm.metrics=[lpips,psnr] \
21
+ experiment.training.checkpointing.every_n_train_steps=2500 \
22
+ resume=your_wandb_job_id e.g.yhht29bz \
23
+ +output_dir=your_saving_path e.g. outputs/2025-05-18/15-16-32 \
24
+ experiment.training.max_steps=240000
25
+
train_stage_3.sh ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ wandb enabled
2
+
3
+ # set -e
4
+ python -m main +name=train \
5
+ dataset.n_frames=8 \
6
+ dataset.save_dir=data/minecraft \
7
+ +dataset.n_frames_valid=700 \
8
+ +dataset.angle_range=110 \
9
+ +dataset.pos_range=8 \
10
+ +dataset.memory_condition_length=8 \
11
+ +dataset.customized_validation=true \
12
+ +dataset.add_timestamp_embedding=true \
13
+ +dataset.wo_updown=false \
14
+ +algorithm.n_tokens=8 \
15
+ +algorithm.memory_condition_length=8 \
16
+ algorithm.context_frames=600 \
17
+ +algorithm.relative_embedding=true \
18
+ +algorithm.log_video=true \
19
+ +algorithm.add_timestamp_embedding=true \
20
+ algorithm.metrics=[lpips,psnr] \
21
+ experiment.training.checkpointing.every_n_train_steps=2500 \
22
+ resume=your_wandb_job_id e.g.yhht29bz \
23
+ +output_dir=your_saving_path e.g. outputs/2025-05-18/15-16-32 \
24
+ experiment.training.max_steps=700000