Spaces:
Running on Zero
Running on Zero
Deploy ARDY ZeroGPU Blender motion API
Browse files
app.py
CHANGED
|
@@ -526,17 +526,37 @@ def generate_blender_motion(
|
|
| 526 |
randomize_seed,
|
| 527 |
None,
|
| 528 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 529 |
motion_tensor = torch.from_numpy(motion_np).to("cuda")
|
| 530 |
-
out = _decode_motion(
|
| 531 |
-
r = RIGS[
|
| 532 |
skeleton = r["skeleton"]
|
| 533 |
joint_names = list(skeleton.bone_order_names)
|
| 534 |
parents = list(r["parents"])
|
| 535 |
rest_positions = to_numpy(skeleton.neutral_joints)
|
| 536 |
|
| 537 |
output_dir = Path(tempfile.mkdtemp(prefix="ardy_blender_"))
|
| 538 |
-
npz_path = output_dir / "
|
| 539 |
-
bvh_path = output_dir / "
|
| 540 |
metadata_path = output_dir / "metadata.json"
|
| 541 |
|
| 542 |
np.savez_compressed(
|
|
@@ -545,7 +565,7 @@ def generate_blender_motion(
|
|
| 545 |
fps=np.asarray(r["fps"], dtype=np.float32),
|
| 546 |
text=np.asarray(prompt),
|
| 547 |
seed=np.asarray(seed, dtype=np.int64),
|
| 548 |
-
rig=np.asarray(
|
| 549 |
joint_names=np.asarray(joint_names),
|
| 550 |
parents=np.asarray(parents, dtype=np.int32),
|
| 551 |
rest_positions=np.asarray(rest_positions, dtype=np.float32),
|
|
@@ -562,15 +582,23 @@ def generate_blender_motion(
|
|
| 562 |
metadata_path.write_text(
|
| 563 |
json.dumps(
|
| 564 |
{
|
| 565 |
-
"model":
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 569 |
"fps": r["fps"],
|
| 570 |
"frames": int(out["posed_joints"].shape[0]),
|
|
|
|
|
|
|
|
|
|
| 571 |
"joints": joint_names,
|
| 572 |
"parents": parents,
|
| 573 |
"coordinate_system": "Y-up, metric",
|
|
|
|
| 574 |
"post_processing": False,
|
| 575 |
},
|
| 576 |
ensure_ascii=False,
|
|
@@ -578,100 +606,18 @@ def generate_blender_motion(
|
|
| 578 |
),
|
| 579 |
encoding="utf-8",
|
| 580 |
)
|
| 581 |
-
return str(bvh_path), str(npz_path), str(metadata_path), seed
|
| 582 |
|
| 583 |
|
| 584 |
@spaces.GPU
|
| 585 |
-
def
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
seed: int = 0,
|
| 590 |
-
randomize_seed: bool = True,
|
| 591 |
) -> tuple[str, str, str, int]:
|
| 592 |
-
"""
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
r = RIGS["human"]
|
| 596 |
-
segments = normalize_timeline(timeline, fps=float(r["fps"]))
|
| 597 |
-
motion_np = None
|
| 598 |
-
resolved_seed = int(seed)
|
| 599 |
-
|
| 600 |
-
for index, segment in enumerate(segments):
|
| 601 |
-
start_frame = 0 if motion_np is None else int(motion_np.shape[1])
|
| 602 |
-
_, resolved_seed, motion_np = _core_generate(
|
| 603 |
-
"human",
|
| 604 |
-
segment["prompt"],
|
| 605 |
-
segment["duration_seconds"],
|
| 606 |
-
diffusion_steps,
|
| 607 |
-
cfg_weight,
|
| 608 |
-
resolved_seed,
|
| 609 |
-
bool(randomize_seed) if index == 0 else False,
|
| 610 |
-
motion_np,
|
| 611 |
-
)
|
| 612 |
-
end_frame = int(motion_np.shape[1])
|
| 613 |
-
segment["start_frame"] = start_frame
|
| 614 |
-
segment["end_frame"] = end_frame
|
| 615 |
-
segment["duration_seconds"] = (end_frame - start_frame) / float(r["fps"])
|
| 616 |
-
|
| 617 |
-
if motion_np is None:
|
| 618 |
-
raise gr.Error("The timeline did not contain any motion segments.")
|
| 619 |
-
|
| 620 |
-
motion_tensor = torch.from_numpy(motion_np).to("cuda")
|
| 621 |
-
out = _decode_motion("human", motion_tensor)
|
| 622 |
-
skeleton = r["skeleton"]
|
| 623 |
-
joint_names = list(skeleton.bone_order_names)
|
| 624 |
-
parents = list(r["parents"])
|
| 625 |
-
rest_positions = to_numpy(skeleton.neutral_joints)
|
| 626 |
-
|
| 627 |
-
output_dir = Path(tempfile.mkdtemp(prefix="ardy_timeline_"))
|
| 628 |
-
npz_path = output_dir / "ardy_timeline_motion.npz"
|
| 629 |
-
bvh_path = output_dir / "ardy_timeline_motion.bvh"
|
| 630 |
-
metadata_path = output_dir / "timeline_metadata.json"
|
| 631 |
-
|
| 632 |
-
timeline_json = json.dumps(segments, ensure_ascii=False)
|
| 633 |
-
np.savez_compressed(
|
| 634 |
-
npz_path,
|
| 635 |
-
**out,
|
| 636 |
-
fps=np.asarray(r["fps"], dtype=np.float32),
|
| 637 |
-
text=np.asarray(timeline_json),
|
| 638 |
-
seed=np.asarray(resolved_seed, dtype=np.int64),
|
| 639 |
-
rig=np.asarray("human"),
|
| 640 |
-
joint_names=np.asarray(joint_names),
|
| 641 |
-
parents=np.asarray(parents, dtype=np.int32),
|
| 642 |
-
rest_positions=np.asarray(rest_positions, dtype=np.float32),
|
| 643 |
-
)
|
| 644 |
-
write_bvh(
|
| 645 |
-
bvh_path,
|
| 646 |
-
joint_names=joint_names,
|
| 647 |
-
parents=parents,
|
| 648 |
-
rest_positions=rest_positions,
|
| 649 |
-
local_rot_mats=out["local_rot_mats"],
|
| 650 |
-
root_positions=out["root_positions"],
|
| 651 |
-
fps=r["fps"],
|
| 652 |
-
)
|
| 653 |
-
metadata_path.write_text(
|
| 654 |
-
json.dumps(
|
| 655 |
-
{
|
| 656 |
-
"model": "nvidia/ARDY-Core-RP-20FPS-Horizon40",
|
| 657 |
-
"rig": "human",
|
| 658 |
-
"seed": resolved_seed,
|
| 659 |
-
"fps": r["fps"],
|
| 660 |
-
"frames": int(out["posed_joints"].shape[0]),
|
| 661 |
-
"duration_seconds": float(out["posed_joints"].shape[0]) / float(r["fps"]),
|
| 662 |
-
"segments": segments,
|
| 663 |
-
"joints": joint_names,
|
| 664 |
-
"parents": parents,
|
| 665 |
-
"coordinate_system": "Y-up, metric",
|
| 666 |
-
"generation_mode": "autoregressive timeline",
|
| 667 |
-
"post_processing": False,
|
| 668 |
-
},
|
| 669 |
-
ensure_ascii=False,
|
| 670 |
-
indent=2,
|
| 671 |
-
),
|
| 672 |
-
encoding="utf-8",
|
| 673 |
-
)
|
| 674 |
-
return str(bvh_path), str(npz_path), str(metadata_path), resolved_seed
|
| 675 |
|
| 676 |
|
| 677 |
# -----------------------------------------------------------------------------
|
|
@@ -1149,45 +1095,16 @@ with gr.Blocks() as demo:
|
|
| 1149 |
|
| 1150 |
with gr.Accordion("Blender export", open=False):
|
| 1151 |
gr.Markdown(
|
| 1152 |
-
"
|
| 1153 |
-
"
|
|
|
|
| 1154 |
)
|
| 1155 |
-
export_blender = gr.Button("
|
| 1156 |
with gr.Row():
|
| 1157 |
bvh_file = gr.File(label="Blender BVH")
|
| 1158 |
npz_file = gr.File(label="ARDY NPZ")
|
| 1159 |
metadata_file = gr.File(label="Metadata")
|
| 1160 |
|
| 1161 |
-
with gr.Accordion("Continuous action timeline", open=True):
|
| 1162 |
-
gr.Markdown(
|
| 1163 |
-
"Add one short instruction per row. ARDY carries the previous pose and "
|
| 1164 |
-
"root trajectory into the next row, producing one continuous motion. "
|
| 1165 |
-
"Each segment is 2–4 seconds (the model's generation window is 2 seconds), "
|
| 1166 |
-
"and the combined duration may not exceed 8 seconds."
|
| 1167 |
-
)
|
| 1168 |
-
timeline_input = gr.Dataframe(
|
| 1169 |
-
headers=["Action prompt", "Duration (seconds)"],
|
| 1170 |
-
datatype=["str", "number"],
|
| 1171 |
-
type="array",
|
| 1172 |
-
row_count=(4, "dynamic"),
|
| 1173 |
-
col_count=(2, "fixed"),
|
| 1174 |
-
value=[
|
| 1175 |
-
["Walk forward two steps.", 2.0],
|
| 1176 |
-
["Turn around smoothly.", 2.0],
|
| 1177 |
-
["Wave the right hand.", 2.0],
|
| 1178 |
-
["Sit down on the ground.", 2.0],
|
| 1179 |
-
],
|
| 1180 |
-
label="Action segments",
|
| 1181 |
-
)
|
| 1182 |
-
generate_timeline = gr.Button(
|
| 1183 |
-
"Generate continuous timeline",
|
| 1184 |
-
variant="primary",
|
| 1185 |
-
)
|
| 1186 |
-
with gr.Row():
|
| 1187 |
-
timeline_bvh = gr.File(label="Continuous Blender BVH")
|
| 1188 |
-
timeline_npz = gr.File(label="Continuous ARDY NPZ")
|
| 1189 |
-
timeline_metadata = gr.File(label="Timeline metadata")
|
| 1190 |
-
|
| 1191 |
gr.Examples(
|
| 1192 |
examples=EXAMPLES,
|
| 1193 |
inputs=[prompt, rig, duration],
|
|
@@ -1217,23 +1134,15 @@ with gr.Blocks() as demo:
|
|
| 1217 |
cont.click(fn=ui_continue, inputs=_gen_inputs + [seq_state],
|
| 1218 |
outputs=[player, seed, seq_state], api_name=False)
|
| 1219 |
export_blender.click(
|
| 1220 |
-
fn=
|
| 1221 |
-
inputs=[
|
| 1222 |
outputs=[bvh_file, npz_file, metadata_file, seed],
|
| 1223 |
api_name=False,
|
| 1224 |
)
|
| 1225 |
-
generate_timeline.click(
|
| 1226 |
-
fn=generate_timeline_motion,
|
| 1227 |
-
inputs=[timeline_input, diffusion_steps, cfg_weight, seed, randomize_seed],
|
| 1228 |
-
outputs=[timeline_bvh, timeline_npz, timeline_metadata, seed],
|
| 1229 |
-
api_name=False,
|
| 1230 |
-
concurrency_limit=1,
|
| 1231 |
-
)
|
| 1232 |
|
| 1233 |
# Clean single-shot endpoint for the HTTP API / MCP tool (no session state).
|
| 1234 |
gr.api(generate_motion, api_name="generate")
|
| 1235 |
gr.api(generate_blender_motion, api_name="generate_blender")
|
| 1236 |
-
gr.api(generate_timeline_motion, api_name="generate_timeline")
|
| 1237 |
|
| 1238 |
demo.queue()
|
| 1239 |
|
|
|
|
| 526 |
randomize_seed,
|
| 527 |
None,
|
| 528 |
)
|
| 529 |
+
return _export_motion_files("human", motion_np, prompt, seed)
|
| 530 |
+
|
| 531 |
+
|
| 532 |
+
def _export_motion_files(
|
| 533 |
+
rig: str,
|
| 534 |
+
motion_np: np.ndarray,
|
| 535 |
+
prompt: str,
|
| 536 |
+
seed: int,
|
| 537 |
+
) -> tuple[str, str, str, int]:
|
| 538 |
+
"""Decode an accumulated ARDY sequence and write Blender export files."""
|
| 539 |
+
rig_key = _normalize_rig(rig)
|
| 540 |
+
if motion_np is None:
|
| 541 |
+
raise gr.Error(
|
| 542 |
+
"Generate a motion first, then use Continue to extend it before exporting."
|
| 543 |
+
)
|
| 544 |
+
|
| 545 |
+
motion_np = np.asarray(motion_np)
|
| 546 |
+
if motion_np.ndim != 3 or motion_np.shape[0] != 1 or motion_np.shape[1] < 1:
|
| 547 |
+
raise gr.Error(f"Invalid ARDY sequence state: shape={motion_np.shape}")
|
| 548 |
+
|
| 549 |
motion_tensor = torch.from_numpy(motion_np).to("cuda")
|
| 550 |
+
out = _decode_motion(rig_key, motion_tensor)
|
| 551 |
+
r = RIGS[rig_key]
|
| 552 |
skeleton = r["skeleton"]
|
| 553 |
joint_names = list(skeleton.bone_order_names)
|
| 554 |
parents = list(r["parents"])
|
| 555 |
rest_positions = to_numpy(skeleton.neutral_joints)
|
| 556 |
|
| 557 |
output_dir = Path(tempfile.mkdtemp(prefix="ardy_blender_"))
|
| 558 |
+
npz_path = output_dir / f"ardy_{rig_key}_motion.npz"
|
| 559 |
+
bvh_path = output_dir / f"ardy_{rig_key}_motion.bvh"
|
| 560 |
metadata_path = output_dir / "metadata.json"
|
| 561 |
|
| 562 |
np.savez_compressed(
|
|
|
|
| 565 |
fps=np.asarray(r["fps"], dtype=np.float32),
|
| 566 |
text=np.asarray(prompt),
|
| 567 |
seed=np.asarray(seed, dtype=np.int64),
|
| 568 |
+
rig=np.asarray(rig_key),
|
| 569 |
joint_names=np.asarray(joint_names),
|
| 570 |
parents=np.asarray(parents, dtype=np.int32),
|
| 571 |
rest_positions=np.asarray(rest_positions, dtype=np.float32),
|
|
|
|
| 582 |
metadata_path.write_text(
|
| 583 |
json.dumps(
|
| 584 |
{
|
| 585 |
+
"model": (
|
| 586 |
+
"nvidia/ARDY-Core-RP-20FPS-Horizon40"
|
| 587 |
+
if rig_key == "human"
|
| 588 |
+
else "nvidia/ARDY-G1-RP-25FPS-Horizon52"
|
| 589 |
+
),
|
| 590 |
+
"rig": rig_key,
|
| 591 |
+
"latest_prompt": prompt,
|
| 592 |
+
"seed": int(seed),
|
| 593 |
"fps": r["fps"],
|
| 594 |
"frames": int(out["posed_joints"].shape[0]),
|
| 595 |
+
"duration_seconds": (
|
| 596 |
+
float(out["posed_joints"].shape[0]) / float(r["fps"])
|
| 597 |
+
),
|
| 598 |
"joints": joint_names,
|
| 599 |
"parents": parents,
|
| 600 |
"coordinate_system": "Y-up, metric",
|
| 601 |
+
"source": "current accumulated Generate/Continue sequence",
|
| 602 |
"post_processing": False,
|
| 603 |
},
|
| 604 |
ensure_ascii=False,
|
|
|
|
| 606 |
),
|
| 607 |
encoding="utf-8",
|
| 608 |
)
|
| 609 |
+
return str(bvh_path), str(npz_path), str(metadata_path), int(seed)
|
| 610 |
|
| 611 |
|
| 612 |
@spaces.GPU
|
| 613 |
+
def export_current_sequence(
|
| 614 |
+
state: object,
|
| 615 |
+
rig: str = DEFAULT_RIG,
|
| 616 |
+
prompt: str = "",
|
| 617 |
seed: int = 0,
|
|
|
|
| 618 |
) -> tuple[str, str, str, int]:
|
| 619 |
+
"""Export the complete sequence currently held by Generate/Continue."""
|
| 620 |
+
return _export_motion_files(rig, state, prompt, seed)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 621 |
|
| 622 |
|
| 623 |
# -----------------------------------------------------------------------------
|
|
|
|
| 1095 |
|
| 1096 |
with gr.Accordion("Blender export", open=False):
|
| 1097 |
gr.Markdown(
|
| 1098 |
+
"Export the **complete motion currently in the player**, including "
|
| 1099 |
+
"every segment added with **Continue ▸**, as Blender **BVH**, "
|
| 1100 |
+
"full-fidelity **NPZ**, and JSON metadata."
|
| 1101 |
)
|
| 1102 |
+
export_blender = gr.Button("Export current sequence", variant="secondary")
|
| 1103 |
with gr.Row():
|
| 1104 |
bvh_file = gr.File(label="Blender BVH")
|
| 1105 |
npz_file = gr.File(label="ARDY NPZ")
|
| 1106 |
metadata_file = gr.File(label="Metadata")
|
| 1107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1108 |
gr.Examples(
|
| 1109 |
examples=EXAMPLES,
|
| 1110 |
inputs=[prompt, rig, duration],
|
|
|
|
| 1134 |
cont.click(fn=ui_continue, inputs=_gen_inputs + [seq_state],
|
| 1135 |
outputs=[player, seed, seq_state], api_name=False)
|
| 1136 |
export_blender.click(
|
| 1137 |
+
fn=export_current_sequence,
|
| 1138 |
+
inputs=[seq_state, rig, prompt, seed],
|
| 1139 |
outputs=[bvh_file, npz_file, metadata_file, seed],
|
| 1140 |
api_name=False,
|
| 1141 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1142 |
|
| 1143 |
# Clean single-shot endpoint for the HTTP API / MCP tool (no session state).
|
| 1144 |
gr.api(generate_motion, api_name="generate")
|
| 1145 |
gr.api(generate_blender_motion, api_name="generate_blender")
|
|
|
|
| 1146 |
|
| 1147 |
demo.queue()
|
| 1148 |
|