zy22b commited on
Commit
6f48477
·
verified ·
1 Parent(s): d0a3d5e

initial commit

Browse files
.gitattributes CHANGED
@@ -1,35 +1 @@
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
  *.safetensors filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,3 +1,371 @@
1
  ---
2
  license: mit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  license: mit
3
+ language:
4
+ - en
5
+ tags:
6
+ - motion
7
+ - human-motion
8
+ - text-to-motion
9
+ - motion-to-text
10
+ - motion-captioning
11
+ - motion-generation
12
+ - autoencoder
13
+ - gpt2
14
+ datasets:
15
+ - humanml3d
16
+ pipeline_tag: text-generation
17
+ library_name: transformers
18
+ base_model:
19
+ - openai-community/gpt2
20
  ---
21
+
22
+ # MUGEN
23
+
24
+ **A Unified Framework for Efficient Motion Understanding and Generation**
25
+
26
+ MUGEN is a unified motion-language model. One model turns a description into
27
+ human motion, and describes an observed motion in words. Both directions share a
28
+ single motion representation, and that representation is continuous: **no
29
+ codebook, one draw.**
30
+
31
+ - 📦 Code: [github.com/JYe16/MUGEN](https://github.com/JYe16/MUGEN)
32
+ - 🏋️ This checkpoint: HumanML3D, **K = 2** latent slots, 183.7M parameters
33
+
34
+ ---
35
+
36
+ ## What makes it different
37
+
38
+ Unified motion-language systems have traditionally coupled generation and
39
+ understanding through a shared *discrete* motion codebook, but quantization
40
+ limits generation quality. The strongest generators buy that quality back at
41
+ growing cost: stacked residual codebooks enlarge the representation, and masked
42
+ decoding stages, long autoregressive rollouts and denoising chains of tens to
43
+ hundreds of steps stretch inference. None of that decoding machinery serves
44
+ understanding.
45
+
46
+ MUGEN pays neither cost. Generating a motion costs **K language-model steps, one
47
+ draw, and one decoder pass** — with K = 2 here, that is two rollout steps.
48
+
49
+ ## Architecture
50
+
51
+ ```
52
+ MugenForConditionalGeneration 183,690,603 params
53
+ ├── motion_autoencoder (AdaptiveLengthAutoEncoder) 48.9M
54
+ │ ├── encoder_backbone dilated Conv1d ResNet, per-frame features
55
+ │ ├── latent_queries K learnable queries -> the K slots
56
+ │ ├── encoder_blocks 4 x cross-attention (frames -> slots)
57
+ │ ├── decoder_blocks 4 x cross-attention (slots -> frames)
58
+ │ └── decoder_refiner dilated Conv1d ResNet -> 263-d motion
59
+ ├── language_model (GPT2LMHeadModel) 124.4M
60
+ │ └── 12 layers, 768 hidden, vocab 50261 (GPT-2 + <MOT> + 3 reserved)
61
+ ├── layer router 9.1M
62
+ │ ├── text_mem_proj prompt tokens -> routing memory
63
+ │ ├── cross_attn_blocks 2 x cross-attention, per-slot routing queries
64
+ │ ├── static_router_logits (K, 12) text-independent component
65
+ │ └── layer_router MLP -> (K, 12) text-conditional component
66
+ ├── calibrated latent head 0.9M
67
+ │ ├── projector hidden -> (mu, logvar) per slot
68
+ │ ├── latent_factors (K*512, 64) global covariance basis U
69
+ │ └── factor_scale_head hidden -> per-factor log amplitudes
70
+ └── motion_in_projector latent slots -> GPT-2 embeddings (captioning) 0.4M
71
+ ```
72
+
73
+ **Adaptive-length autoencoder.** Cross-attention compresses a clip of *any*
74
+ length into K = 2 continuous vectors of width 512, and a second cross-attention
75
+ stack expands them back to *any* requested frame count. Decoder queries encode a
76
+ frame's relative phase within the clip rather than an absolute frame index, which
77
+ is why one decoder serves every length, and why you can decode the same latents
78
+ at 60 frames and at 200 frames.
79
+
80
+ **Depth-routed hidden states.** A text-conditioned router gives each latent slot
81
+ its own soft mixture over all twelve transformer layers, so a slot reads from the
82
+ depth it needs instead of squeezing every piece of motion evidence through the
83
+ final layer. Both halves of the routing logit are tanh-bounded, so no logit
84
+ margin can saturate the routing softmax.
85
+
86
+ **Calibrated latent head.** The head predicts
87
+ `N(mu, U diag(a)^2 U^T + diag(sigma^2))` over the whole flattened `K x 512`
88
+ latent, with rank 64, trained by exact maximum likelihood. One draw therefore
89
+ carries text-conditional variance that is *correlated across slots*, which is
90
+ what a single-step sampler has to supply all at once.
91
+
92
+ | | |
93
+ |---|---|
94
+ | Motion features | HumanML3D, 263-d per frame, 20 fps, 22 joints |
95
+ | Latent slots | K = 2, width 512 |
96
+ | Language model | GPT-2 (124M), fine-tuned |
97
+ | Covariance rank | 64 |
98
+ | Training | Joint generation + understanding, 240 epochs (Stage 2) |
99
+ | Parameters | 183,690,603 |
100
+ | Precision | float32 |
101
+
102
+ ---
103
+
104
+ ## Installation
105
+
106
+ ```bash
107
+ pip install torch transformers safetensors numpy
108
+ ```
109
+
110
+ `trust_remote_code=True` is required: the autoencoder, the router and the
111
+ calibrated head are custom modules that ship with this repository.
112
+
113
+ **Nothing else is needed.** The model carries its own tokenizer and its own
114
+ HumanML3D feature statistics, so it returns motion in real units without you
115
+ downloading the dataset.
116
+
117
+ ---
118
+
119
+ ## Quick start
120
+
121
+ ```python
122
+ import torch
123
+ from transformers import AutoModelForCausalLM, AutoTokenizer
124
+
125
+ model = AutoModelForCausalLM.from_pretrained(
126
+ "zy22b/MUGEN", trust_remote_code=True
127
+ ).eval()
128
+ tokenizer = AutoTokenizer.from_pretrained("zy22b/MUGEN")
129
+
130
+ features = model.generate_motion(
131
+ ["a person walks forward and then waves with the right hand."],
132
+ lengths=[120], # frames at 20 fps, so 6 seconds
133
+ tokenizer=tokenizer,
134
+ )
135
+ print(features.shape) # torch.Size([1, 120, 263])
136
+
137
+ joints = model.features_to_joints(features)
138
+ print(joints.shape) # torch.Size([1, 120, 22, 3]) metres
139
+
140
+ print(model.generate_caption(features, tokenizer=tokenizer))
141
+ # ['a person walks forward, turns to the left, and walks back.']
142
+ ```
143
+
144
+ That last line captions the motion the model just generated, so it changes from
145
+ call to call: the motion is *sampled*, not a fixed function of the prompt.
146
+
147
+ ---
148
+
149
+ ## Inference tutorial
150
+
151
+ ### 1. Text to motion
152
+
153
+ ```python
154
+ prompts = [
155
+ "a person walks forward and then waves with the right hand.",
156
+ "the person jumps up and lands with both feet together.",
157
+ "a man slowly sits down on a chair.",
158
+ "a person runs in a circle to the left.",
159
+ ]
160
+
161
+ features = model.generate_motion(prompts, lengths=120, tokenizer=tokenizer)
162
+ ```
163
+
164
+ `generate_motion` returns `(batch, frames, 263)` denormalised HumanML3D features.
165
+
166
+ | Argument | Meaning |
167
+ |---|---|
168
+ | `texts` | one description, or a list of them |
169
+ | `lengths` | frames per description at 20 fps; an `int` applies to the whole batch |
170
+ | `temperature` | multiplier on the sampled perturbation; defaults to the calibrated `1.0` |
171
+ | `generator` | a `torch.Generator` for a reproducible draw |
172
+ | `denormalize` | `True` (default) returns raw HumanML3D units |
173
+
174
+ **Choosing a length.** HumanML3D clips are multiples of 4 frames and at most 196
175
+ (9.8 s), so stay in `[20, 196]` and on the multiple-of-4 grid to stay in
176
+ distribution. The decoder will happily produce other lengths, but you are then
177
+ outside what it was trained on.
178
+
179
+ **Per-row lengths.** Because decoder queries are relative-phase, a row must be
180
+ decoded at its own length rather than cropped from a longer one. Pass a list and
181
+ the model handles it; the returned tensor is zero-padded to the longest row.
182
+
183
+ ```python
184
+ features = model.generate_motion(prompts[:2], lengths=[60, 196], tokenizer=tokenizer)
185
+ # (2, 196, 263); row 0 is valid up to frame 60
186
+ ```
187
+
188
+ ### 2. Temperature, and why the default is 1.0
189
+
190
+ The head predicts a distribution, and `temperature` scales the whole zero-mean
191
+ perturbation around its mean.
192
+
193
+ ```python
194
+ # The calibrated conditional distribution. This is the protocol every reported
195
+ # number uses, and different calls give different motions for the same prompt.
196
+ features = model.generate_motion(prompts, lengths=120, tokenizer=tokenizer)
197
+
198
+ # The distribution mean. Deterministic, and a *regression* protocol: numbers
199
+ # obtained this way are not comparable to generative ones.
200
+ mean_motion = model.generate_motion(prompts, lengths=120, tokenizer=tokenizer,
201
+ temperature=0.0)
202
+
203
+ # Reproducible sampling.
204
+ g = torch.Generator().manual_seed(1234)
205
+ features = model.generate_motion(prompts, lengths=120, tokenizer=tokenizer, generator=g)
206
+ ```
207
+
208
+ Sampling at temperature 1.0 is what makes the same prompt yield genuinely
209
+ different, plausible motions. Captioning five seeded draws of the same prompt
210
+ shows how far apart they land:
211
+
212
+ ```
213
+ prompt: "a person walks forward and then waves with the right hand."
214
+ seed 0 a person walks forward and then turns around and walks back.
215
+ seed 1 a person walks forward and reaches out with their right hand.
216
+ seed 2 a person walks forward and then waves with their left hand.
217
+ seed 3 a person walks forward and puts their right hand on their head.
218
+ seed 4 a person walks forward and then puts something on a counter.
219
+ ```
220
+
221
+ Lowering the temperature trades that diversity for proximity to the conditional
222
+ mean.
223
+
224
+ ### 3. Motion to text
225
+
226
+ ```python
227
+ import numpy as np
228
+
229
+ motion = np.load("000004.npy") # (T, 263) HumanML3D features
230
+ clip = torch.from_numpy(motion).float().unsqueeze(0)
231
+
232
+ print(model.generate_caption(clip, tokenizer=tokenizer))
233
+ # ['a person bends over to touch the ground with their hands, then stands up straight.']
234
+ ```
235
+
236
+ Pass `normalized=True` if your features are already standardised by the dataset
237
+ statistics. Captioning is greedy by default; `max_new_tokens` and `num_beams`
238
+ override the config values.
239
+
240
+ ### 4. The shared latent interface
241
+
242
+ The K slots are the only motion representation in the system: the language model
243
+ generates them for text-to-motion and reads these same vectors back for
244
+ captioning. You can work with them directly.
245
+
246
+ ```python
247
+ latents = model.encode_motion(clip) # (1, 2, 512): a whole clip, two vectors
248
+ print(latents.shape)
249
+
250
+ # Decode at the original length, and at a different one.
251
+ same = model.decode_motion(latents, clip.shape[1])
252
+ longer = model.decode_motion(latents, 200)
253
+
254
+ # Caption straight from latents, skipping the encoder.
255
+ print(model.generate_caption(latents=latents, tokenizer=tokenizer))
256
+
257
+ # Text -> latents, without decoding.
258
+ z = model.text_to_latents(["a person waves."], tokenizer=tokenizer)
259
+ ```
260
+
261
+ Interpolating between two clips is then a two-line operation:
262
+
263
+ ```python
264
+ a = model.encode_motion(clip_a)
265
+ b = model.encode_motion(clip_b)
266
+ blend = model.decode_motion(0.5 * a + 0.5 * b, 120)
267
+ ```
268
+
269
+ ### 5. Running on GPU, and in batches
270
+
271
+ ```python
272
+ model = model.to("cuda")
273
+ features = model.generate_motion(prompts, lengths=120, tokenizer=tokenizer) # follows the model
274
+ ```
275
+
276
+ Batched prompts are left-padded, which reproduces the training and evaluation
277
+ behaviour exactly. Single-prompt calls involve no padding at all.
278
+
279
+ ### 6. Saving and rendering
280
+
281
+ `features_to_joints` gives `(batch, frames, 22, 3)` positions in metres, on the
282
+ HumanML3D skeleton, with the y axis vertical.
283
+
284
+ ```python
285
+ joints = model.features_to_joints(features)
286
+ np.save("sample.npy", joints[0].cpu().numpy())
287
+ ```
288
+
289
+ For animation, feed those positions to any HumanML3D-compatible renderer. The
290
+ [code repository](https://github.com/JYe16/MUGEN) includes
291
+ `motGPT/utils/render_utils.py`, which writes an mp4 directly from a `[T, J, 3]`
292
+ array.
293
+
294
+ ### 7. Sanity check
295
+
296
+ If you swap in different feature statistics, the failure is silent: shapes stay
297
+ right and the numbers look plausible while the whole skeleton is mis-scaled. One
298
+ assertion catches it.
299
+
300
+ ```python
301
+ joints = model.features_to_joints(features)
302
+ high, low = float(joints[..., 1].max()), float(joints[..., 1].min())
303
+ assert 1.0 < high < 2.5 and -0.5 < low < 0.5, "implausible body height"
304
+ ```
305
+
306
+ ---
307
+
308
+ ## Reproducibility
309
+
310
+ The inference path in `modeling_mugen.py` was verified against the original
311
+ training code on the same checkpoint. Routed hidden states, `mu`, `logvar`, the
312
+ factor log-amplitudes, the autoencoder encode and decode outputs, and a
313
+ seeded draw all agree to **`max|diff| = 0.000e+00`**, and generated captions match
314
+ token for token.
315
+
316
+ Two consequences worth knowing:
317
+
318
+ - A given seed reproduces the same draw here as it does in the research code,
319
+ because the factor noise is drawn before the diagonal noise in both.
320
+ - `router_eval_tau` (1.5) is the converged end of the training temperature
321
+ anneal. Changing it changes which transformer depth each slot reads from, so
322
+ leave it alone unless you are deliberately studying the router.
323
+
324
+ Tested with `transformers` 4.47 and PyTorch 2.x. The rollout passes explicit
325
+ `position_ids`, so behaviour does not drift with the transformers version.
326
+
327
+ ## Limitations
328
+
329
+ - Trained on **HumanML3D** only: everyday single-person motion, English
330
+ descriptions, 20 fps, 22 joints. Multi-person interaction, object manipulation,
331
+ and highly stylised motion are out of distribution.
332
+ - Length is an **input**, not something the model infers from the text. Ask for
333
+ 120 frames and you get 120 frames whether or not the description warrants it.
334
+ - K = 2 slots is a deliberately tight budget, chosen for this dataset. It is not
335
+ a universal setting: the SnapMoGen model in the code repository uses K = 4, and
336
+ the best budget is dataset-dependent.
337
+ - Captions come from a fine-tuned GPT-2 (124M) and are short and generic
338
+ relative to human references. They describe the dominant action reliably and
339
+ fine detail unreliably. On HumanML3D clip `000004`, whose reference reads *"a
340
+ man is pretending to be a chicken, constantly pecking at the ground and waving
341
+ his arms like a chicken"*, the model produces *"a person bends over to touch
342
+ the ground with their hands, then stands up straight"*: the gross movement is
343
+ right, the intent and the arm motion are gone.
344
+ - Not a safety-filtered model. It generates skeletal motion and short English
345
+ captions, and it has no content filtering of any kind.
346
+
347
+ ## Citation
348
+
349
+ The paper is under review. A citation entry will be added here once it appears;
350
+ in the meantime please cite the repository and this model card.
351
+
352
+ ```bibtex
353
+ @misc{mugen2026,
354
+ title = {MUGEN: A Unified Framework for Efficient Motion Understanding and Generation},
355
+ note = {Under review. Code: https://github.com/JYe16/MUGEN,
356
+ model: https://huggingface.co/zy22b/MUGEN},
357
+ year = {2026}
358
+ }
359
+ ```
360
+
361
+ ## Acknowledgements
362
+
363
+ Built on [HumanML3D](https://github.com/EricGuo5513/HumanML3D),
364
+ [MotionGPT](https://github.com/OpenMotionLab/MotionGPT),
365
+ [MLD](https://github.com/ChenFengYe/motion-latent-diffusion), and our earlier
366
+ discrete-codebook system
367
+ [GeoMotionGPT](https://huggingface.co/zy22b/GeoMotionGPT).
368
+
369
+ ## License
370
+
371
+ MIT
added_tokens.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "<MOT>": 50260,
3
+ "<motion_id_0>": 50257,
4
+ "<motion_id_1>": 50258,
5
+ "<motion_id_2>": 50259
6
+ }
config.json ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "activation_function": "gelu_new",
3
+ "alae_activation": "gelu",
4
+ "alae_depth": 3,
5
+ "alae_dilation_growth_rate": 3,
6
+ "alae_dim_feedforward": 2048,
7
+ "alae_dropout": 0.05,
8
+ "alae_hidden_dim": 512,
9
+ "alae_max_decode_len": 256,
10
+ "alae_nhead": 8,
11
+ "alae_norm": null,
12
+ "alae_num_decoder_layers": 4,
13
+ "alae_num_encoder_layers": 4,
14
+ "alae_num_res_blocks": 2,
15
+ "architectures": [
16
+ "MugenForConditionalGeneration"
17
+ ],
18
+ "attn_pdrop": 0.1,
19
+ "auto_map": {
20
+ "AutoConfig": "configuration_mugen.MugenConfig",
21
+ "AutoModelForCausalLM": "modeling_mugen.MugenForConditionalGeneration"
22
+ },
23
+ "bos_token_id": 50256,
24
+ "embd_pdrop": 0.1,
25
+ "eos_token_id": 50256,
26
+ "eval_sample_temperature": 1.0,
27
+ "fps": 20,
28
+ "initializer_range": 0.02,
29
+ "k_latent_slots": 2,
30
+ "latent_dim": 512,
31
+ "latent_low_rank": 64,
32
+ "layer_norm_epsilon": 1e-05,
33
+ "length_multiple": 4,
34
+ "m2t_max_new_tokens": 64,
35
+ "m2t_num_beams": 1,
36
+ "m2t_prompt_prefix": "Please describe the following human motion using plain text:",
37
+ "model_type": "mugen",
38
+ "mot_token": "<MOT>",
39
+ "mot_token_id": 50260,
40
+ "motion_input_dim": 263,
41
+ "n_embd": 768,
42
+ "n_head": 12,
43
+ "n_inner": null,
44
+ "n_layer": 12,
45
+ "n_positions": 1024,
46
+ "num_cross_attn_layers": 2,
47
+ "num_joints": 22,
48
+ "pad_token_id": 50256,
49
+ "resid_pdrop": 0.1,
50
+ "router_delta_scale": 4.0,
51
+ "router_eval_tau": 1.5,
52
+ "router_hidden": 512,
53
+ "router_static_scale": 4.0,
54
+ "t2m_prompt_template": "Please generate human motion based on the following textual description: {text} {mot}",
55
+ "torch_dtype": "float32",
56
+ "transformers_version": "4.51.0",
57
+ "vocab_size": 50261
58
+ }
configuration_mugen.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MUGEN configuration.
2
+
3
+ MUGEN couples an adaptive-length motion autoencoder (ALAE) with a GPT-2
4
+ language model. The autoencoder compresses a motion clip of any length into K
5
+ continuous latent slots, and those slots are the only motion representation in
6
+ the system: the language model generates them for text-to-motion and reads
7
+ them back for motion understanding (captioning).
8
+ """
9
+
10
+ from transformers import PretrainedConfig
11
+
12
+
13
+ class MugenConfig(PretrainedConfig):
14
+ """Configuration for :class:`MugenForConditionalGeneration`.
15
+
16
+ Args:
17
+ motion_input_dim (`int`, *optional*, defaults to 263):
18
+ Dimensionality of one motion frame. 263 is the HumanML3D feature
19
+ layout (root velocities, RIC joint positions, 6D rotations, foot
20
+ contacts).
21
+ k_latent_slots (`int`, *optional*, defaults to 2):
22
+ Number of continuous latent slots K. The whole clip, whatever its
23
+ length, is represented by exactly K vectors.
24
+ latent_dim (`int`, *optional*, defaults to 512):
25
+ Width of one latent slot.
26
+ alae_hidden_dim (`int`, *optional*, defaults to 512):
27
+ Width of the autoencoder's convolutional trunk.
28
+ alae_depth (`int`, *optional*, defaults to 3):
29
+ Number of dilated convolutions inside one residual block.
30
+ alae_dilation_growth_rate (`int`, *optional*, defaults to 3):
31
+ Dilation growth factor across the residual stack.
32
+ alae_activation (`str`, *optional*, defaults to `"gelu"`):
33
+ Activation used throughout the autoencoder.
34
+ alae_norm (`str`, *optional*, defaults to `None`):
35
+ Normalisation inside the residual blocks (`None`, `"LN"`, `"GN"`,
36
+ `"BN"`).
37
+ alae_num_res_blocks (`int`, *optional*, defaults to 2):
38
+ Residual blocks in the encoder trunk and the decoder refiner.
39
+ alae_num_encoder_layers (`int`, *optional*, defaults to 4):
40
+ Cross-attention blocks that compress frames into the K slots.
41
+ alae_num_decoder_layers (`int`, *optional*, defaults to 4):
42
+ Cross-attention blocks that expand the K slots back to frames.
43
+ alae_nhead (`int`, *optional*, defaults to 8):
44
+ Attention heads in the autoencoder's cross-attention blocks.
45
+ alae_dim_feedforward (`int`, *optional*, defaults to 2048):
46
+ Feed-forward width in those blocks.
47
+ alae_dropout (`float`, *optional*, defaults to 0.05):
48
+ Dropout in those blocks (inactive at inference).
49
+ alae_max_decode_len (`int`, *optional*, defaults to 256):
50
+ Size of the decoder's positional table. Query positions are spread
51
+ across the whole table so a position encodes the frame's relative
52
+ phase in the clip rather than an absolute frame index, which is
53
+ what lets one decoder serve every clip length.
54
+ vocab_size (`int`, *optional*, defaults to 50261):
55
+ Language-model vocabulary: GPT-2's 50257 plus three reserved
56
+ motion-id slots and the `<MOT>` seed token.
57
+ n_positions, n_embd, n_layer, n_head, n_inner, activation_function,
58
+ resid_pdrop, embd_pdrop, attn_pdrop, layer_norm_epsilon,
59
+ initializer_range:
60
+ Standard GPT-2 hyperparameters.
61
+ num_cross_attn_layers (`int`, *optional*, defaults to 2):
62
+ Cross-attention blocks in the layer router's text encoder.
63
+ router_hidden (`int`, *optional*, defaults to 512):
64
+ Hidden width of the router MLP.
65
+ router_static_scale (`float`, *optional*, defaults to 4.0):
66
+ Bound on the router's text-independent logit component.
67
+ router_delta_scale (`float`, *optional*, defaults to 4.0):
68
+ Bound on the router's text-conditional logit component. Both parts
69
+ are tanh-capped, so no logit margin can saturate the softmax.
70
+ router_eval_tau (`float`, *optional*, defaults to 1.5):
71
+ Inference temperature of the routing softmax. This is the converged
72
+ end of the training anneal; changing it changes which transformer
73
+ depth each slot reads from.
74
+ latent_low_rank (`int`, *optional*, defaults to 64):
75
+ Rank r of the calibrated head's covariance. The head predicts
76
+ `N(mu, U diag(a)^2 U^T + diag(sigma^2))` over the flattened K*D
77
+ latent, so a single draw carries variance correlated across slots.
78
+ eval_sample_temperature (`float`, *optional*, defaults to 1.0):
79
+ Default multiplier on the sampled perturbation around `mu`. 1.0 is
80
+ the calibrated conditional distribution and the setting every
81
+ reported number uses; 0.0 decodes the mean.
82
+ m2t_max_new_tokens (`int`, *optional*, defaults to 64):
83
+ Caption length cap for motion-to-text generation.
84
+ m2t_num_beams (`int`, *optional*, defaults to 1):
85
+ Beam count for captioning.
86
+ mot_token (`str`, *optional*, defaults to `"<MOT>"`):
87
+ Seed token that ends the text-to-motion prompt.
88
+ mot_token_id (`int`, *optional*, defaults to 50260):
89
+ Token id of `mot_token` in the shipped tokenizer.
90
+ t2m_prompt_template (`str`, *optional*):
91
+ Prompt used for text-to-motion. Must end in `mot_token`.
92
+ m2t_prompt_prefix (`str`, *optional*):
93
+ Instruction placed before the motion slots for captioning.
94
+ fps (`int`, *optional*, defaults to 20):
95
+ Frame rate of the HumanML3D features this model was trained on.
96
+ num_joints (`int`, *optional*, defaults to 22):
97
+ Joint count recovered by `features_to_joints`.
98
+ length_multiple (`int`, *optional*, defaults to 4):
99
+ HumanML3D crops clips to a multiple of 4 frames; requesting a
100
+ length off this grid moves you away from the training distribution.
101
+
102
+ Example:
103
+ ```python
104
+ from transformers import AutoConfig
105
+
106
+ config = AutoConfig.from_pretrained("zy22b/MUGEN", trust_remote_code=True)
107
+ print(config.k_latent_slots) # 2
108
+ ```
109
+ """
110
+
111
+ model_type = "mugen"
112
+
113
+ def __init__(
114
+ self,
115
+ # ---- motion autoencoder (ALAE) ----
116
+ motion_input_dim: int = 263,
117
+ k_latent_slots: int = 2,
118
+ latent_dim: int = 512,
119
+ alae_hidden_dim: int = 512,
120
+ alae_depth: int = 3,
121
+ alae_dilation_growth_rate: int = 3,
122
+ alae_activation: str = "gelu",
123
+ alae_norm=None,
124
+ alae_num_res_blocks: int = 2,
125
+ alae_num_encoder_layers: int = 4,
126
+ alae_num_decoder_layers: int = 4,
127
+ alae_nhead: int = 8,
128
+ alae_dim_feedforward: int = 2048,
129
+ alae_dropout: float = 0.05,
130
+ alae_max_decode_len: int = 256,
131
+ # ---- language model (GPT-2) ----
132
+ vocab_size: int = 50261,
133
+ n_positions: int = 1024,
134
+ n_embd: int = 768,
135
+ n_layer: int = 12,
136
+ n_head: int = 12,
137
+ n_inner=None,
138
+ activation_function: str = "gelu_new",
139
+ resid_pdrop: float = 0.1,
140
+ embd_pdrop: float = 0.1,
141
+ attn_pdrop: float = 0.1,
142
+ layer_norm_epsilon: float = 1e-5,
143
+ initializer_range: float = 0.02,
144
+ # ---- layer router ----
145
+ num_cross_attn_layers: int = 2,
146
+ router_hidden: int = 512,
147
+ router_static_scale: float = 4.0,
148
+ router_delta_scale: float = 4.0,
149
+ router_eval_tau: float = 1.5,
150
+ # ---- calibrated latent head ----
151
+ latent_low_rank: int = 64,
152
+ eval_sample_temperature: float = 1.0,
153
+ # ---- motion understanding ----
154
+ m2t_max_new_tokens: int = 64,
155
+ m2t_num_beams: int = 1,
156
+ # ---- prompting ----
157
+ mot_token: str = "<MOT>",
158
+ mot_token_id: int = 50260,
159
+ t2m_prompt_template: str = (
160
+ "Please generate human motion based on the following textual "
161
+ "description: {text} {mot}"
162
+ ),
163
+ m2t_prompt_prefix: str = (
164
+ "Please describe the following human motion using plain text:"
165
+ ),
166
+ # ---- data conventions ----
167
+ fps: int = 20,
168
+ num_joints: int = 22,
169
+ length_multiple: int = 4,
170
+ # ---- special tokens ----
171
+ bos_token_id: int = 50256,
172
+ eos_token_id: int = 50256,
173
+ pad_token_id: int = 50256,
174
+ **kwargs,
175
+ ):
176
+ self.motion_input_dim = motion_input_dim
177
+ self.k_latent_slots = k_latent_slots
178
+ self.latent_dim = latent_dim
179
+ self.alae_hidden_dim = alae_hidden_dim
180
+ self.alae_depth = alae_depth
181
+ self.alae_dilation_growth_rate = alae_dilation_growth_rate
182
+ self.alae_activation = alae_activation
183
+ self.alae_norm = alae_norm
184
+ self.alae_num_res_blocks = alae_num_res_blocks
185
+ self.alae_num_encoder_layers = alae_num_encoder_layers
186
+ self.alae_num_decoder_layers = alae_num_decoder_layers
187
+ self.alae_nhead = alae_nhead
188
+ self.alae_dim_feedforward = alae_dim_feedforward
189
+ self.alae_dropout = alae_dropout
190
+ self.alae_max_decode_len = alae_max_decode_len
191
+
192
+ self.vocab_size = vocab_size
193
+ self.n_positions = n_positions
194
+ self.n_embd = n_embd
195
+ self.n_layer = n_layer
196
+ self.n_head = n_head
197
+ self.n_inner = n_inner
198
+ self.activation_function = activation_function
199
+ self.resid_pdrop = resid_pdrop
200
+ self.embd_pdrop = embd_pdrop
201
+ self.attn_pdrop = attn_pdrop
202
+ self.layer_norm_epsilon = layer_norm_epsilon
203
+ self.initializer_range = initializer_range
204
+
205
+ self.num_cross_attn_layers = num_cross_attn_layers
206
+ self.router_hidden = router_hidden
207
+ self.router_static_scale = router_static_scale
208
+ self.router_delta_scale = router_delta_scale
209
+ self.router_eval_tau = router_eval_tau
210
+
211
+ self.latent_low_rank = latent_low_rank
212
+ self.eval_sample_temperature = eval_sample_temperature
213
+
214
+ self.m2t_max_new_tokens = m2t_max_new_tokens
215
+ self.m2t_num_beams = m2t_num_beams
216
+
217
+ self.mot_token = mot_token
218
+ self.mot_token_id = mot_token_id
219
+ self.t2m_prompt_template = t2m_prompt_template
220
+ self.m2t_prompt_prefix = m2t_prompt_prefix
221
+
222
+ self.fps = fps
223
+ self.num_joints = num_joints
224
+ self.length_multiple = length_multiple
225
+
226
+ super().__init__(
227
+ bos_token_id=bos_token_id,
228
+ eos_token_id=eos_token_id,
229
+ pad_token_id=pad_token_id,
230
+ **kwargs,
231
+ )
merges.txt ADDED
The diff for this file is too large to render. See raw diff
 
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:81bb85c54cdd870742c2ff84929476ac58c112da94386fb79cbb810c3b8ad79e
3
+ size 734822348
modeling_mugen.py ADDED
@@ -0,0 +1,899 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MUGEN: a unified motion-language model.
2
+
3
+ The model has three parts:
4
+
5
+ 1. An adaptive-length motion autoencoder (ALAE). Cross-attention compresses a
6
+ clip of any length into K continuous latent slots, and a second
7
+ cross-attention stack expands those slots back to any requested number of
8
+ frames. There is no codebook and no quantisation anywhere in the pipeline.
9
+ 2. A GPT-2 language model. For text-to-motion it rolls out K hidden states
10
+ after a `<MOT>` seed token; for captioning it reads the K slots back as
11
+ continuous input embeddings.
12
+ 3. A layer router and a calibrated latent head. The router gives each slot its
13
+ own soft mixture over all twelve transformer layers, so a slot reads from
14
+ the depth it needs instead of the final layer only. The head predicts a
15
+ joint Gaussian over the whole flattened latent set with a rank-64 plus
16
+ diagonal covariance, so one draw carries text-conditional variance that is
17
+ correlated across slots.
18
+
19
+ Generating a motion therefore costs K language-model steps, one draw, and one
20
+ decoder pass. No iterative refinement, no residual stages, no denoising chain.
21
+
22
+ Usage:
23
+ ```python
24
+ import torch
25
+ from transformers import AutoModelForCausalLM, AutoTokenizer
26
+
27
+ model = AutoModelForCausalLM.from_pretrained("zy22b/MUGEN", trust_remote_code=True).eval()
28
+ tokenizer = AutoTokenizer.from_pretrained("zy22b/MUGEN")
29
+
30
+ feats = model.generate_motion(["a person walks forward and waves."],
31
+ lengths=[120], tokenizer=tokenizer)
32
+ joints = model.features_to_joints(feats) # (1, 120, 22, 3)
33
+ caption = model.generate_caption(feats, tokenizer=tokenizer)
34
+ ```
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import math
40
+ from typing import List, Optional, Sequence, Tuple, Union
41
+
42
+ import torch
43
+ import torch.nn as nn
44
+ import torch.nn.functional as F
45
+ from transformers import GPT2Config, GPT2LMHeadModel, PreTrainedModel
46
+ from transformers.modeling_outputs import ModelOutput
47
+
48
+ try:
49
+ from .configuration_mugen import MugenConfig
50
+ except ImportError: # standalone / trust_remote_code loading
51
+ from configuration_mugen import MugenConfig
52
+
53
+
54
+ # =====================================================================
55
+ # Motion autoencoder building blocks
56
+ # =====================================================================
57
+
58
+ class Swish(nn.Module):
59
+ """x * sigmoid(x)."""
60
+
61
+ def forward(self, x):
62
+ return x * torch.sigmoid(x)
63
+
64
+
65
+ def _activation(name: str) -> nn.Module:
66
+ if name == "relu":
67
+ return nn.ReLU()
68
+ if name == "gelu":
69
+ return nn.GELU()
70
+ if name == "silu":
71
+ return Swish()
72
+ raise ValueError(f"Unsupported activation: {name}")
73
+
74
+
75
+ class ResConv1DBlock(nn.Module):
76
+ """Dilated residual convolution over the time axis."""
77
+
78
+ def __init__(self, n_in, n_state, dilation=1, activation="gelu", norm=None):
79
+ super().__init__()
80
+ padding = dilation
81
+ self.norm = norm
82
+ if norm == "LN":
83
+ self.norm1 = nn.LayerNorm(n_in)
84
+ self.norm2 = nn.LayerNorm(n_in)
85
+ elif norm == "GN":
86
+ self.norm1 = nn.GroupNorm(32, n_in, eps=1e-6, affine=True)
87
+ self.norm2 = nn.GroupNorm(32, n_in, eps=1e-6, affine=True)
88
+ elif norm == "BN":
89
+ self.norm1 = nn.BatchNorm1d(n_in, eps=1e-6, affine=True)
90
+ self.norm2 = nn.BatchNorm1d(n_in, eps=1e-6, affine=True)
91
+ else:
92
+ self.norm1 = nn.Identity()
93
+ self.norm2 = nn.Identity()
94
+
95
+ self.activation1 = _activation(activation)
96
+ self.activation2 = _activation(activation)
97
+ self.conv1 = nn.Conv1d(n_in, n_state, 3, 1, padding, dilation)
98
+ self.conv2 = nn.Conv1d(n_state, n_in, 1, 1, 0)
99
+
100
+ def forward(self, x):
101
+ x_orig = x
102
+ if self.norm == "LN":
103
+ x = self.norm1(x.transpose(-2, -1))
104
+ x = self.activation1(x.transpose(-2, -1))
105
+ else:
106
+ x = self.norm1(x)
107
+ x = self.activation1(x)
108
+ x = self.conv1(x)
109
+ if self.norm == "LN":
110
+ x = self.norm2(x.transpose(-2, -1))
111
+ x = self.activation2(x.transpose(-2, -1))
112
+ else:
113
+ x = self.norm2(x)
114
+ x = self.activation2(x)
115
+ x = self.conv2(x)
116
+ return x + x_orig
117
+
118
+
119
+ class Resnet1D(nn.Module):
120
+ """Stack of dilated residual convolutions."""
121
+
122
+ def __init__(self, n_in, n_depth, dilation_growth_rate=1,
123
+ reverse_dilation=True, activation="gelu", norm=None):
124
+ super().__init__()
125
+ blocks = [
126
+ ResConv1DBlock(n_in, n_in, dilation=dilation_growth_rate ** depth,
127
+ activation=activation, norm=norm)
128
+ for depth in range(n_depth)
129
+ ]
130
+ if reverse_dilation:
131
+ blocks = blocks[::-1]
132
+ self.model = nn.Sequential(*blocks)
133
+
134
+ def forward(self, x):
135
+ return self.model(x)
136
+
137
+
138
+ class CrossAttentionBlock(nn.Module):
139
+ """Pre-norm self-attention, cross-attention and feed-forward block.
140
+
141
+ `memory_key_padding_mask` (True marks padding) is forwarded to the
142
+ cross-attention. Passing `None` reproduces the unmasked block exactly.
143
+ """
144
+
145
+ def __init__(self, d_model, nhead, dim_feedforward, dropout=0.1, activation="gelu"):
146
+ super().__init__()
147
+ self.self_attn = nn.MultiheadAttention(d_model, nhead, dropout=dropout, batch_first=True)
148
+ self.cross_attn = nn.MultiheadAttention(d_model, nhead, dropout=dropout, batch_first=True)
149
+ self.linear1 = nn.Linear(d_model, dim_feedforward)
150
+ self.linear2 = nn.Linear(dim_feedforward, d_model)
151
+ self.dropout = nn.Dropout(dropout)
152
+
153
+ self.norm1 = nn.LayerNorm(d_model)
154
+ self.norm2 = nn.LayerNorm(d_model)
155
+ self.norm3 = nn.LayerNorm(d_model)
156
+ self.drop1 = nn.Dropout(dropout)
157
+ self.drop2 = nn.Dropout(dropout)
158
+ self.drop3 = nn.Dropout(dropout)
159
+ self.activation = _activation(activation)
160
+
161
+ def forward(self, target, memory, memory_key_padding_mask=None):
162
+ x = self.norm1(target)
163
+ x_sa, _ = self.self_attn(x, x, x, need_weights=False)
164
+ target = target + self.drop1(x_sa)
165
+
166
+ x = self.norm2(target)
167
+ x_ca, _ = self.cross_attn(
168
+ x, memory, memory, need_weights=False,
169
+ key_padding_mask=memory_key_padding_mask,
170
+ )
171
+ target = target + self.drop2(x_ca)
172
+
173
+ x = self.norm3(target)
174
+ x_ff = self.linear2(self.dropout(self.activation(self.linear1(x))))
175
+ target = target + self.drop3(x_ff)
176
+ return target
177
+
178
+
179
+ class MotionEncoderBackbone(nn.Module):
180
+ """Per-frame convolutional trunk feeding the cross-attention encoder."""
181
+
182
+ def __init__(self, input_dim, hidden_dim, latent_dim, depth,
183
+ dilation_growth_rate, activation, norm, num_res_blocks):
184
+ super().__init__()
185
+ blocks = [
186
+ nn.Conv1d(input_dim, hidden_dim, kernel_size=3, stride=1, padding=1),
187
+ _activation(activation),
188
+ ]
189
+ for _ in range(num_res_blocks):
190
+ blocks.append(Resnet1D(hidden_dim, depth, dilation_growth_rate,
191
+ reverse_dilation=False, activation=activation, norm=norm))
192
+ self.backbone = nn.Sequential(*blocks)
193
+ self.output_proj = nn.Conv1d(hidden_dim, latent_dim, kernel_size=1)
194
+
195
+ def forward(self, x):
196
+ return self.output_proj(self.backbone(x))
197
+
198
+
199
+ class MotionDecoderRefiner(nn.Module):
200
+ """Convolutional refiner turning decoded per-frame features into motion."""
201
+
202
+ def __init__(self, latent_dim, hidden_dim, output_dim, depth,
203
+ dilation_growth_rate, activation, norm, num_res_blocks):
204
+ super().__init__()
205
+ blocks = [
206
+ nn.Conv1d(latent_dim, hidden_dim, kernel_size=3, stride=1, padding=1),
207
+ _activation(activation),
208
+ ]
209
+ for _ in range(num_res_blocks):
210
+ blocks.append(Resnet1D(hidden_dim, depth, dilation_growth_rate,
211
+ reverse_dilation=True, activation=activation, norm=norm))
212
+ blocks.extend([
213
+ nn.Conv1d(hidden_dim, hidden_dim, kernel_size=3, stride=1, padding=1),
214
+ _activation(activation),
215
+ nn.Conv1d(hidden_dim, output_dim, kernel_size=3, stride=1, padding=1),
216
+ ])
217
+ self.model = nn.Sequential(*blocks)
218
+
219
+ def forward(self, x):
220
+ return self.model(x)
221
+
222
+
223
+ def build_sine_position_encoding(length, dim, device, dtype=torch.float32):
224
+ pe = torch.zeros(length, dim, device=device, dtype=dtype)
225
+ position = torch.arange(length, device=device, dtype=dtype).unsqueeze(1)
226
+ div_term = torch.exp(
227
+ torch.arange(0, dim, 2, device=device, dtype=dtype) * (-math.log(10000.0) / dim)
228
+ )
229
+ pe[:, 0::2] = torch.sin(position * div_term)
230
+ pe[:, 1::2] = torch.cos(position * div_term)
231
+ return pe
232
+
233
+
234
+ class AdaptiveLengthAutoEncoder(nn.Module):
235
+ """Compress a clip of any length into K latent slots, and back again.
236
+
237
+ `encode` returns `(batch, K, latent_dim)` regardless of the input length.
238
+ `decode` takes those slots plus a target frame count. Decoder query
239
+ positions are spread evenly across the whole positional table, so a query
240
+ encodes a frame's relative phase within the clip rather than an absolute
241
+ frame index. That is what lets one decoder serve every clip length.
242
+ """
243
+
244
+ def __init__(self, config: MugenConfig):
245
+ super().__init__()
246
+ self.input_dim = config.motion_input_dim
247
+ self.k = config.k_latent_slots
248
+ self.latent_dim = config.latent_dim
249
+ self.max_decode_len = config.alae_max_decode_len
250
+
251
+ common = dict(
252
+ depth=config.alae_depth,
253
+ dilation_growth_rate=config.alae_dilation_growth_rate,
254
+ activation=config.alae_activation,
255
+ norm=config.alae_norm,
256
+ num_res_blocks=config.alae_num_res_blocks,
257
+ )
258
+ self.encoder_backbone = MotionEncoderBackbone(
259
+ input_dim=config.motion_input_dim,
260
+ hidden_dim=config.alae_hidden_dim,
261
+ latent_dim=config.latent_dim,
262
+ **common,
263
+ )
264
+ self.latent_queries = nn.Parameter(torch.randn(self.k, config.latent_dim) * 0.02)
265
+
266
+ block_kwargs = dict(
267
+ d_model=config.latent_dim,
268
+ nhead=config.alae_nhead,
269
+ dim_feedforward=config.alae_dim_feedforward,
270
+ dropout=config.alae_dropout,
271
+ activation=config.alae_activation,
272
+ )
273
+ self.encoder_blocks = nn.ModuleList(
274
+ [CrossAttentionBlock(**block_kwargs) for _ in range(config.alae_num_encoder_layers)]
275
+ )
276
+ self.encoder_norm = nn.LayerNorm(config.latent_dim)
277
+
278
+ self.decoder_blocks = nn.ModuleList(
279
+ [CrossAttentionBlock(**block_kwargs) for _ in range(config.alae_num_decoder_layers)]
280
+ )
281
+ self.decoder_norm = nn.LayerNorm(config.latent_dim)
282
+ self.decoder_refiner = MotionDecoderRefiner(
283
+ latent_dim=config.latent_dim,
284
+ hidden_dim=config.alae_hidden_dim,
285
+ output_dim=config.motion_input_dim,
286
+ **common,
287
+ )
288
+
289
+ self.register_buffer(
290
+ "decoder_query_pe",
291
+ build_sine_position_encoding(self.max_decode_len, config.latent_dim,
292
+ device=torch.device("cpu")),
293
+ persistent=False,
294
+ )
295
+
296
+ def ensure_decode_capacity(self, target_len: int):
297
+ target_len = int(target_len)
298
+ if target_len <= self.max_decode_len:
299
+ return
300
+ self.max_decode_len = target_len
301
+ self.register_buffer(
302
+ "decoder_query_pe",
303
+ build_sine_position_encoding(target_len, self.latent_dim,
304
+ device=torch.device("cpu")),
305
+ persistent=False,
306
+ )
307
+
308
+ def encode(self, motion: torch.Tensor) -> torch.Tensor:
309
+ """`(batch, time, input_dim)` -> `(batch, K, latent_dim)`."""
310
+ x_in = motion.permute(0, 2, 1).float()
311
+ memory = self.encoder_backbone(x_in).permute(0, 2, 1)
312
+ queries = self.latent_queries.unsqueeze(0).expand(memory.shape[0], -1, -1)
313
+ latents = queries
314
+ for block in self.encoder_blocks:
315
+ latents = block(latents, memory)
316
+ return self.encoder_norm(latents)
317
+
318
+ def decode(self, latents: torch.Tensor, target_len: int) -> torch.Tensor:
319
+ """`(batch, K, latent_dim)` -> `(batch, target_len, input_dim)`."""
320
+ target_len = int(target_len)
321
+ self.ensure_decode_capacity(target_len)
322
+ if target_len > 1:
323
+ idx = torch.linspace(0, self.max_decode_len - 1, target_len,
324
+ device=latents.device).round().long()
325
+ else:
326
+ idx = torch.zeros(target_len, device=latents.device, dtype=torch.long)
327
+ query_pe = self.decoder_query_pe.to(device=latents.device, dtype=latents.dtype)[idx]
328
+ decoded = query_pe.unsqueeze(0).expand(latents.shape[0], -1, -1)
329
+ for block in self.decoder_blocks:
330
+ decoded = block(decoded, latents)
331
+ decoded = self.decoder_norm(decoded).permute(0, 2, 1)
332
+ return self.decoder_refiner(decoded).permute(0, 2, 1)
333
+
334
+ def forward(self, motion: torch.Tensor, target_len: Optional[int] = None):
335
+ if target_len is None:
336
+ target_len = motion.shape[1]
337
+ latents = self.encode(motion)
338
+ return self.decode(latents, target_len), latents
339
+
340
+
341
+ # =====================================================================
342
+ # Quaternion helpers for joint recovery
343
+ # =====================================================================
344
+
345
+ def _qinv(q: torch.Tensor) -> torch.Tensor:
346
+ mask = torch.ones_like(q)
347
+ mask[..., 1:] = -mask[..., 1:]
348
+ return q * mask
349
+
350
+
351
+ def _qrot(q: torch.Tensor, v: torch.Tensor) -> torch.Tensor:
352
+ original_shape = list(v.shape)
353
+ q = q.contiguous().view(-1, 4)
354
+ v = v.contiguous().view(-1, 3)
355
+ qvec = q[:, 1:]
356
+ uv = torch.cross(qvec, v, dim=1)
357
+ uuv = torch.cross(qvec, uv, dim=1)
358
+ return (v + 2 * (q[:, :1] * uv + uuv)).view(original_shape)
359
+
360
+
361
+ def recover_from_ric(features: torch.Tensor, num_joints: int) -> torch.Tensor:
362
+ """HumanML3D features -> joint positions.
363
+
364
+ `features` are the DENORMALISED 263-dimensional vectors; the return value
365
+ is `(..., time, num_joints, 3)` in metres.
366
+ """
367
+ rot_vel = features[..., 0]
368
+ r_rot_ang = torch.zeros_like(rot_vel)
369
+ r_rot_ang[..., 1:] = rot_vel[..., :-1]
370
+ r_rot_ang = torch.cumsum(r_rot_ang, dim=-1)
371
+
372
+ r_rot_quat = torch.zeros(features.shape[:-1] + (4,),
373
+ dtype=features.dtype, device=features.device)
374
+ r_rot_quat[..., 0] = torch.cos(r_rot_ang)
375
+ r_rot_quat[..., 2] = torch.sin(r_rot_ang)
376
+
377
+ r_pos = torch.zeros(features.shape[:-1] + (3,),
378
+ dtype=features.dtype, device=features.device)
379
+ r_pos[..., 1:, [0, 2]] = features[..., :-1, 1:3]
380
+ r_pos = _qrot(_qinv(r_rot_quat), r_pos)
381
+ r_pos = torch.cumsum(r_pos, dim=-2)
382
+ r_pos[..., 1] = features[..., 3]
383
+
384
+ positions = features[..., 4:(num_joints - 1) * 3 + 4]
385
+ positions = positions.view(positions.shape[:-1] + (-1, 3))
386
+ positions = _qrot(
387
+ _qinv(r_rot_quat[..., None, :]).expand(positions.shape[:-1] + (4,)), positions
388
+ )
389
+ positions[..., 0] += r_pos[..., 0:1]
390
+ positions[..., 2] += r_pos[..., 2:3]
391
+ return torch.cat([r_pos.unsqueeze(-2), positions], dim=-2)
392
+
393
+
394
+ # =====================================================================
395
+ # Model
396
+ # =====================================================================
397
+
398
+ class MugenOutput(ModelOutput):
399
+ """Output of :meth:`MugenForConditionalGeneration.forward`.
400
+
401
+ Attributes:
402
+ latents: `(batch, K, latent_dim)` slots, in raw autoencoder space.
403
+ reconstruction: `(batch, time, motion_input_dim)` when a motion was given.
404
+ logits: language-model logits when `input_ids` was given.
405
+ """
406
+
407
+ latents: Optional[torch.FloatTensor] = None
408
+ reconstruction: Optional[torch.FloatTensor] = None
409
+ logits: Optional[torch.FloatTensor] = None
410
+
411
+
412
+ class MugenPreTrainedModel(PreTrainedModel):
413
+ config_class = MugenConfig
414
+ base_model_prefix = "mugen"
415
+ supports_gradient_checkpointing = False
416
+
417
+ def _init_weights(self, module):
418
+ std = self.config.initializer_range
419
+ if isinstance(module, (nn.Linear, nn.Conv1d)):
420
+ module.weight.data.normal_(mean=0.0, std=std)
421
+ if module.bias is not None:
422
+ module.bias.data.zero_()
423
+ elif isinstance(module, nn.Embedding):
424
+ module.weight.data.normal_(mean=0.0, std=std)
425
+ if module.padding_idx is not None:
426
+ module.weight.data[module.padding_idx].zero_()
427
+ elif isinstance(module, nn.LayerNorm):
428
+ module.bias.data.zero_()
429
+ module.weight.data.fill_(1.0)
430
+
431
+
432
+ class MugenForConditionalGeneration(MugenPreTrainedModel):
433
+ """MUGEN: text-to-motion generation and motion-to-text understanding.
434
+
435
+ The public API is three methods:
436
+
437
+ * :meth:`generate_motion` turns text into motion features.
438
+ * :meth:`generate_caption` turns motion into text.
439
+ * :meth:`encode_motion` / :meth:`decode_motion` expose the latent interface
440
+ both directions share.
441
+
442
+ :meth:`features_to_joints` converts motion features into joint positions
443
+ for rendering or measurement.
444
+ """
445
+
446
+ _tied_weights_keys = ["language_model.lm_head.weight"]
447
+ _keys_to_ignore_on_load_missing = [r"motion_autoencoder\.decoder_query_pe"]
448
+
449
+ def __init__(self, config: MugenConfig):
450
+ super().__init__(config)
451
+
452
+ self.k = int(config.k_latent_slots)
453
+ self.latent_dim = int(config.latent_dim)
454
+
455
+ # ---- part 1: the motion autoencoder ----
456
+ self.motion_autoencoder = AdaptiveLengthAutoEncoder(config)
457
+
458
+ # ---- part 2: the language model ----
459
+ gpt2_config = GPT2Config(
460
+ vocab_size=config.vocab_size,
461
+ n_positions=config.n_positions,
462
+ n_embd=config.n_embd,
463
+ n_layer=config.n_layer,
464
+ n_head=config.n_head,
465
+ n_inner=config.n_inner,
466
+ activation_function=config.activation_function,
467
+ resid_pdrop=config.resid_pdrop,
468
+ embd_pdrop=config.embd_pdrop,
469
+ attn_pdrop=config.attn_pdrop,
470
+ layer_norm_epsilon=config.layer_norm_epsilon,
471
+ initializer_range=config.initializer_range,
472
+ bos_token_id=config.bos_token_id,
473
+ eos_token_id=config.eos_token_id,
474
+ tie_word_embeddings=True,
475
+ )
476
+ self.language_model = GPT2LMHeadModel(gpt2_config)
477
+ self.n_layers = int(config.n_layer)
478
+ hidden_size = int(config.n_embd)
479
+
480
+ # ---- part 3a: the layer router ----
481
+ # A slot's routing logits are a bounded static table plus a bounded
482
+ # text-conditional correction. Both halves are tanh-capped, so no logit
483
+ # margin can grow large enough to saturate the routing softmax.
484
+ self.static_router_logits = nn.Parameter(torch.zeros(self.k, self.n_layers))
485
+ self.text_mem_proj = nn.Linear(hidden_size, self.latent_dim)
486
+ self.stage2_latent_queries = nn.Parameter(torch.zeros(self.k, self.latent_dim))
487
+ self.cross_attn_blocks = nn.ModuleList([
488
+ CrossAttentionBlock(
489
+ d_model=self.latent_dim,
490
+ nhead=config.alae_nhead,
491
+ dim_feedforward=config.alae_dim_feedforward,
492
+ dropout=config.alae_dropout,
493
+ activation=config.alae_activation,
494
+ )
495
+ for _ in range(int(config.num_cross_attn_layers))
496
+ ])
497
+ self.cross_attn_norm = nn.LayerNorm(self.latent_dim)
498
+ self.layer_router = nn.Sequential(
499
+ nn.Linear(self.latent_dim, int(config.router_hidden)),
500
+ nn.GELU(),
501
+ nn.Linear(int(config.router_hidden), self.n_layers),
502
+ )
503
+
504
+ # ---- part 3b: the calibrated latent head ----
505
+ self.projector_norm = nn.LayerNorm(hidden_size)
506
+ self.projector = nn.Linear(hidden_size, 2 * self.latent_dim)
507
+ self.feedback_norm = nn.LayerNorm(hidden_size)
508
+ self.latent_low_rank = int(config.latent_low_rank)
509
+ if self.latent_low_rank > 0:
510
+ self.latent_factors = nn.Parameter(
511
+ torch.zeros(self.k * self.latent_dim, self.latent_low_rank)
512
+ )
513
+ self.factor_scale_head = nn.Linear(hidden_size, self.latent_low_rank)
514
+
515
+ # ---- the understanding direction ----
516
+ self.motion_in_projector = nn.Linear(self.latent_dim, hidden_size)
517
+
518
+ # ---- normalisation statistics ----
519
+ # latent_*: standardise the latent space the head predicts in.
520
+ # feature_*: HumanML3D feature statistics, so the model can hand back
521
+ # denormalised motion without the caller owning the dataset.
522
+ self.register_buffer("latent_mean", torch.zeros(self.k, self.latent_dim))
523
+ self.register_buffer("latent_std", torch.ones(self.k, self.latent_dim))
524
+ self.register_buffer("feature_mean", torch.zeros(config.motion_input_dim))
525
+ self.register_buffer("feature_std", torch.ones(config.motion_input_dim))
526
+
527
+ self._tokenizer = None
528
+ self.post_init()
529
+
530
+ # ------------------------------------------------------------------
531
+ # plumbing
532
+ # ------------------------------------------------------------------
533
+ def get_input_embeddings(self):
534
+ return self.language_model.get_input_embeddings()
535
+
536
+ def set_input_embeddings(self, value):
537
+ self.language_model.set_input_embeddings(value)
538
+
539
+ def get_output_embeddings(self):
540
+ return self.language_model.get_output_embeddings()
541
+
542
+ def set_output_embeddings(self, new_embeddings):
543
+ self.language_model.set_output_embeddings(new_embeddings)
544
+
545
+ def set_tokenizer(self, tokenizer):
546
+ """Cache a tokenizer so the generate helpers can be called without one."""
547
+ self._tokenizer = tokenizer
548
+ return self
549
+
550
+ def _get_tokenizer(self, tokenizer=None):
551
+ if tokenizer is not None:
552
+ return tokenizer
553
+ if self._tokenizer is None:
554
+ from transformers import AutoTokenizer
555
+ name = self.config._name_or_path or "zy22b/MUGEN"
556
+ self._tokenizer = AutoTokenizer.from_pretrained(name)
557
+ return self._tokenizer
558
+
559
+ # ------------------------------------------------------------------
560
+ # the latent interface both directions share
561
+ # ------------------------------------------------------------------
562
+ @torch.no_grad()
563
+ def encode_motion(self, motion: torch.Tensor, normalized: bool = False) -> torch.Tensor:
564
+ """Motion -> K latent slots.
565
+
566
+ Args:
567
+ motion: `(batch, time, 263)` HumanML3D features.
568
+ normalized: `True` if `motion` is already standardised by the
569
+ dataset statistics. `False` (the default) normalises it here.
570
+
571
+ Returns:
572
+ `(batch, K, latent_dim)` slots in raw autoencoder space.
573
+ """
574
+ motion = motion.to(self.device, dtype=torch.float32)
575
+ if not normalized:
576
+ motion = (motion - self.feature_mean) / self.feature_std
577
+ return self.motion_autoencoder.encode(motion)
578
+
579
+ @torch.no_grad()
580
+ def decode_motion(self, latents: torch.Tensor, length: int,
581
+ denormalize: bool = True) -> torch.Tensor:
582
+ """K latent slots -> `(batch, length, 263)` motion features."""
583
+ latents = latents.to(self.device, dtype=torch.float32)
584
+ feats = self.motion_autoencoder.decode(latents, target_len=int(length))
585
+ if denormalize:
586
+ feats = feats * self.feature_std + self.feature_mean
587
+ return feats
588
+
589
+ def standardize_latents(self, latents: torch.Tensor) -> torch.Tensor:
590
+ return (latents - self.latent_mean) / self.latent_std
591
+
592
+ def destandardize_latents(self, latents: torch.Tensor) -> torch.Tensor:
593
+ return latents * self.latent_std + self.latent_mean
594
+
595
+ # ------------------------------------------------------------------
596
+ # text -> latents
597
+ # ------------------------------------------------------------------
598
+ def _build_t2m_prompts(self, texts: Sequence[str]) -> List[str]:
599
+ tpl = self.config.t2m_prompt_template
600
+ return [tpl.format(text=t.strip(), mot=self.config.mot_token) for t in texts]
601
+
602
+ def _routed_hidden(self, input_ids: torch.Tensor,
603
+ attention_mask: torch.Tensor) -> torch.Tensor:
604
+ """Roll out K steps and return the routed per-slot hidden states.
605
+
606
+ Returns `(batch, K, hidden_size)`. Each slot is a soft mixture over all
607
+ `n_layers` transformer layers of its own rollout step, with mixture
608
+ weights produced by the router from the prompt.
609
+ """
610
+ K = self.k
611
+ device = input_ids.device
612
+ prompt_len = input_ids.shape[1]
613
+
614
+ def _positions(start: int, count: int) -> torch.Tensor:
615
+ return torch.arange(start, start + count, dtype=torch.long,
616
+ device=device).unsqueeze(0)
617
+
618
+ out = self.language_model(
619
+ input_ids=input_ids,
620
+ attention_mask=attention_mask,
621
+ position_ids=_positions(0, prompt_len),
622
+ output_hidden_states=True,
623
+ use_cache=True,
624
+ return_dict=True,
625
+ )
626
+
627
+ # Router memory is the FULL last-layer token sequence plus its pad mask,
628
+ # so the routing queries can attend to individual words.
629
+ memory = self.text_mem_proj(out.hidden_states[-1])
630
+ pad_mask = attention_mask == 0
631
+
632
+ def _stack_all_layers(hidden_states) -> torch.Tensor:
633
+ cols = [h[:, -1:, :] for h in hidden_states[1:]]
634
+ return torch.stack(cols, dim=2) # (B, 1, L, H)
635
+
636
+ all_steps = [_stack_all_layers(out.hidden_states)]
637
+ last_hiddens = [out.hidden_states[-1][:, -1:, :]]
638
+ past = out.past_key_values
639
+ cur_attn = attention_mask
640
+
641
+ for step_idx in range(K - 1):
642
+ next_input = self.feedback_norm(last_hiddens[-1])
643
+ cur_attn = torch.cat(
644
+ [cur_attn, torch.ones((cur_attn.shape[0], 1), dtype=cur_attn.dtype, device=device)],
645
+ dim=1,
646
+ )
647
+ step = self.language_model(
648
+ inputs_embeds=next_input,
649
+ attention_mask=cur_attn,
650
+ position_ids=_positions(prompt_len + step_idx, 1),
651
+ past_key_values=past,
652
+ output_hidden_states=True,
653
+ use_cache=True,
654
+ return_dict=True,
655
+ )
656
+ past = step.past_key_values
657
+ all_steps.append(_stack_all_layers(step.hidden_states))
658
+ last_hiddens.append(step.hidden_states[-1][:, -1:, :])
659
+
660
+ H_all = torch.cat(all_steps, dim=1) # (B, K, L, H)
661
+ B = H_all.shape[0]
662
+
663
+ latents = self.stage2_latent_queries.unsqueeze(0).expand(B, -1, -1)
664
+ for block in self.cross_attn_blocks:
665
+ latents = block(latents, memory, memory_key_padding_mask=pad_mask)
666
+ latents = self.cross_attn_norm(latents) # (B, K, D)
667
+
668
+ s_s = float(self.config.router_static_scale)
669
+ s_d = float(self.config.router_delta_scale)
670
+ static = s_s * torch.tanh(self.static_router_logits / s_s) # (K, L)
671
+ delta = s_d * torch.tanh(self.layer_router(latents).float() / s_d)
672
+ logits = static.unsqueeze(0).float() + delta # (B, K, L)
673
+
674
+ tau = float(self.config.router_eval_tau)
675
+ weights = F.softmax(logits / tau, dim=-1).to(H_all.dtype)
676
+ return torch.einsum("bkl,bklh->bkh", weights, H_all)
677
+
678
+ def _latent_distribution(self, hidden: torch.Tensor):
679
+ """Routed hidden states -> `(mu, logvar, log_a)` in standardised space."""
680
+ h = self.projector_norm(hidden)
681
+ mu, logvar = self.projector(h).chunk(2, dim=-1)
682
+ logvar = logvar.clamp(-10.0, 10.0)
683
+ log_a = None
684
+ if self.latent_low_rank > 0:
685
+ log_a = self.factor_scale_head(h.mean(dim=1)) # (B, r)
686
+ return mu, logvar, log_a
687
+
688
+ def sample_latents(self, mu: torch.Tensor, logvar: torch.Tensor,
689
+ log_a: Optional[torch.Tensor], temperature: float,
690
+ generator: Optional[torch.Generator] = None) -> torch.Tensor:
691
+ """One structured draw `z = mu + t * (U (a * eps1) + sigma * eps2)`.
692
+
693
+ The rank-r factor term carries the variance that is correlated across
694
+ slots; the diagonal term carries the rest. `temperature` scales the
695
+ whole zero-mean perturbation, so 0.0 returns `mu` exactly.
696
+ """
697
+ B, K, D = mu.shape
698
+ if temperature <= 0.0:
699
+ return mu
700
+
701
+ def _randn(like):
702
+ if generator is None:
703
+ return torch.randn_like(like)
704
+ return torch.randn(like.shape, generator=generator,
705
+ device=like.device, dtype=like.dtype)
706
+
707
+ # The factor noise is drawn BEFORE the diagonal noise, matching the
708
+ # training implementation, so a given seed reproduces the same draw
709
+ # here as it does there.
710
+ factor_part = 0.0
711
+ if log_a is not None:
712
+ a = torch.exp(log_a.clamp(-6.0, 4.0)) # (B, r)
713
+ factor_part = torch.einsum(
714
+ "dr,br->bd", self.latent_factors, a * _randn(a)
715
+ )
716
+ sigma = torch.exp(0.5 * logvar).reshape(B, K * D)
717
+ diag_part = sigma * _randn(sigma)
718
+ z = mu.reshape(B, K * D) + float(temperature) * (factor_part + diag_part)
719
+ return z.reshape(B, K, D)
720
+
721
+ @torch.no_grad()
722
+ def text_to_latents(self, texts: Union[str, Sequence[str]], tokenizer=None,
723
+ temperature: Optional[float] = None,
724
+ generator: Optional[torch.Generator] = None) -> torch.Tensor:
725
+ """Text -> K latent slots in raw autoencoder space."""
726
+ if isinstance(texts, str):
727
+ texts = [texts]
728
+ tokenizer = self._get_tokenizer(tokenizer)
729
+ prev_side = tokenizer.padding_side
730
+ tokenizer.padding_side = "left"
731
+ try:
732
+ enc = tokenizer(
733
+ self._build_t2m_prompts(texts),
734
+ return_tensors="pt", padding=True, truncation=True, max_length=256,
735
+ )
736
+ finally:
737
+ tokenizer.padding_side = prev_side
738
+ enc = {k: v.to(self.device) for k, v in enc.items()}
739
+
740
+ hidden = self._routed_hidden(enc["input_ids"], enc["attention_mask"])
741
+ mu, logvar, log_a = self._latent_distribution(hidden)
742
+ temp = float(self.config.eval_sample_temperature if temperature is None else temperature)
743
+ z_norm = self.sample_latents(mu, logvar, log_a, temp, generator=generator)
744
+ return self.destandardize_latents(z_norm)
745
+
746
+ @torch.no_grad()
747
+ def generate_motion(self, texts: Union[str, Sequence[str]],
748
+ lengths: Union[int, Sequence[int], None] = None,
749
+ tokenizer=None,
750
+ temperature: Optional[float] = None,
751
+ denormalize: bool = True,
752
+ generator: Optional[torch.Generator] = None) -> torch.Tensor:
753
+ """Text -> motion features.
754
+
755
+ Args:
756
+ texts: one description, or a batch of them.
757
+ lengths: target frame count per description (20 fps). A single int
758
+ applies to the whole batch. Defaults to 120 frames (6 seconds).
759
+ HumanML3D clips are multiples of 4 frames and at most 196, so
760
+ stay in that range to stay in distribution.
761
+ tokenizer: the tokenizer shipped with this repo. Loaded lazily when
762
+ omitted.
763
+ temperature: multiplier on the sampled perturbation. Defaults to
764
+ `config.eval_sample_temperature` (1.0), the calibrated
765
+ conditional distribution. 0.0 decodes the distribution mean,
766
+ which is not the protocol the published numbers use.
767
+ denormalize: return features in raw HumanML3D units. Keep this on
768
+ unless you intend to feed the output back into the encoder.
769
+ generator: optional `torch.Generator` for reproducible draws.
770
+
771
+ Returns:
772
+ `(batch, max_length, 263)`. When lengths differ within a batch, the
773
+ tensor is padded with zeros and each row is valid up to its own
774
+ length.
775
+ """
776
+ if isinstance(texts, str):
777
+ texts = [texts]
778
+ texts = list(texts)
779
+ if lengths is None:
780
+ lengths = [120] * len(texts)
781
+ elif isinstance(lengths, int):
782
+ lengths = [lengths] * len(texts)
783
+ lengths = [int(v) for v in lengths]
784
+ if len(lengths) != len(texts):
785
+ raise ValueError(f"got {len(texts)} texts but {len(lengths)} lengths")
786
+
787
+ latents = self.text_to_latents(texts, tokenizer=tokenizer,
788
+ temperature=temperature, generator=generator)
789
+
790
+ max_len = max(lengths)
791
+ if len(set(lengths)) == 1:
792
+ return self.decode_motion(latents, max_len, denormalize=denormalize)
793
+
794
+ # Mixed lengths: the decoder's relative-phase queries mean a row must be
795
+ # decoded at its own length, not cropped from the longest one.
796
+ out = latents.new_zeros((len(texts), max_len, self.config.motion_input_dim))
797
+ for i, length in enumerate(lengths):
798
+ out[i, :length] = self.decode_motion(
799
+ latents[i: i + 1], length, denormalize=denormalize
800
+ )[0]
801
+ return out
802
+
803
+ # ------------------------------------------------------------------
804
+ # motion -> text
805
+ # ------------------------------------------------------------------
806
+ def _m2t_prompt_embeds(self, latents: torch.Tensor, tokenizer):
807
+ """`[instruction, <MOT>, K motion slots]` as input embeddings."""
808
+ B = latents.shape[0]
809
+ device = latents.device
810
+ wte = self.language_model.get_input_embeddings()
811
+
812
+ prefix_ids = torch.tensor(
813
+ tokenizer(self.config.m2t_prompt_prefix, add_special_tokens=False)["input_ids"],
814
+ dtype=torch.long, device=device,
815
+ ).unsqueeze(0).expand(B, -1)
816
+ mot_ids = torch.full((B, 1), int(self.config.mot_token_id),
817
+ dtype=torch.long, device=device)
818
+
819
+ prompt = torch.cat(
820
+ [wte(prefix_ids), wte(mot_ids), self.motion_in_projector(latents)], dim=1
821
+ )
822
+ attn = torch.ones(prompt.shape[:2], dtype=torch.long, device=device)
823
+ return prompt, attn
824
+
825
+ @torch.no_grad()
826
+ def generate_caption(self, motion: Optional[torch.Tensor] = None,
827
+ latents: Optional[torch.Tensor] = None,
828
+ tokenizer=None, normalized: bool = False,
829
+ max_new_tokens: Optional[int] = None,
830
+ num_beams: Optional[int] = None) -> List[str]:
831
+ """Motion -> one caption per clip.
832
+
833
+ Pass either `motion` (`(batch, time, 263)` features) or `latents`
834
+ (`(batch, K, latent_dim)`) if you already have the slots. Set
835
+ `normalized=True` when `motion` is already standardised.
836
+ """
837
+ if (motion is None) == (latents is None):
838
+ raise ValueError("pass exactly one of `motion` or `latents`")
839
+ tokenizer = self._get_tokenizer(tokenizer)
840
+ if latents is None:
841
+ latents = self.encode_motion(motion, normalized=normalized)
842
+ latents = latents.to(self.device, dtype=torch.float32)
843
+
844
+ prompt, attn = self._m2t_prompt_embeds(latents, tokenizer)
845
+ gen = self.language_model.generate(
846
+ inputs_embeds=prompt,
847
+ attention_mask=attn,
848
+ max_new_tokens=int(self.config.m2t_max_new_tokens
849
+ if max_new_tokens is None else max_new_tokens),
850
+ do_sample=False,
851
+ num_beams=int(self.config.m2t_num_beams if num_beams is None else num_beams),
852
+ pad_token_id=tokenizer.eos_token_id,
853
+ )
854
+ # Called with inputs_embeds and no input_ids, generate returns only the
855
+ # newly produced tokens.
856
+ return [t.strip() for t in tokenizer.batch_decode(gen, skip_special_tokens=True)]
857
+
858
+ # ------------------------------------------------------------------
859
+ # utilities
860
+ # ------------------------------------------------------------------
861
+ def features_to_joints(self, features: torch.Tensor,
862
+ normalized: bool = False) -> torch.Tensor:
863
+ """Motion features -> `(batch, time, num_joints, 3)` joint positions in metres."""
864
+ features = features.to(self.device, dtype=torch.float32)
865
+ if normalized:
866
+ features = features * self.feature_std + self.feature_mean
867
+ return recover_from_ric(features, int(self.config.num_joints))
868
+
869
+ def forward(self,
870
+ motion: Optional[torch.Tensor] = None,
871
+ input_ids: Optional[torch.LongTensor] = None,
872
+ attention_mask: Optional[torch.Tensor] = None,
873
+ target_length: Optional[int] = None,
874
+ normalized: bool = False,
875
+ return_dict: bool = True,
876
+ **kwargs) -> MugenOutput:
877
+ """Autoencode a motion, run the language model, or both.
878
+
879
+ This is the plumbing entry point. For generation use
880
+ :meth:`generate_motion` and :meth:`generate_caption`.
881
+ """
882
+ latents = reconstruction = logits = None
883
+ if motion is not None:
884
+ motion = motion.to(self.device, dtype=torch.float32)
885
+ if not normalized:
886
+ motion = (motion - self.feature_mean) / self.feature_std
887
+ latents = self.motion_autoencoder.encode(motion)
888
+ reconstruction = self.motion_autoencoder.decode(
889
+ latents, target_len=int(target_length or motion.shape[1])
890
+ )
891
+ if input_ids is not None:
892
+ logits = self.language_model(
893
+ input_ids=input_ids, attention_mask=attention_mask, return_dict=True
894
+ ).logits
895
+ return MugenOutput(latents=latents, reconstruction=reconstruction, logits=logits)
896
+
897
+
898
+ MugenConfig.register_for_auto_class()
899
+ MugenForConditionalGeneration.register_for_auto_class("AutoModelForCausalLM")
special_tokens_map.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "additional_special_tokens": [
3
+ {
4
+ "content": "<MOT>",
5
+ "lstrip": false,
6
+ "normalized": false,
7
+ "rstrip": false,
8
+ "single_word": false
9
+ }
10
+ ],
11
+ "bos_token": "<|endoftext|>",
12
+ "eos_token": "<|endoftext|>",
13
+ "pad_token": "<|endoftext|>",
14
+ "unk_token": "<|endoftext|>"
15
+ }
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer_config.json ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": false,
3
+ "added_tokens_decoder": {
4
+ "50256": {
5
+ "content": "<|endoftext|>",
6
+ "lstrip": false,
7
+ "normalized": true,
8
+ "rstrip": false,
9
+ "single_word": false,
10
+ "special": true
11
+ },
12
+ "50257": {
13
+ "content": "<motion_id_0>",
14
+ "lstrip": false,
15
+ "normalized": true,
16
+ "rstrip": false,
17
+ "single_word": false,
18
+ "special": false
19
+ },
20
+ "50258": {
21
+ "content": "<motion_id_1>",
22
+ "lstrip": false,
23
+ "normalized": true,
24
+ "rstrip": false,
25
+ "single_word": false,
26
+ "special": false
27
+ },
28
+ "50259": {
29
+ "content": "<motion_id_2>",
30
+ "lstrip": false,
31
+ "normalized": true,
32
+ "rstrip": false,
33
+ "single_word": false,
34
+ "special": false
35
+ },
36
+ "50260": {
37
+ "content": "<MOT>",
38
+ "lstrip": false,
39
+ "normalized": false,
40
+ "rstrip": false,
41
+ "single_word": false,
42
+ "special": true
43
+ }
44
+ },
45
+ "additional_special_tokens": [
46
+ "<MOT>"
47
+ ],
48
+ "bos_token": "<|endoftext|>",
49
+ "clean_up_tokenization_spaces": false,
50
+ "eos_token": "<|endoftext|>",
51
+ "extra_special_tokens": {},
52
+ "legacy": true,
53
+ "model_max_length": 1024,
54
+ "pad_token": "<|endoftext|>",
55
+ "tokenizer_class": "GPT2Tokenizer",
56
+ "unk_token": "<|endoftext|>"
57
+ }
vocab.json ADDED
The diff for this file is too large to render. See raw diff