From 6a0aa4b8e08bdd904dbfdc6674b759318eabb189 Mon Sep 17 00:00:00 2001 From: m3ultra Date: Tue, 4 Aug 2026 00:06:06 +1000 Subject: [PATCH] Add ardy_motion operator: NVIDIA ARDY text-to-motion on MPS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- server/operators/ardy_motion/manifest.json | 22 ++++++ server/operators/ardy_motion/run.py | 89 ++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 server/operators/ardy_motion/manifest.json create mode 100644 server/operators/ardy_motion/run.py diff --git a/server/operators/ardy_motion/manifest.json b/server/operators/ardy_motion/manifest.json new file mode 100644 index 0000000..3bdeefc --- /dev/null +++ b/server/operators/ardy_motion/manifest.json @@ -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"} + } + } +} diff --git a/server/operators/ardy_motion/run.py b/server/operators/ardy_motion/run.py new file mode 100644 index 0000000..9744980 --- /dev/null +++ b/server/operators/ardy_motion/run.py @@ -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)