khania commited on
Commit
8d54c4c
·
verified ·
1 Parent(s): 146f9e2

Motion VQ-VAE model update

Browse files
Files changed (7) hide show
  1. .gitattributes +2 -32
  2. README.md +224 -0
  3. config.json +15 -0
  4. mean.npy +3 -0
  5. motion_vqvae_hf.py +760 -0
  6. pytorch_model.bin +3 -0
  7. std.npy +3 -0
.gitattributes CHANGED
@@ -1,35 +1,5 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
  *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
  *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
  *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz 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
 
 
 
1
  *.bin filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
2
  *.npy filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
3
  *.safetensors filter=lfs diff=lfs merge=lfs -text
4
+ *.pth filter=lfs diff=lfs merge=lfs -text
5
+ *.pt filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
README.md ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: cc-by-nc-4.0
3
+ tags:
4
+ - motion
5
+ - vqvae
6
+ - motion-tokenization
7
+ - motion-generation
8
+ - human-motion
9
+ - vector-quantization
10
+ language:
11
+ - en
12
+ library_name: transformers
13
+ pipeline_tag: feature-extraction
14
+ datasets:
15
+ - MotionMillion
16
+ ---
17
+
18
+ # MotionVQVAE
19
+
20
+ A Multi-Group Vector Quantized VAE (MG-VQVAE) trained on the MotionMillion dataset for motion tokenization and reconstruction.
21
+
22
+ > ⚠️ **License Notice**: This model is released under **CC BY-NC 4.0** (Creative Commons Attribution-NonCommercial 4.0). The training data includes datasets with mixed licensing terms, some of which restrict commercial use. **This model is for research and non-commercial use only.**
23
+
24
+ > 📋 **Body Model**: This model was trained on motion data using the **SMPL body model** (22 joints). Input motions must be in SMPL skeleton format.
25
+
26
+ ## Model Description
27
+
28
+ MotionVQVAE learns to compress human motion sequences into discrete tokens using a Multi-Group Vector Quantization approach. The model can:
29
+
30
+ - **Tokenize** motion sequences into discrete tokens for downstream generation tasks
31
+ - **Reconstruct** motions from tokens with high fidelity
32
+ - **Compress** variable-length motions with 4× temporal downsampling
33
+
34
+ ### Multi-Group VQ Architecture
35
+
36
+ Instead of a single codebook, MG-VQVAE uses **64 parallel groups**, each with its own 512-code codebook. This provides:
37
+ - Effective codebook size: $512^{64} \approx 2.47 \times 10^{173}$ combinations
38
+ - Fine-grained control over different motion aspects
39
+ - Better reconstruction quality through distributed quantization
40
+
41
+ ## Usage
42
+
43
+ ### Installation
44
+
45
+ ```bash
46
+ pip install torch huggingface_hub numpy
47
+ ```
48
+
49
+ ### Download the Model Code
50
+
51
+ Download `motion_vqvae_hf.py` from this repository or copy it to your project.
52
+
53
+ ### Quick Start
54
+
55
+ ```python
56
+ from motion_vqvae_hf import MotionVQVAE
57
+ import numpy as np
58
+
59
+ # Load model (auto-downloads from HuggingFace)
60
+ model = MotionVQVAE.from_pretrained("khania/motion-vqvae")
61
+
62
+ # Prepare motion data (272-dim absolute root format)
63
+ motion = np.random.randn(120, 272).astype(np.float32) # Replace with real motion
64
+
65
+ # Encode motion to tokens
66
+ tokens = model.encode(motion) # Returns token indices for each group
67
+ print(f"Tokens shape: {tokens.shape}") # (64, 1, 30) - 64 groups, batch=1, T/4 timesteps
68
+
69
+ # Decode tokens back to motion
70
+ motion_recon = model.decode(tokens)
71
+ print(f"Reconstructed motion shape: {motion_recon.shape}") # (1, 120, 272)
72
+
73
+ # Full forward pass (encode + decode)
74
+ motion_recon, tokens = model(motion)
75
+ ```
76
+
77
+ ### Batch Processing
78
+
79
+ ```python
80
+ # Process multiple motions
81
+ motions = [
82
+ np.random.randn(100, 272).astype(np.float32),
83
+ np.random.randn(150, 272).astype(np.float32),
84
+ np.random.randn(80, 272).astype(np.float32),
85
+ ]
86
+
87
+ # Encode batch (will pad to max length)
88
+ tokens = model.encode_batch(motions)
89
+
90
+ # Decode batch
91
+ motions_recon = model.decode_batch(tokens)
92
+ ```
93
+
94
+ ### Access Codebook
95
+
96
+ ```python
97
+ # Get quantized embeddings for analysis
98
+ embeddings = model.get_codebook_embeddings()
99
+ print(f"Codebook shape: {embeddings.shape}") # (64, 512, 8) - 64 groups, 512 codes, 8-dim each
100
+ ```
101
+
102
+ ## Model Architecture
103
+
104
+ | Component | Details |
105
+ |-----------|---------|
106
+ | **Encoder** | 1D CNN with residual blocks |
107
+ | **Decoder** | 1D CNN with residual blocks |
108
+ | **Width** | 1024 |
109
+ | **Depth** | 3 residual blocks per stage |
110
+ | **Downsampling** | 4× (stride 2, 2 stages) |
111
+ | **Quantizer** | Multi-Group VQ with EMA updates |
112
+ | **Groups** | 64 |
113
+ | **Codebook Size** | 512 codes per group |
114
+ | **Code Dimension** | 8 per group (512 total) |
115
+ | **Total Parameters** | ~73M |
116
+
117
+ ## Motion Format
118
+
119
+ The model expects **272-dimensional motion features in absolute root format** based on the **SMPL body model** (22 joints).
120
+
121
+ ### SMPL Body Model Requirement
122
+
123
+ This model was trained exclusively on motion data represented using the [SMPL body model](https://smpl.is.tue.mpg.de/). Your input motions must:
124
+
125
+ - Use the **SMPL skeleton** with 22 joints
126
+ - Follow the SMPL joint ordering
127
+ - Be converted to the 272-dimensional HumanML3D-style representation
128
+
129
+ If your motion data uses a different skeleton (e.g., CMU, Mixamo, custom rigs), you must first retarget it to SMPL before using this model.
130
+
131
+ ### Feature Dimensions
132
+
133
+ | Dimensions | Description |
134
+ |------------|-------------|
135
+ | `[0:2]` | Root XZ velocities |
136
+ | `[2:8]` | Absolute heading rotation (6D representation) |
137
+ | `[8:74]` | Local joint positions (22 joints × 3) |
138
+ | `[74:140]` | Local joint velocities (22 joints × 3) |
139
+ | `[140:272]` | Joint rotations in 6D (22 joints × 6) |
140
+
141
+ The model automatically normalizes input motions using the bundled mean/std statistics.
142
+
143
+ ## Training Details
144
+
145
+ | Parameter | Value |
146
+ |-----------|-------|
147
+ | **Dataset** | MotionMillion |
148
+ | **Batch Size** | 128 |
149
+ | **Training Iterations** | 300,000 |
150
+ | **Learning Rate** | 2e-4 |
151
+ | **LR Schedule** | Step decay at 50K, 400K |
152
+ | **Loss Function** | L1 Smooth + Commitment |
153
+ | **Commitment Weight** | 0.02 |
154
+ | **Window Size** | 64 frames |
155
+
156
+ ### Loss Weights
157
+
158
+ | Component | Weight |
159
+ |-----------|--------|
160
+ | Root XZ Velocity | 3.0 |
161
+ | Root Rotation | 1.5 |
162
+ | Joint Position | 0.1 |
163
+ | Joint Velocity | 0.5 |
164
+ | Joint Rotation | 5.0 |
165
+ | Velocity Temporal | 0.5 |
166
+
167
+ ## Performance
168
+
169
+ Final evaluation metrics at 300K iterations:
170
+
171
+ | Metric | Value |
172
+ |--------|-------|
173
+ | **Reconstruction Loss** | 0.0095 |
174
+ | **Commitment Loss** | 0.0255 |
175
+ | **Perplexity** | 508.57 |
176
+ | **Codebook Utilization** | 100% |
177
+
178
+ ### Per-Component Reconstruction Loss (Eval)
179
+
180
+ | Component | Loss |
181
+ |-----------|------|
182
+ | Root XZ Velocity | 0.00107 |
183
+ | Root Rotation | 0.00029 |
184
+ | Joint Position | 0.00851 |
185
+ | Joint Velocity | 0.02301 |
186
+ | Joint Rotation | 0.00383 |
187
+
188
+ ## Files in This Repository
189
+
190
+ | File | Size | Description |
191
+ |------|------|-------------|
192
+ | `config.json` | ~300 B | Model configuration |
193
+ | `pytorch_model.bin` | ~280 MB | Model weights (~73M parameters) |
194
+ | `mean.npy` | 1.2 KB | Motion normalization mean (272,) |
195
+ | `std.npy` | 1.2 KB | Motion normalization std (272,) |
196
+ | `motion_vqvae_hf.py` | ~20 KB | Model implementation |
197
+
198
+ ## Use Cases
199
+
200
+ - **Motion Generation**: Tokenize motions for autoregressive or diffusion-based generation
201
+ - **Motion Compression**: Efficiently store motion data as discrete tokens
202
+ - **Motion Editing**: Manipulate tokens for motion modification
203
+ - **Downstream Tasks**: Use tokens as input for text-to-motion models
204
+
205
+ ## Limitations
206
+
207
+ - Trained on English text descriptions only (for associated metadata)
208
+ - Motion format is specific to HumanML3D-style 272-dim representation
209
+ - 4× temporal downsampling may lose very fine-grained details
210
+ - Best performance on motions similar to training distribution (daily activities, sports, etc.)
211
+
212
+ ## Citation
213
+
214
+ ```bibtex
215
+ @article{motionmillion2026,
216
+ title={MotionMillion: A Large-Scale Motion-Language Dataset},
217
+ author={...},
218
+ year={2026}
219
+ }
220
+ ```
221
+
222
+ ## License
223
+
224
+ **CC BY-NC 4.0** (Creative Commons Attribution-NonCommercial 4.0 International)
config.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "input_dim": 272,
3
+ "code_dim": 512,
4
+ "nb_code": 512,
5
+ "num_groups": 64,
6
+ "down_t": 2,
7
+ "stride_t": 2,
8
+ "width": 1024,
9
+ "depth": 3,
10
+ "dilation_growth_rate": 3,
11
+ "kernel_size": 3,
12
+ "activation": "relu",
13
+ "use_mgvq": true,
14
+ "absolute_root": true
15
+ }
mean.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a3e3ce8012ec7085209c805c3d9f8deb56bc447e8901b8f30fea8da6a841f302
3
+ size 1216
motion_vqvae_hf.py ADDED
@@ -0,0 +1,760 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MotionVQVAE - Motion Vector Quantized VAE for HuggingFace
3
+
4
+ Load and use the MotionVQVAE model for motion tokenization and reconstruction.
5
+
6
+ Usage:
7
+ from motion_vqvae_hf import MotionVQVAE
8
+
9
+ # Load from HuggingFace Hub
10
+ model = MotionVQVAE.from_pretrained("khania/motion-vqvae")
11
+
12
+ # Encode motion to tokens
13
+ tokens = model.encode(motion_array) # (B, T, 272) -> (num_groups, B, T')
14
+
15
+ # Decode tokens back to motion
16
+ motion_recon = model.decode(tokens) # (num_groups, B, T') -> (B, T, 272)
17
+
18
+ # Full forward pass
19
+ motion_recon, tokens = model(motion_array)
20
+ """
21
+
22
+ import os
23
+ import json
24
+ import math
25
+ import torch
26
+ import torch.nn as nn
27
+ import torch.nn.functional as F
28
+ import numpy as np
29
+ from typing import List, Union, Optional, Dict, Any, Tuple
30
+ from pathlib import Path
31
+
32
+ try:
33
+ from huggingface_hub import snapshot_download
34
+ HF_HUB_AVAILABLE = True
35
+ except ImportError:
36
+ HF_HUB_AVAILABLE = False
37
+
38
+
39
+ # =============================================================================
40
+ # Encoder / Decoder Components (matching original architecture exactly)
41
+ # =============================================================================
42
+
43
+ class ResConv1DBlock(nn.Module):
44
+ """Residual 1D Convolution Block - matches original models/resnet.py exactly."""
45
+
46
+ def __init__(self, n_in: int, n_state: int, dilation: int = 1,
47
+ activation: str = 'relu', norm: str = None, kernel_size: int = 3):
48
+ super().__init__()
49
+ padding = dilation * (kernel_size - 1) // 2
50
+ self.norm = norm
51
+
52
+ # Norm layers
53
+ if norm == "LN":
54
+ self.norm1 = nn.LayerNorm(n_in)
55
+ self.norm2 = nn.LayerNorm(n_in)
56
+ elif norm == "GN":
57
+ self.norm1 = nn.GroupNorm(num_groups=32, num_channels=n_in, eps=1e-6, affine=True)
58
+ self.norm2 = nn.GroupNorm(num_groups=32, num_channels=n_in, eps=1e-6, affine=True)
59
+ elif norm == "BN":
60
+ self.norm1 = nn.BatchNorm1d(num_features=n_in, eps=1e-6, affine=True)
61
+ self.norm2 = nn.BatchNorm1d(num_features=n_in, eps=1e-6, affine=True)
62
+ else:
63
+ self.norm1 = nn.Identity()
64
+ self.norm2 = nn.Identity()
65
+
66
+ # Activation layers
67
+ if activation == "relu":
68
+ self.activation1 = nn.ReLU()
69
+ self.activation2 = nn.ReLU()
70
+ elif activation == "silu":
71
+ self.activation1 = nn.SiLU()
72
+ self.activation2 = nn.SiLU()
73
+ elif activation == "gelu":
74
+ self.activation1 = nn.GELU()
75
+ self.activation2 = nn.GELU()
76
+ else:
77
+ self.activation1 = nn.ReLU()
78
+ self.activation2 = nn.ReLU()
79
+
80
+ # Convolution layers - MUST be named conv1 and conv2 to match checkpoint
81
+ self.conv1 = nn.Conv1d(n_in, n_state, kernel_size, 1, padding, dilation)
82
+ self.conv2 = nn.Conv1d(n_state, n_in, 1, 1, 0)
83
+
84
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
85
+ x_orig = x
86
+ if self.norm == "LN":
87
+ x = self.norm1(x.transpose(-2, -1))
88
+ x = self.activation1(x.transpose(-2, -1))
89
+ else:
90
+ x = self.norm1(x)
91
+ x = self.activation1(x)
92
+
93
+ x = self.conv1(x)
94
+
95
+ if self.norm == "LN":
96
+ x = self.norm2(x.transpose(-2, -1))
97
+ x = self.activation2(x.transpose(-2, -1))
98
+ else:
99
+ x = self.norm2(x)
100
+ x = self.activation2(x)
101
+
102
+ x = self.conv2(x)
103
+ x = x + x_orig
104
+ return x
105
+
106
+
107
+ class Resnet1D(nn.Module):
108
+ """1D Residual Network - matches original models/resnet.py exactly.
109
+
110
+ Uses self.model = nn.Sequential(*blocks) to match checkpoint key structure.
111
+ """
112
+
113
+ def __init__(self, n_in: int, n_depth: int, dilation_growth_rate: int = 1,
114
+ reverse_dilation: bool = False, activation: str = 'relu',
115
+ norm: str = None, kernel_size: int = 3):
116
+ super().__init__()
117
+
118
+ blocks = [
119
+ ResConv1DBlock(n_in, n_in, dilation=dilation_growth_rate ** depth,
120
+ activation=activation, norm=norm, kernel_size=kernel_size)
121
+ for depth in range(n_depth)
122
+ ]
123
+ if reverse_dilation:
124
+ blocks = blocks[::-1]
125
+
126
+ # MUST be named 'model' to match checkpoint keys like 'model.0.conv1.weight'
127
+ self.model = nn.Sequential(*blocks)
128
+
129
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
130
+ return self.model(x)
131
+
132
+
133
+ class Encoder(nn.Module):
134
+ """1D CNN Encoder - matches original models/encdec.py exactly.
135
+
136
+ Uses self.model = nn.Sequential(*blocks) to match checkpoint key structure:
137
+ - model.0: Conv1d (input projection)
138
+ - model.1: ReLU
139
+ - model.2: Sequential(Conv1d, Resnet1D) for first downsample
140
+ - model.3: Sequential(Conv1d, Resnet1D) for second downsample
141
+ - model.4: Conv1d (output projection)
142
+ """
143
+
144
+ def __init__(
145
+ self,
146
+ input_emb_width: int = 272,
147
+ output_emb_width: int = 512,
148
+ down_t: int = 2,
149
+ stride_t: int = 2,
150
+ width: int = 512,
151
+ depth: int = 3,
152
+ dilation_growth_rate: int = 3,
153
+ activation: str = 'relu',
154
+ norm: str = None,
155
+ kernel_size: int = 3
156
+ ):
157
+ super().__init__()
158
+
159
+ blocks = []
160
+ filter_t, pad_t = stride_t * 2, stride_t // 2
161
+
162
+ # model.0: input conv
163
+ blocks.append(nn.Conv1d(input_emb_width, width, kernel_size, 1, (kernel_size - 1) // 2))
164
+ # model.1: ReLU
165
+ blocks.append(nn.ReLU())
166
+
167
+ # model.2, model.3, ...: downsample blocks
168
+ for i in range(down_t):
169
+ input_dim = width
170
+ block = nn.Sequential(
171
+ nn.Conv1d(input_dim, width, filter_t, stride_t, pad_t),
172
+ Resnet1D(width, depth, dilation_growth_rate, activation=activation,
173
+ norm=norm, kernel_size=kernel_size),
174
+ )
175
+ blocks.append(block)
176
+
177
+ # model.4: output conv
178
+ blocks.append(nn.Conv1d(width, output_emb_width, kernel_size, 1, (kernel_size - 1) // 2))
179
+
180
+ # MUST be named 'model' to match checkpoint keys
181
+ self.model = nn.Sequential(*blocks)
182
+
183
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
184
+ return self.model(x)
185
+
186
+
187
+ class Decoder(nn.Module):
188
+ """1D CNN Decoder - matches original models/encdec.py exactly.
189
+
190
+ Uses self.model = nn.Sequential(*blocks) to match checkpoint key structure:
191
+ - model.0: Conv1d (input projection)
192
+ - model.1: ReLU
193
+ - model.2: Sequential(Resnet1D, Upsample, Conv1d) for first upsample
194
+ - model.3: Sequential(Resnet1D, Upsample, Conv1d) for second upsample
195
+ - model.4: Conv1d
196
+ - model.5: ReLU
197
+ - model.6: Conv1d (output projection)
198
+ """
199
+
200
+ def __init__(
201
+ self,
202
+ input_emb_width: int = 272,
203
+ output_emb_width: int = 512,
204
+ down_t: int = 2,
205
+ stride_t: int = 2,
206
+ width: int = 512,
207
+ depth: int = 3,
208
+ dilation_growth_rate: int = 3,
209
+ activation: str = 'relu',
210
+ norm: str = None,
211
+ kernel_size: int = 3
212
+ ):
213
+ super().__init__()
214
+
215
+ blocks = []
216
+ filter_t, pad_t = stride_t * 2, stride_t // 2
217
+
218
+ # model.0: input conv
219
+ blocks.append(nn.Conv1d(output_emb_width, width, kernel_size, 1, (kernel_size - 1) // 2))
220
+ # model.1: ReLU
221
+ blocks.append(nn.ReLU())
222
+
223
+ # model.2, model.3, ...: upsample blocks
224
+ for i in range(down_t):
225
+ out_dim = width
226
+ block = nn.Sequential(
227
+ Resnet1D(width, depth, dilation_growth_rate, reverse_dilation=True,
228
+ activation=activation, norm=norm, kernel_size=kernel_size),
229
+ nn.Upsample(scale_factor=2, mode='linear', align_corners=False),
230
+ nn.Conv1d(width, out_dim, 3, 1, 1)
231
+ )
232
+ blocks.append(block)
233
+
234
+ # model.4: conv
235
+ blocks.append(nn.Conv1d(width, width, kernel_size, 1, (kernel_size - 1) // 2))
236
+ # model.5: ReLU
237
+ blocks.append(nn.ReLU())
238
+ # model.6: output conv
239
+ blocks.append(nn.Conv1d(width, input_emb_width, kernel_size, 1, (kernel_size - 1) // 2))
240
+
241
+ # MUST be named 'model' to match checkpoint keys
242
+ self.model = nn.Sequential(*blocks)
243
+
244
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
245
+ return self.model(x)
246
+
247
+
248
+ # =============================================================================
249
+ # Vector Quantizer
250
+ # =============================================================================
251
+
252
+ class VectorQuantizerEMA(nn.Module):
253
+ """Single Vector Quantizer with EMA updates.
254
+
255
+ Uses self.codebook as nn.Parameter to match checkpoint keys.
256
+ """
257
+
258
+ def __init__(
259
+ self,
260
+ num_embeddings: int = 512,
261
+ embedding_dim: int = 8, # per-group dimension
262
+ decay: float = 0.99,
263
+ epsilon: float = 1e-5
264
+ ):
265
+ super().__init__()
266
+
267
+ self.num_embeddings = num_embeddings
268
+ self.embedding_dim = embedding_dim
269
+ self.decay = decay
270
+ self.epsilon = epsilon
271
+
272
+ # MUST be named 'codebook' to match checkpoint keys
273
+ self.codebook = nn.Parameter(torch.randn(num_embeddings, embedding_dim))
274
+
275
+ def forward(self, z: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
276
+ """
277
+ Args:
278
+ z: (B, D, T) latent features for this group
279
+ Returns:
280
+ z_q: (B, D, T) quantized features
281
+ indices: (B, T) codebook indices
282
+ """
283
+ B, D, T = z.shape
284
+
285
+ # Reshape: (B, D, T) -> (B*T, D)
286
+ z_flat = z.permute(0, 2, 1).reshape(-1, D)
287
+
288
+ # Compute distances to codebook
289
+ # d(z, e) = ||z||^2 + ||e||^2 - 2*z*e
290
+ distances = (
291
+ torch.sum(z_flat ** 2, dim=1, keepdim=True)
292
+ + torch.sum(self.codebook ** 2, dim=1)
293
+ - 2 * torch.matmul(z_flat, self.codebook.t())
294
+ )
295
+
296
+ # Get nearest codebook entry
297
+ indices = torch.argmin(distances, dim=1)
298
+
299
+ # Quantize
300
+ z_q_flat = F.embedding(indices, self.codebook)
301
+
302
+ # Reshape back: (B*T, D) -> (B, D, T)
303
+ z_q = z_q_flat.reshape(B, T, D).permute(0, 2, 1)
304
+
305
+ # Straight-through estimator
306
+ z_q = z + (z_q - z).detach()
307
+
308
+ # Reshape indices: (B*T,) -> (B, T)
309
+ indices = indices.reshape(B, T)
310
+
311
+ return z_q, indices
312
+
313
+ def decode_indices(self, indices: torch.Tensor) -> torch.Tensor:
314
+ """
315
+ Args:
316
+ indices: (B, T) codebook indices
317
+ Returns:
318
+ z_q: (B, D, T) quantized features
319
+ """
320
+ B, T = indices.shape
321
+ z_q_flat = F.embedding(indices.reshape(-1), self.codebook)
322
+ z_q = z_q_flat.reshape(B, T, -1).permute(0, 2, 1)
323
+ return z_q
324
+
325
+
326
+ class MultiGroupVectorQuantizer(nn.Module):
327
+ """Multi-Group Vector Quantizer - splits latent into groups.
328
+
329
+ Uses self.quantizers = nn.ModuleList to match checkpoint keys like
330
+ 'quantizer.quantizers.0.codebook', 'quantizer.quantizers.1.codebook', etc.
331
+ """
332
+
333
+ def __init__(
334
+ self,
335
+ num_groups: int = 64,
336
+ num_embeddings: int = 512,
337
+ embedding_dim: int = 512, # total latent dim
338
+ decay: float = 0.99,
339
+ epsilon: float = 1e-5
340
+ ):
341
+ super().__init__()
342
+
343
+ self.num_groups = num_groups
344
+ self.num_embeddings = num_embeddings
345
+ self.embedding_dim = embedding_dim
346
+ self.group_dim = embedding_dim // num_groups
347
+
348
+ # MUST be named 'quantizers' to match checkpoint keys
349
+ self.quantizers = nn.ModuleList([
350
+ VectorQuantizerEMA(num_embeddings, self.group_dim, decay, epsilon)
351
+ for _ in range(num_groups)
352
+ ])
353
+
354
+ def forward(self, z: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
355
+ """
356
+ Args:
357
+ z: (B, D, T) latent features
358
+ Returns:
359
+ z_q: (B, D, T) quantized features
360
+ indices: (num_groups, B, T) codebook indices per group
361
+ """
362
+ B, D, T = z.shape
363
+
364
+ # Split into groups
365
+ z_groups = z.chunk(self.num_groups, dim=1) # list of (B, group_dim, T)
366
+
367
+ z_q_groups = []
368
+ indices_list = []
369
+
370
+ for i, (z_g, quantizer) in enumerate(zip(z_groups, self.quantizers)):
371
+ z_q_g, idx_g = quantizer(z_g)
372
+ z_q_groups.append(z_q_g)
373
+ indices_list.append(idx_g)
374
+
375
+ # Concatenate quantized groups
376
+ z_q = torch.cat(z_q_groups, dim=1)
377
+
378
+ # Stack indices: list of (B, T) -> (num_groups, B, T)
379
+ indices = torch.stack(indices_list, dim=0)
380
+
381
+ return z_q, indices
382
+
383
+ def decode_indices(self, indices: torch.Tensor) -> torch.Tensor:
384
+ """
385
+ Args:
386
+ indices: (num_groups, B, T) codebook indices
387
+ Returns:
388
+ z_q: (B, D, T) quantized features
389
+ """
390
+ z_q_groups = []
391
+ for i, quantizer in enumerate(self.quantizers):
392
+ z_q_g = quantizer.decode_indices(indices[i])
393
+ z_q_groups.append(z_q_g)
394
+
395
+ z_q = torch.cat(z_q_groups, dim=1)
396
+ return z_q
397
+
398
+
399
+ # =============================================================================
400
+ # Main Model
401
+ # =============================================================================
402
+
403
+ class MotionVQVAE(nn.Module):
404
+ """Motion Vector Quantized VAE for HuggingFace.
405
+
406
+ Architecture matches the original training code exactly to ensure
407
+ checkpoint compatibility.
408
+ """
409
+
410
+ def __init__(self, config: Optional[Dict[str, Any]] = None):
411
+ super().__init__()
412
+
413
+ # Default config
414
+ if config is None:
415
+ config = {}
416
+
417
+ self.config = config
418
+
419
+ # Model parameters
420
+ # Support both naming conventions for config keys
421
+ self.motion_dim = config.get('motion_dim', config.get('input_dim', 272))
422
+ self.latent_dim = config.get('latent_dim', config.get('code_dim', 512))
423
+ self.num_groups = config.get('num_groups', 64)
424
+ self.num_codes = config.get('num_codes', config.get('nb_code', 512))
425
+ self.down_t = config.get('down_t', 2)
426
+ self.stride_t = config.get('stride_t', 2)
427
+ self.width = config.get('width', 512)
428
+ self.depth = config.get('depth', 3)
429
+ self.dilation_growth_rate = config.get('dilation_growth_rate', 3)
430
+ self.activation = config.get('activation', 'relu')
431
+ self.kernel_size = config.get('kernel_size', 3)
432
+
433
+ # Normalization stats (loaded separately)
434
+ self.register_buffer('mean', torch.zeros(self.motion_dim))
435
+ self.register_buffer('std', torch.ones(self.motion_dim))
436
+
437
+ # Build model components - names must match checkpoint
438
+ self.encoder = Encoder(
439
+ input_emb_width=self.motion_dim,
440
+ output_emb_width=self.latent_dim,
441
+ down_t=self.down_t,
442
+ stride_t=self.stride_t,
443
+ width=self.width,
444
+ depth=self.depth,
445
+ dilation_growth_rate=self.dilation_growth_rate,
446
+ activation=self.activation,
447
+ kernel_size=self.kernel_size
448
+ )
449
+
450
+ self.decoder = Decoder(
451
+ input_emb_width=self.motion_dim,
452
+ output_emb_width=self.latent_dim,
453
+ down_t=self.down_t,
454
+ stride_t=self.stride_t,
455
+ width=self.width,
456
+ depth=self.depth,
457
+ dilation_growth_rate=self.dilation_growth_rate,
458
+ activation=self.activation,
459
+ kernel_size=self.kernel_size
460
+ )
461
+
462
+ self.quantizer = MultiGroupVectorQuantizer(
463
+ num_groups=self.num_groups,
464
+ num_embeddings=self.num_codes,
465
+ embedding_dim=self.latent_dim
466
+ )
467
+
468
+ def normalize(self, motion: torch.Tensor) -> torch.Tensor:
469
+ """Normalize motion data using mean and std."""
470
+ # motion: (B, T, D) or (B, D, T)
471
+ mean = self.mean.view(1, 1, -1)
472
+ std = self.std.view(1, 1, -1)
473
+ std_safe = torch.clamp(std, min=0.01)
474
+
475
+ if motion.shape[-1] != self.motion_dim:
476
+ # (B, D, T) format
477
+ mean = mean.permute(0, 2, 1)
478
+ std_safe = std_safe.permute(0, 2, 1)
479
+
480
+ normalized = (motion - mean) / std_safe
481
+ return torch.clamp(normalized, -20, 20)
482
+
483
+ def denormalize(self, motion: torch.Tensor) -> torch.Tensor:
484
+ """Denormalize motion data using mean and std."""
485
+ mean = self.mean.view(1, 1, -1)
486
+ std = self.std.view(1, 1, -1)
487
+ std_safe = torch.clamp(std, min=0.01)
488
+
489
+ if motion.shape[-1] != self.motion_dim:
490
+ # (B, D, T) format
491
+ mean = mean.permute(0, 2, 1)
492
+ std_safe = std_safe.permute(0, 2, 1)
493
+
494
+ return motion * std_safe + mean
495
+
496
+ def encode(self, motion: torch.Tensor, normalize: bool = True) -> torch.Tensor:
497
+ """
498
+ Encode motion to discrete tokens.
499
+
500
+ Args:
501
+ motion: (B, T, D) motion data where D=272
502
+ normalize: whether to normalize input
503
+
504
+ Returns:
505
+ tokens: (num_groups, B, T') discrete tokens where T' = T // 4
506
+ """
507
+ # Normalize if needed
508
+ if normalize:
509
+ motion = self.normalize(motion)
510
+
511
+ # Convert to (B, D, T) for conv layers
512
+ x = motion.permute(0, 2, 1)
513
+
514
+ # Encode
515
+ z = self.encoder(x)
516
+
517
+ # Quantize
518
+ _, indices = self.quantizer(z)
519
+
520
+ return indices
521
+
522
+ def decode(self, tokens: torch.Tensor, denormalize: bool = True) -> torch.Tensor:
523
+ """
524
+ Decode discrete tokens to motion.
525
+
526
+ Args:
527
+ tokens: (num_groups, B, T') discrete tokens
528
+ denormalize: whether to denormalize output
529
+
530
+ Returns:
531
+ motion: (B, T, D) reconstructed motion
532
+ """
533
+ # Decode tokens to latent
534
+ z_q = self.quantizer.decode_indices(tokens)
535
+
536
+ # Decode latent to motion
537
+ x_recon = self.decoder(z_q)
538
+
539
+ # Convert to (B, T, D)
540
+ motion = x_recon.permute(0, 2, 1)
541
+
542
+ # Denormalize if needed
543
+ if denormalize:
544
+ motion = self.denormalize(motion)
545
+
546
+ return motion
547
+
548
+ def forward(
549
+ self,
550
+ motion: torch.Tensor,
551
+ normalize: bool = True,
552
+ denormalize: bool = True
553
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
554
+ """
555
+ Full forward pass: encode to tokens and decode back.
556
+
557
+ Args:
558
+ motion: (B, T, D) motion data
559
+ normalize: whether to normalize input
560
+ denormalize: whether to denormalize output
561
+
562
+ Returns:
563
+ motion_recon: (B, T, D) reconstructed motion
564
+ tokens: (num_groups, B, T') discrete tokens
565
+ """
566
+ # Normalize if needed
567
+ if normalize:
568
+ motion_normalized = self.normalize(motion)
569
+ else:
570
+ motion_normalized = motion
571
+
572
+ # Convert to (B, D, T) for conv layers
573
+ x = motion_normalized.permute(0, 2, 1)
574
+
575
+ # Encode
576
+ z = self.encoder(x)
577
+
578
+ # Quantize
579
+ z_q, indices = self.quantizer(z)
580
+
581
+ # Decode
582
+ x_recon = self.decoder(z_q)
583
+
584
+ # Convert to (B, T, D)
585
+ motion_recon = x_recon.permute(0, 2, 1)
586
+
587
+ # Denormalize if needed
588
+ if denormalize:
589
+ motion_recon = self.denormalize(motion_recon)
590
+
591
+ return motion_recon, indices
592
+
593
+ @classmethod
594
+ def from_pretrained(
595
+ cls,
596
+ pretrained_path: str,
597
+ device: Optional[str] = None,
598
+ **kwargs
599
+ ) -> "MotionVQVAE":
600
+ """
601
+ Load pretrained model from HuggingFace Hub or local path.
602
+
603
+ Args:
604
+ pretrained_path: HuggingFace repo ID (e.g., "khania/motion-vqvae")
605
+ or local directory path
606
+ device: Device to load model on ('cuda', 'cpu', or None for auto)
607
+ **kwargs: Additional arguments passed to model initialization
608
+
609
+ Returns:
610
+ Loaded MotionVQVAE model
611
+ """
612
+ # Determine if path is HF repo or local
613
+ if os.path.isdir(pretrained_path):
614
+ model_dir = pretrained_path
615
+ elif HF_HUB_AVAILABLE:
616
+ model_dir = snapshot_download(repo_id=pretrained_path)
617
+ else:
618
+ raise ValueError(
619
+ f"Path {pretrained_path} is not a local directory and "
620
+ "huggingface_hub is not installed. Install with: pip install huggingface_hub"
621
+ )
622
+
623
+ # Load config
624
+ config_path = os.path.join(model_dir, "config.json")
625
+ if os.path.exists(config_path):
626
+ with open(config_path, 'r') as f:
627
+ config = json.load(f)
628
+ else:
629
+ config = {}
630
+
631
+ # Override config with kwargs
632
+ config.update(kwargs)
633
+
634
+ # Create model
635
+ model = cls(config)
636
+
637
+ # Load weights (includes mean/std buffers if converted with updated script)
638
+ weights_path = os.path.join(model_dir, "pytorch_model.bin")
639
+ if os.path.exists(weights_path):
640
+ state_dict = torch.load(weights_path, map_location='cpu')
641
+
642
+ # Load state dict
643
+ missing, unexpected = model.load_state_dict(state_dict, strict=False)
644
+
645
+ # Filter out expected missing keys (mean/std might be in separate files for older checkpoints)
646
+ missing_filtered = [k for k in missing if k not in ['mean', 'std']]
647
+
648
+ if missing_filtered:
649
+ print(f"Warning: Missing keys in state_dict: {missing_filtered}")
650
+ if unexpected:
651
+ print(f"Warning: Unexpected keys in state_dict: {unexpected}")
652
+ else:
653
+ raise ValueError(f"Model weights not found at {weights_path}")
654
+
655
+ # Fallback: Load mean/std from numpy files if not in state_dict
656
+ # This supports both old format (separate files) and new format (in weights)
657
+ mean_path = os.path.join(model_dir, "mean.npy")
658
+ std_path = os.path.join(model_dir, "std.npy")
659
+
660
+ if 'mean' not in state_dict and os.path.exists(mean_path):
661
+ model.mean = torch.from_numpy(np.load(mean_path)).float()
662
+ if 'std' not in state_dict and os.path.exists(std_path):
663
+ model.std = torch.from_numpy(np.load(std_path)).float()
664
+
665
+ # Move to device
666
+ if device is None:
667
+ device = 'cuda' if torch.cuda.is_available() else 'cpu'
668
+ model = model.to(device)
669
+ model.eval()
670
+
671
+ return model
672
+
673
+ def save_pretrained(self, save_dir: str):
674
+ """
675
+ Save model to directory in HuggingFace format.
676
+
677
+ Args:
678
+ save_dir: Directory to save model to
679
+ """
680
+ os.makedirs(save_dir, exist_ok=True)
681
+
682
+ # Save config
683
+ config_path = os.path.join(save_dir, "config.json")
684
+ with open(config_path, 'w') as f:
685
+ json.dump(self.config, f, indent=2)
686
+
687
+ # Save normalization stats
688
+ mean_path = os.path.join(save_dir, "mean.npy")
689
+ std_path = os.path.join(save_dir, "std.npy")
690
+ np.save(mean_path, self.mean.cpu().numpy())
691
+ np.save(std_path, self.std.cpu().numpy())
692
+
693
+ # Save weights
694
+ weights_path = os.path.join(save_dir, "pytorch_model.bin")
695
+ torch.save(self.state_dict(), weights_path)
696
+
697
+ print(f"Model saved to {save_dir}")
698
+
699
+
700
+ # =============================================================================
701
+ # Utility Functions
702
+ # =============================================================================
703
+
704
+ def load_motion_vqvae(
705
+ pretrained_path: str = "khania/motion-vqvae",
706
+ device: Optional[str] = None
707
+ ) -> MotionVQVAE:
708
+ """
709
+ Convenience function to load MotionVQVAE.
710
+
711
+ Args:
712
+ pretrained_path: HuggingFace repo ID or local path
713
+ device: Device to load on
714
+
715
+ Returns:
716
+ Loaded model
717
+ """
718
+ return MotionVQVAE.from_pretrained(pretrained_path, device=device)
719
+
720
+
721
+ if __name__ == "__main__":
722
+ # Test model creation and forward pass
723
+ print("Testing MotionVQVAE...")
724
+
725
+ config = {
726
+ 'motion_dim': 272,
727
+ 'latent_dim': 512,
728
+ 'num_groups': 64,
729
+ 'num_codes': 512,
730
+ 'down_t': 2,
731
+ 'stride_t': 2,
732
+ 'width': 512,
733
+ 'depth': 3,
734
+ 'dilation_growth_rate': 3,
735
+ 'activation': 'relu',
736
+ 'kernel_size': 3
737
+ }
738
+
739
+ model = MotionVQVAE(config)
740
+ print(f"Model created with {sum(p.numel() for p in model.parameters()):,} parameters")
741
+
742
+ # Print model architecture for debugging
743
+ print("\nModel state_dict keys:")
744
+ for k in sorted(model.state_dict().keys())[:20]:
745
+ print(f" {k}")
746
+ print(" ...")
747
+
748
+ # Test forward pass
749
+ batch_size = 2
750
+ seq_len = 64
751
+ motion = torch.randn(batch_size, seq_len, 272)
752
+
753
+ model.eval()
754
+ with torch.no_grad():
755
+ motion_recon, tokens = model(motion, normalize=False, denormalize=False)
756
+
757
+ print(f"\nInput shape: {motion.shape}")
758
+ print(f"Output shape: {motion_recon.shape}")
759
+ print(f"Tokens shape: {tokens.shape}")
760
+ print(f"MSE (random weights): {F.mse_loss(motion, motion_recon).item():.4f}")
pytorch_model.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:eaee73dd7bd25dbd49320fa242bea134efde8ad12b3a64974ea1154b8bfa2d2c
3
+ size 293120435
std.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:86c79a66805f80a5219047235536aee339de3accc4aa6de4a1857ff6ff61fc41
3
+ size 1216