Add ardy_motion operator: NVIDIA ARDY text-to-motion on MPS
Text → 20fps skeletal motion → npz + Mixamo-convention BVH (via ardy2bvh). Runs ~/Documents/ardy branch mps (upstream + community PR6), venvs/ardy. Text encoder loads pre-merged bf16 LLM2Vec weights from data/text_encoders/ (ungated mirror base + mntp adapter merged offline) and disables the implicit HF token so token rot can't break public downloads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
431aeb08ea
commit
6a0aa4b8e0
22
server/operators/ardy_motion/manifest.json
Normal file
22
server/operators/ardy_motion/manifest.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "ardy_motion",
|
||||
"name": "Motion (local, ARDY/MPS)",
|
||||
"category": "motion",
|
||||
"description": "Text → character motion as BVH (+ npz) via NVIDIA ARDY on MPS. Streaming-class text-to-motion trained on 630+ hrs of studio mocap — the Mixamo-replacement upgrade over motion_local/MoMask. Mixamo-convention bone names: output BVH retargets straight onto character_kit rigs in Blender. Strong on locomotion/gesture/combat/dance; DJ-specific hand work is out of distribution. First job after a restart pays ~1-2 min model load (Llama-3-8B text encoder); generation itself is faster than real-time.",
|
||||
"accepts": [],
|
||||
"produces": ["motion"],
|
||||
"resources": "gpu",
|
||||
"entry": "run.py",
|
||||
"python": "venvs/ardy/bin/python",
|
||||
"params_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {"type": "string", "default": "", "description": "The action: 'a person crouches at a crate, flips through records, stands up holding one'"},
|
||||
"duration": {"type": "number", "default": 5.0, "minimum": 0.5, "maximum": 30, "description": "Seconds of motion at 20fps"},
|
||||
"seed": {"type": "integer", "default": -1, "description": "Random seed; -1 = random"},
|
||||
"num_samples": {"type": "integer", "default": 1, "minimum": 1, "maximum": 4, "description": "Variations to generate"},
|
||||
"model": {"type": "string", "default": "core", "enum": ["core", "core8"], "description": "core = Horizon40 (best quality), core8 = short-horizon (fastest)"},
|
||||
"no_postprocess": {"type": "boolean", "default": false, "description": "Skip the foot-skate IK cleanup pass"}
|
||||
}
|
||||
}
|
||||
}
|
||||
89
server/operators/ardy_motion/run.py
Normal file
89
server/operators/ardy_motion/run.py
Normal file
@ -0,0 +1,89 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
os.environ["PATH"] = "/opt/homebrew/bin:/usr/local/bin:" + os.environ.get("PATH", "")
|
||||
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--input", action="append", default=[])
|
||||
ap.add_argument("--outdir", required=True)
|
||||
ap.add_argument("--params", default="{}")
|
||||
a = ap.parse_args()
|
||||
p = json.loads(a.params)
|
||||
|
||||
text = (p.get("text") or "").strip()
|
||||
if not text:
|
||||
print("ERROR: text is required")
|
||||
sys.exit(1)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
ARDY = Path.home() / "Documents" / "ardy"
|
||||
ARDY2BVH = Path.home() / "Documents" / "ardy2bvh" / "ardy2bvh.py"
|
||||
if not (ARDY / "scripts" / "generate.py").exists():
|
||||
print("ERROR: ardy repo not found at ~/Documents/ardy")
|
||||
sys.exit(1)
|
||||
|
||||
env = dict(os.environ)
|
||||
env["LOCAL_CACHE"] = "true"
|
||||
env["TEXT_ENCODERS_DIR"] = str(ROOT / "data" / "text_encoders")
|
||||
# The adapters in TEXT_ENCODERS_DIR pin the ungated Llama-3 mirror; never let a
|
||||
# stale ~/.cache/huggingface/token break those public downloads.
|
||||
env["HF_HUB_DISABLE_IMPLICIT_TOKEN"] = "1"
|
||||
env["TOKENIZERS_PARALLELISM"] = "false"
|
||||
|
||||
stem = f"mb_{uuid.uuid4().hex[:10]}"
|
||||
workdir = Path(a.outdir) / "_work"
|
||||
workdir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
cmd = [
|
||||
sys.executable, "scripts/generate.py", text,
|
||||
"--model", p.get("model", "core"),
|
||||
"--duration", str(float(p.get("duration", 5.0))),
|
||||
"--num_samples", str(int(p.get("num_samples", 1))),
|
||||
"--device", "mps",
|
||||
"--output", str(workdir / stem),
|
||||
]
|
||||
seed = int(p.get("seed", -1))
|
||||
if seed >= 0:
|
||||
cmd += ["--seed", str(seed)]
|
||||
if p.get("no_postprocess"):
|
||||
cmd += ["--no-postprocess"]
|
||||
|
||||
t0 = time.time()
|
||||
r = subprocess.run(cmd, cwd=str(ARDY), capture_output=True, text=True, env=env)
|
||||
npz = sorted(workdir.rglob(f"{stem}*.npz"))
|
||||
if r.returncode != 0 or not npz:
|
||||
print(r.stdout[-1500:])
|
||||
print(r.stderr[-2000:], file=sys.stderr)
|
||||
print("ERROR: ardy generation failed")
|
||||
sys.exit(1)
|
||||
t_gen = time.time() - t0
|
||||
|
||||
outdir = Path(a.outdir)
|
||||
slug = "".join(c if c.isalnum() else "_" for c in text[:40]).strip("_")
|
||||
n = 0
|
||||
for i, f in enumerate(npz):
|
||||
suffix = f"_{i:02d}" if len(npz) > 1 else ""
|
||||
npz_out = outdir / f"motion_{slug}{suffix}.npz"
|
||||
shutil.copy(f, npz_out)
|
||||
n += 1
|
||||
rb = subprocess.run(
|
||||
[sys.executable, str(ARDY2BVH), str(f), "-o", str(outdir / f"motion_{slug}{suffix}.bvh")],
|
||||
capture_output=True, text=True, env=env,
|
||||
)
|
||||
if rb.returncode == 0:
|
||||
n += 1
|
||||
else:
|
||||
print(f"WARN: bvh conversion failed for {f.name}: {rb.stderr[-500:]}", file=sys.stderr)
|
||||
|
||||
shutil.rmtree(workdir, ignore_errors=True)
|
||||
if not any(outdir.glob("*.bvh")):
|
||||
print("ERROR: no BVH produced")
|
||||
sys.exit(1)
|
||||
print(f"done: gen {t_gen:.0f}s, {n} files for '{text[:60]}'", flush=True)
|
||||
Loading…
Reference in New Issue
Block a user