multimodalart HF Staff commited on
Commit
9a5720a
·
verified ·
1 Parent(s): 7d7bdc7

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. README.md +37 -8
  2. app.py +365 -0
  3. requirements.txt +12 -0
README.md CHANGED
@@ -1,13 +1,42 @@
1
  ---
2
- title: Prism Text To Motion
3
- emoji: 📉
4
- colorFrom: red
5
- colorTo: indigo
6
  sdk: gradio
7
- sdk_version: 6.24.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: PRISM Text-to-Motion
3
+ emoji: 🏃
4
+ colorFrom: purple
5
+ colorTo: yellow
6
  sdk: gradio
7
+ sdk_version: 5.50.0
 
8
  app_file: app.py
9
+ short_description: Generate human motion sequences from text prompts
10
+ python_version: "3.12"
11
+ startup_duration_timeout: 30m
12
  ---
13
 
14
+ # PRISM: Streaming Human Motion Generation with Per-Joint Latent Decomposition
15
+
16
+ This Space demonstrates **PRISM**, a text-to-motion generation model that produces
17
+ SMPL body motion sequences from natural language prompts.
18
+
19
+ ## How it works
20
+
21
+ 1. Enter a text prompt describing a human motion
22
+ 2. The model generates a motion sequence using a flow-matching DiT transformer
23
+ with a causal spatio-temporal Motion VAE
24
+ 3. The output is rendered as a 3D skeleton animation
25
+
26
+ ## Model
27
+
28
+ - **Model**: `ZeyuLing/PRISM-TP2M-1.4B` (~1.4B parameters)
29
+ - **Architecture**: Flow-matching DiT transformer with per-joint latent decomposition
30
+ - **Text encoder**: UMT5 (T5-style)
31
+ - **Output**: SMPL body parameters (22 joints, rotation_6d, 30 fps)
32
+
33
+ ## Citation
34
+
35
+ ```bibtex
36
+ @article{ling2026prism,
37
+ title={PRISM: Streaming Human Motion Generation with Per-Joint Latent Decomposition},
38
+ author={Ling, Zeyu and Shuai, Qing and Zhang, Teng and Li, Shiyang and Han, Bo and Zou, Changqing},
39
+ journal={arXiv preprint arXiv:2603.08590},
40
+ year={2026}
41
+ }
42
+ ```
app.py ADDED
@@ -0,0 +1,365 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import spaces # MUST be first
2
+ import torch
3
+ import numpy as np
4
+ import os
5
+ import sys
6
+ import tempfile
7
+ import subprocess
8
+ import json
9
+ import logging
10
+
11
+ # ── Clone PRISM repo at module scope ──────────────────────────────────────────
12
+ PRISM_REPO_DIR = "/tmp/prism_src"
13
+ if not os.path.isdir(PRISM_REPO_DIR):
14
+ subprocess.run(
15
+ ["git", "clone", "--depth", "1", "https://github.com/ZeyuLing/PRISM.git", PRISM_REPO_DIR],
16
+ check=True,
17
+ capture_output=True,
18
+ )
19
+ sys.path.insert(0, PRISM_REPO_DIR)
20
+
21
+ # ── Download auxiliary files at module scope ────────────────────────────────
22
+ from huggingface_hub import hf_hub_download, snapshot_download
23
+
24
+ # Model weights
25
+ MODEL_ID = "ZeyuLing/PRISM-TP2M-1.4B"
26
+ model_dir = snapshot_download(MODEL_ID)
27
+
28
+ # Tokenizer from the motius-prism repo (same architecture, shares UMT5 tokenizer)
29
+ tokenizer_dir = snapshot_download(
30
+ "ZeyuLing/motius-prism-1.0-humanml3d",
31
+ allow_patterns=["tokenizer/*"],
32
+ )
33
+ tokenizer_path = os.path.join(tokenizer_dir, "tokenizer")
34
+
35
+ # Stats file
36
+ stats_file = hf_hub_download(
37
+ "ZeyuLing/motius-prism-1.0-humanml3d", "motion_stats.json"
38
+ )
39
+
40
+ # SMPL model files
41
+ SMPL_DIR = "/tmp/smpl_models/smplx"
42
+ os.makedirs(SMPL_DIR, exist_ok=True)
43
+
44
+ # SMPLX_NEUTRAL.npz from gvhmr_ckp dataset
45
+ smplx_neutral_path = hf_hub_download(
46
+ "wendell0218/gvhmr_ckp",
47
+ "body_models/smplx/SMPLX_NEUTRAL.npz",
48
+ repo_type="dataset",
49
+ local_dir=SMPL_DIR,
50
+ )
51
+
52
+ # SMPL helper files from GVHMR GitHub repo
53
+ import urllib.request
54
+ GVHMR_RAW = "https://raw.githubusercontent.com/zju3dv/GVHMR/main/hmr4d/utils/body_model"
55
+ for fname in ["smplx2smpl_sparse.pt", "smpl_coco17_J_regressor.pt", "smplx_verts437.pt"]:
56
+ fpath = os.path.join(SMPL_DIR, fname)
57
+ if not os.path.isfile(fpath):
58
+ urllib.request.urlretrieve(f"{GVHMR_RAW}/{fname}", fpath)
59
+
60
+ # ── Set environment variables for the pipeline ──────────────────────────────
61
+ os.environ["PRISM_TOKENIZER_PATH"] = tokenizer_path
62
+ os.environ["PRISM_STATS_FILE"] = stats_file
63
+ os.environ["PRISM_SMPL_MODEL_PATH"] = SMPL_DIR
64
+
65
+ # ── Load the pipeline ────────────────────────────────────────────────────────
66
+ from prism.pipelines.prism_from_pretrained import load_prism_pipeline_from_pretrained
67
+
68
+ pipe = load_prism_pipeline_from_pretrained(
69
+ model_dir,
70
+ device="cuda",
71
+ torch_dtype=torch.bfloat16,
72
+ )
73
+ pipe.transformer.eval()
74
+
75
+ # ── SMPL skeleton for rendering ──────────────────────────────────────────────
76
+ # SMPL 22-joint skeleton connections (parent→child)
77
+ SMPL_SKELETON = [
78
+ (0, 1), (0, 2), (0, 3), (1, 4), (2, 5), (3, 6),
79
+ (4, 7), (5, 8), (6, 9), (7, 10), (8, 11),
80
+ (0, 12), (0, 13), (12, 14), (13, 15), (14, 16),
81
+ (15, 17), (16, 18), (17, 19), (18, 20),
82
+ (9, 21), # right hand → right wrist (using 22nd joint if available)
83
+ ]
84
+
85
+ # Standard SMPL parent indices
86
+ SMPL_PARENTS = [
87
+ -1, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 12, 13, 14, 15, 16, 17, 18, 9
88
+ ]
89
+
90
+ # Joint connections for SMPL 22-joint model
91
+ SMPL_JOINT_PAIRS = [
92
+ (0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6), # right arm
93
+ (0, 7), (7, 8), (8, 9), (9, 10), (10, 11), # left arm (reversed)
94
+ (0, 12), (12, 13), (13, 14), (14, 15), # right leg
95
+ (0, 16), (16, 17), (17, 18), (18, 19), # left leg
96
+ (3, 20), # neck→head
97
+ (9, 21), # left wrist→left hand
98
+ ]
99
+
100
+ # Joint names for labels
101
+ JOINT_NAMES = [
102
+ "root", "r_hip", "r_knee", "r_ankle", "r_foot",
103
+ "l_hip", "l_knee", "l_ankle", "l_foot",
104
+ "spine", "neck", "head",
105
+ "l_collar", "l_shoulder", "l_elbow", "l_wrist", "l_hand",
106
+ "r_collar", "r_shoulder", "r_elbow", "r_wrist", "r_hand"
107
+ ]
108
+
109
+
110
+ def smplx_dict_to_joints(smplx_dict):
111
+ """Convert SMPL-X output dict to 3D joint positions for rendering.
112
+
113
+ Uses the SmplxLite FK model to compute 3D joint positions from
114
+ the axis-angle rotations and translation.
115
+
116
+ Returns:
117
+ joints: (T, 22, 3) numpy array of 3D joint positions
118
+ """
119
+ from prism.models.body_models.smplx_lite import SmplxLite
120
+
121
+ device = "cuda" if torch.cuda.is_available() else "cpu"
122
+
123
+ # Load the SMPL model (same path as pipeline)
124
+ smpl_model = SmplxLite(
125
+ model_path=SMPL_DIR,
126
+ gender="neutral",
127
+ num_betas=10,
128
+ ).to(device=device, dtype=torch.float32)
129
+ smpl_model.eval()
130
+
131
+ transl = torch.from_numpy(smplx_dict["transl"]).float().to(device) # (T, 3)
132
+ global_orient = torch.from_numpy(smplx_dict["global_orient"]).float().to(device) # (T, 3)
133
+ body_pose = torch.from_numpy(smplx_dict["body_pose"]).float().to(device) # (T, 63)
134
+
135
+ T = transl.shape[0]
136
+ # Process in chunks to avoid OOM
137
+ chunk = 128
138
+ all_joints = []
139
+ with torch.no_grad():
140
+ for i in range(0, T, chunk):
141
+ t = transl[i:i+chunk] # (C, 3)
142
+ go = global_orient[i:i+chunk] # (C, 3)
143
+ bp = body_pose[i:i+chunk] # (C, 63)
144
+ C = t.shape[0]
145
+ joints, _, _ = smpl_model.fk(
146
+ transl=t.unsqueeze(0), # (1, C, 3)
147
+ global_orient=go.unsqueeze(0), # (1, C, 3)
148
+ body_pose=bp.unsqueeze(0), # (1, C, 63)
149
+ betas=torch.zeros(1, C, 10, device=device, dtype=torch.float32),
150
+ )
151
+ # joints: (1, C, 22, 3)
152
+ all_joints.append(joints.squeeze(0).cpu().numpy())
153
+
154
+ joints = np.concatenate(all_joints, axis=0) # (T, 22, 3)
155
+ return joints
156
+
157
+
158
+ def render_skeleton_video(joints, fps=30, figsize=(6, 6), dpi=100):
159
+ """Render a (T, 22, 3) joint array to an MP4 video using matplotlib.
160
+
161
+ Returns path to the temporary video file.
162
+ """
163
+ import matplotlib
164
+ matplotlib.use("Agg")
165
+ import matplotlib.pyplot as plt
166
+ from mpl_toolkits.mplot3d import Axes3D
167
+ import imageio
168
+
169
+ T, J, _ = joints.shape
170
+
171
+ # Compute global coordinate range for consistent view
172
+ all_pts = joints.reshape(-1, 3)
173
+ mins = all_pts.min(axis=0)
174
+ maxs = all_pts.max(axis=0)
175
+ center = (mins + maxs) / 2
176
+ extent = (maxs - mins).max() / 2 * 1.2
177
+
178
+ # Create temporary directory for frames
179
+ tmpdir = tempfile.mkdtemp()
180
+ frame_paths = []
181
+
182
+ for t in range(T):
183
+ fig = plt.figure(figsize=figsize, dpi=dpi)
184
+ ax = fig.add_subplot(111, projection="3d")
185
+
186
+ pts = joints[t] # (22, 3)
187
+
188
+ # Plot skeleton lines
189
+ for (i, j) in SMPL_JOINT_PAIRS:
190
+ if i < J and j < J:
191
+ ax.plot(
192
+ [pts[i, 0], pts[j, 0]],
193
+ [pts[i, 1], pts[j, 1]],
194
+ [pts[i, 2], pts[j, 2]],
195
+ color="steelblue",
196
+ linewidth=2.5,
197
+ )
198
+
199
+ # Plot joints
200
+ ax.scatter(pts[:, 0], pts[:, 1], pts[:, 2], c="firebrick", s=30, zorder=5)
201
+
202
+ # Set consistent view
203
+ ax.set_xlim(center[0] - extent, center[0] + extent)
204
+ ax.set_ylim(center[1] - extent, center[1] + extent)
205
+ ax.set_zlim(center[2] - extent, center[2] + extent)
206
+ ax.set_xlabel("X")
207
+ ax.set_ylabel("Z")
208
+ ax.set_zlabel("Y")
209
+ ax.set_title(f"Frame {t+1}/{T}", fontsize=10)
210
+ ax.view_init(elev=15, azim=-60)
211
+
212
+ # Elev=15, looking from a good angle
213
+ ax.set_box_aspect([1, 1, 1])
214
+
215
+ fpath = os.path.join(tmpdir, f"frame_{t:04d}.png")
216
+ fig.savefig(fpath, bbox_inches="tight", pad_inches=0.1)
217
+ plt.close(fig)
218
+ frame_paths.append(fpath)
219
+
220
+ # Create video
221
+ video_path = os.path.join(tmpdir, "motion.mp4")
222
+ writer = imageio.get_writer(video_path, fps=fps, codec="libx264")
223
+ for fpath in frame_paths:
224
+ writer.append_data(imageio.imread(fpath))
225
+ writer.close()
226
+
227
+ # Clean up frame images (keep video)
228
+ for fpath in frame_paths:
229
+ os.remove(fpath)
230
+
231
+ return video_path
232
+
233
+
234
+ @spaces.GPU(duration=120)
235
+ def generate_motion(
236
+ prompt: str,
237
+ num_frames: int = 129,
238
+ guidance_scale: float = 5.0,
239
+ num_inference_steps: int = 50,
240
+ seed: int = 0,
241
+ ):
242
+ """Generate a human motion sequence from a text prompt using PRISM.
243
+
244
+ Args:
245
+ prompt: Text description of the motion to generate.
246
+ num_frames: Number of motion frames (higher = longer motion, ~4s at 30fps).
247
+ guidance_scale: Classifier-free guidance scale (higher = more text-adherent).
248
+ num_inference_steps: Number of denoising steps (higher = better quality).
249
+ seed: Random seed for reproducibility.
250
+ """
251
+ # Set seed
252
+ torch.manual_seed(seed)
253
+ if torch.cuda.is_available():
254
+ torch.cuda.manual_seed(seed)
255
+
256
+ # Generate motion
257
+ smplx_dict = pipe(
258
+ prompts=prompt,
259
+ negative_prompt="",
260
+ num_frames_per_segment=num_frames,
261
+ num_joints=23,
262
+ guidance_scale=guidance_scale,
263
+ num_inference_steps=num_inference_steps,
264
+ )
265
+
266
+ # Convert to 3D joint positions
267
+ joints = smplx_dict_to_joints(smplx_dict)
268
+
269
+ # Render to video
270
+ video_path = render_skeleton_video(joints, fps=30)
271
+
272
+ # Also save the motion data as npz
273
+ output_npz = os.path.join(tempfile.gettempdir(), "motion_output.npz")
274
+ np.savez(output_npz, **smplx_dict)
275
+
276
+ num_frames_actual = smplx_dict["transl"].shape[0]
277
+ duration_sec = num_frames_actual / 30.0
278
+ info = f"Generated {num_frames_actual} frames ({duration_sec:.1f}s) at 30 FPS"
279
+
280
+ return video_path, output_npz, info
281
+
282
+
283
+ import gradio as gr
284
+
285
+ with gr.Blocks(theme=gr.themes.Citrus()) as demo:
286
+ gr.Markdown(
287
+ """
288
+ # 🏃 PRISM: Text-to-Motion Generation
289
+
290
+ Generate human motion sequences from text prompts using
291
+ [PRISM](https://github.com/ZeyuLing/PRISM) — a flow-matching DiT transformer
292
+ with per-joint latent decomposition for streaming motion synthesis.
293
+
294
+ The model outputs SMPL body parameters (22 joints) rendered as a 3D skeleton animation.
295
+ """
296
+ )
297
+
298
+ with gr.Row():
299
+ with gr.Column(scale=1):
300
+ prompt_input = gr.Textbox(
301
+ label="Motion Description",
302
+ placeholder="e.g. A person walks forward and waves their hand.",
303
+ lines=3,
304
+ value="A person walks forward and waves their hand.",
305
+ )
306
+ generate_btn = gr.Button("Generate Motion", variant="primary", size="lg")
307
+
308
+ with gr.Accordion("Advanced Settings", open=False):
309
+ num_frames_slider = gr.Slider(
310
+ label="Number of Frames",
311
+ minimum=33,
312
+ maximum=257,
313
+ step=4,
314
+ value=129,
315
+ info="Higher = longer motion (~4.3s at 30fps for 129 frames)",
316
+ )
317
+ guidance_slider = gr.Slider(
318
+ label="Guidance Scale",
319
+ minimum=1.0,
320
+ maximum=15.0,
321
+ step=0.5,
322
+ value=5.0,
323
+ info="Higher = more text-adherent, lower = more diverse",
324
+ )
325
+ steps_slider = gr.Slider(
326
+ label="Inference Steps",
327
+ minimum=10,
328
+ maximum=100,
329
+ step=5,
330
+ value=50,
331
+ info="Higher = better quality, slower",
332
+ )
333
+ seed_input = gr.Number(
334
+ label="Seed",
335
+ value=0,
336
+ precision=0,
337
+ )
338
+
339
+ with gr.Column(scale=1):
340
+ video_output = gr.Video(label="Generated Motion", format="mp4")
341
+ info_output = gr.Textbox(label="Info", interactive=False)
342
+ npz_output = gr.File(label="Motion Data (.npz)")
343
+
344
+ gr.Examples(
345
+ examples=[
346
+ ["A person walks forward and waves their hand.", 129, 5.0, 50, 0],
347
+ ["A person performs a backflip.", 129, 7.0, 50, 42],
348
+ ["A person sits down on the floor cross-legged.", 129, 5.0, 50, 7],
349
+ ["A person dances happily, spinning around.", 193, 6.0, 50, 123],
350
+ ["A person kicks a soccer ball.", 97, 5.0, 50, 99],
351
+ ],
352
+ inputs=[prompt_input, num_frames_slider, guidance_slider, steps_slider, seed_input],
353
+ fn=generate_motion,
354
+ outputs=[video_output, npz_output, info_output],
355
+ cache_examples=True,
356
+ cache_mode="lazy",
357
+ )
358
+
359
+ generate_btn.click(
360
+ fn=generate_motion,
361
+ inputs=[prompt_input, num_frames_slider, guidance_slider, steps_slider, seed_input],
362
+ outputs=[video_output, npz_output, info_output],
363
+ )
364
+
365
+ demo.launch(mcp_server=True)
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PRISM dependencies
2
+ # Do NOT list gradio, spaces, or huggingface_hub (preinstalled)
3
+ diffusers>=0.32.0
4
+ transformers>=4.45.0
5
+ accelerate
6
+ mmengine
7
+ einops
8
+ smplx
9
+ matplotlib
10
+ imageio
11
+ imageio-ffmpeg
12
+ safetensors