openfree commited on
Commit
68ecd49
·
verified ·
1 Parent(s): 8e24f98

Deploy ZeroGPU ARDY motion generation API

Browse files
Files changed (5) hide show
  1. README.md +20 -7
  2. __pycache__/app.cpython-312.pyc +0 -0
  3. app.py +214 -0
  4. packages.txt +3 -0
  5. requirements.txt +5 -0
README.md CHANGED
@@ -1,13 +1,26 @@
1
  ---
2
- title: CozyClay ZeroGPU
3
- emoji: 🏃
4
- colorFrom: purple
5
- colorTo: pink
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: CozyClay ZeroGPU Motion Engine
3
+ emoji: 🎬
4
+ colorFrom: yellow
5
+ colorTo: gray
6
  sdk: gradio
7
+ sdk_version: 6.8.0
8
+ python_version: 3.12.12
9
  app_file: app.py
10
  pinned: false
11
+ license: gpl-3.0
12
+ suggested_hardware: zero-a10g
13
+ preload_from_hub:
14
+ - NousResearch/Meta-Llama-3-8B-Instruct
15
+ - McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp-supervised
16
+ - nvidia/ARDY-Core-RP-20FPS-Horizon40
17
  ---
18
 
19
+ # CozyClay ZeroGPU Motion Engine
20
+
21
+ GPU API for the [CozyClay Studio](https://huggingface.co/spaces/VIDraft/CozyClay).
22
+ It turns a natural-language instruction into a newly sampled ARDY 3D skeletal
23
+ motion and returns the NPZ motion to the browser editor.
24
+
25
+ CozyClay is GPL-3.0-or-later. NVIDIA ARDY is an independent Apache-2.0 project;
26
+ its released weights have their own NVIDIA Open Model License.
__pycache__/app.cpython-312.pyc ADDED
Binary file (11.4 kB). View file
 
app.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import tempfile
4
+ from pathlib import Path
5
+
6
+ import gradio as gr
7
+ import numpy as np
8
+ import spaces
9
+ import torch
10
+
11
+ MODEL_NAME = "ARDY-Core-RP-20FPS-Horizon40"
12
+ PUBLIC_LLAMA = "NousResearch/Meta-Llama-3-8B-Instruct"
13
+ _model = None
14
+
15
+
16
+ def _load_model():
17
+ """Load once into ZeroGPU's CUDA-emulated process, then reuse it."""
18
+ global _model
19
+ if _model is not None:
20
+ return _model
21
+
22
+ from ardy.model.load_model import TEXT_ENCODER_PRESETS, load_model
23
+ from ardy.model.llm2vec.llm2vec import LLM2Vec
24
+
25
+ # ARDY's released LLM2Vec adapter normally names Meta's gated Llama repo.
26
+ # This public mirror has identical base weights and requires no visitor token.
27
+ TEXT_ENCODER_PRESETS["llm2vec"]["kwargs"]["base_model_name_or_path"] = PUBLIC_LLAMA
28
+
29
+ # Preserve the instruction wrapper used during ARDY training even though
30
+ # the public mirror has a different repository name in its config.
31
+ original_prepare = LLM2Vec.prepare_for_tokenization
32
+ if not getattr(original_prepare, "_cozyclay_public_llama", False):
33
+ def prepare_for_tokenization(self, text):
34
+ model_name = str(getattr(self.model.config, "_name_or_path", ""))
35
+ if model_name == PUBLIC_LLAMA:
36
+ return "<|start_header_id|>user<|end_header_id|>\n\n" + text.strip() + "<|eot_id|>"
37
+ return original_prepare(self, text)
38
+
39
+ prepare_for_tokenization._cozyclay_public_llama = True
40
+ LLM2Vec.prepare_for_tokenization = prepare_for_tokenization
41
+
42
+ _model = load_model(
43
+ MODEL_NAME,
44
+ device="cuda:0",
45
+ text_encoder_mode="local",
46
+ ).eval()
47
+ return _model
48
+
49
+
50
+ def _validate_request(raw):
51
+ try:
52
+ body = json.loads(raw)
53
+ except Exception as exc:
54
+ raise gr.Error(f"Invalid request JSON: {exc}") from exc
55
+ if not isinstance(body, dict):
56
+ raise gr.Error("Request must be a JSON object.")
57
+ prompt = str(body.get("prompt", "")).strip()
58
+ if not prompt or len(prompt) > 500:
59
+ raise gr.Error("Prompt must contain 1 to 500 characters.")
60
+ duration = float(body.get("duration", 5.0))
61
+ if not 0.15 <= duration <= 10.0:
62
+ raise gr.Error("Duration must be between 0.15 and 10 seconds.")
63
+ seed = body.get("seed")
64
+ if seed is not None:
65
+ seed = int(seed)
66
+ if not 0 <= seed <= 2**31 - 1:
67
+ raise gr.Error("Seed is outside the supported range.")
68
+ if body.get("poses") or body.get("motionEdit") or body.get("segments"):
69
+ raise gr.Error(
70
+ "This first ZeroGPU release supports prompt and root-path generation. "
71
+ "Pose pins, prompt schedules, and motion edits are being enabled next."
72
+ )
73
+ waypoints = body.get("waypoints") or []
74
+ if not isinstance(waypoints, list) or len(waypoints) > 32:
75
+ raise gr.Error("Waypoints must be a list with at most 32 entries.")
76
+ return prompt, duration, seed, waypoints
77
+
78
+
79
+ def _duration(raw):
80
+ try:
81
+ seconds = float(json.loads(raw).get("duration", 5.0))
82
+ except Exception:
83
+ seconds = 5.0
84
+ return min(300, max(120, int(90 + seconds * 24)))
85
+
86
+
87
+ @spaces.GPU(duration=_duration, size="large")
88
+ def generate_motion(request_json):
89
+ """Generate a real ARDY motion NPZ for the CozyClay browser client."""
90
+ prompt, duration, seed, waypoints = _validate_request(request_json)
91
+ model = _load_model()
92
+
93
+ from ardy.model.loading import get_env_var
94
+ from ardy.model.registry import resolve_model_name
95
+ from ardy.motion_rep.tools import length_to_mask
96
+ from ardy.postprocess import post_process_motion
97
+ from ardy.skeleton import SOMASkeleton30
98
+ from ardy.tools import seed_everything, to_numpy
99
+
100
+ device = "cuda:0"
101
+ skeleton = model.skeleton
102
+ fps = model.motion_rep.fps
103
+ frames = int(duration * fps)
104
+ if seed is not None:
105
+ seed_everything(seed)
106
+
107
+ constraints = []
108
+ if waypoints:
109
+ from ardy.constraints import Root2DConstraintSet
110
+ ordered = sorted(waypoints, key=lambda item: int(item["frame"]))
111
+ frame_ids = [int(item["frame"]) for item in ordered]
112
+ if frame_ids[0] < 0 or frame_ids[-1] >= frames or len(frame_ids) != len(set(frame_ids)):
113
+ raise gr.Error(f"Waypoint frames must be unique and inside 0..{frames - 1}.")
114
+ xz = [[float(item["x"]), float(item["z"])] for item in ordered]
115
+ constraints.append(
116
+ Root2DConstraintSet(
117
+ skeleton,
118
+ frame_indices=torch.tensor(frame_ids),
119
+ root_2d=torch.tensor(xz, device=device, dtype=torch.float32),
120
+ global_root_heading=None,
121
+ )
122
+ )
123
+
124
+ lengths = torch.tensor([frames], device=device)
125
+ observed, mask = None, None
126
+ if constraints:
127
+ observed, mask = model.motion_rep.create_conditions_from_constraints_batched(
128
+ constraints, lengths, to_normalize=True, device=device
129
+ )
130
+
131
+ patch = model.num_frames_per_token
132
+ max_window = (int(10 * fps) // patch) * patch
133
+ history = ((max_window - model.gen_horizon_len) // patch) * patch
134
+ with torch.inference_mode():
135
+ motion = model(
136
+ [prompt],
137
+ frames,
138
+ num_denoising_steps=int(model.diffusion.num_base_steps),
139
+ pad_mask=length_to_mask(lengths),
140
+ first_heading_angle=torch.zeros(1, device=device),
141
+ motion_mask=mask,
142
+ observed_motion=observed,
143
+ cfg_weight=(2.0, 2.0),
144
+ progress_bar=lambda values: values,
145
+ crop_history_length=history,
146
+ )
147
+ output = model.motion_rep.inverse(motion, is_normalized=True)
148
+
149
+ resolved = resolve_model_name(MODEL_NAME, checkpoints_dir=get_env_var("CHECKPOINTS_DIR"))
150
+ if "g1" not in resolved.lower():
151
+ output.update(
152
+ post_process_motion(
153
+ output["local_rot_mats"], output["root_positions"],
154
+ output["foot_contacts"], skeleton,
155
+ constraint_lst=constraints or None,
156
+ )
157
+ )
158
+ if isinstance(skeleton, SOMASkeleton30):
159
+ output = skeleton.output_to_SOMASkeleton77(output)
160
+ output = to_numpy(output)
161
+ arrays = {
162
+ key: (value[0] if hasattr(value, "shape") and value.ndim > 0 and value.shape[0] == 1 else value)
163
+ for key, value in output.items()
164
+ }
165
+ arrays["fps"] = np.asarray(fps)
166
+ arrays["text"] = np.asarray(prompt)
167
+
168
+ out_dir = Path(tempfile.mkdtemp(prefix="cozyclay-"))
169
+ output_path = out_dir / "generated-motion.npz"
170
+ np.savez(output_path, **arrays)
171
+ report = {
172
+ "target_space": "skeleton_joint_center",
173
+ "frames": frames,
174
+ "fps": int(fps),
175
+ "model": resolved,
176
+ "prompt": prompt,
177
+ "bytes": output_path.stat().st_size,
178
+ "output_name": output_path.name,
179
+ "waypoints": len(waypoints),
180
+ "device": torch.cuda.get_device_name(0),
181
+ }
182
+ return str(output_path), report
183
+
184
+
185
+ with gr.Blocks(title="CozyClay ZeroGPU Motion Engine") as demo:
186
+ gr.Markdown(
187
+ "# CozyClay · ZeroGPU Motion Engine\n"
188
+ "Natural-language ARDY motion generation for the browser-based CozyClay studio. "
189
+ "Use the full editor at **[VIDraft/CozyClay](https://huggingface.co/spaces/VIDraft/CozyClay)**."
190
+ )
191
+ with gr.Row():
192
+ prompt = gr.Textbox(
193
+ value="A person walks forward, turns left, and waves.",
194
+ label="Motion prompt",
195
+ )
196
+ duration = gr.Slider(1, 10, value=5, step=0.5, label="Seconds")
197
+ run = gr.Button("Generate 3D motion", variant="primary")
198
+ motion_file = gr.File(label="Generated ARDY NPZ")
199
+ report = gr.JSON(label="Generation report")
200
+
201
+ def make_request(text, seconds):
202
+ return json.dumps({"prompt": text, "duration": seconds})
203
+
204
+ request = gr.Textbox(visible=False)
205
+ run.click(make_request, [prompt, duration], request, queue=False).then(
206
+ generate_motion,
207
+ request,
208
+ [motion_file, report],
209
+ api_name="generate_motion",
210
+ concurrency_limit=1,
211
+ )
212
+
213
+ demo.queue(default_concurrency_limit=1)
214
+
packages.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ cmake
2
+ g++
3
+
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ spaces
2
+ gradio==6.8.0
3
+ torch==2.10.0
4
+ git+https://github.com/nv-tlabs/ardy.git@693f74d13b3d04a0a22ce127ee79c929dd89756b
5
+