music_local (ACE-Step) + motion_local (MoMask) operators with install scripts
install_momask.sh encodes the compat patches: numpy<2 + umath_tests shim, matplotlib axes-clearing fix, np.float alias wrapper (mb_gen.py) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b7e113ca5b
commit
12a8cf1fd2
10
scripts/install_acestep.sh
Executable file
10
scripts/install_acestep.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install ACE-Step (text → music) into vendor/acestep + venvs/acestep.
|
||||
# Weights (~7GB) auto-download from HF on first generation.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
[ -d vendor/acestep ] || git clone -q https://github.com/ace-step/ACE-Step.git vendor/acestep
|
||||
uv venv venvs/acestep --python 3.11 2>/dev/null || true
|
||||
cd vendor/acestep
|
||||
uv pip install --python ../../venvs/acestep/bin/python -e .
|
||||
echo "acestep installed"
|
||||
54
scripts/install_momask.sh
Executable file
54
scripts/install_momask.sh
Executable file
@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install MoMask (text → motion BVH) into vendor/momask + venvs/momask.
|
||||
# Research code: needs numpy<2 + matplotlib 3.6 + two small compat patches.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
[ -d vendor/momask ] || git clone -q https://github.com/EricGuo5513/momask-codes.git vendor/momask
|
||||
uv venv venvs/momask --python 3.11 2>/dev/null || true
|
||||
uv pip install --python venvs/momask/bin/python \
|
||||
torch "numpy<2" scipy "matplotlib==3.6.3" tqdm einops gdown ftfy regex \
|
||||
"git+https://github.com/openai/CLIP.git"
|
||||
|
||||
cd vendor/momask
|
||||
|
||||
# HumanML3D checkpoints (188MB, Google Drive)
|
||||
if [ ! -d checkpoints/t2m/t2m_nlayer8_nhead6_ld384_ff1024_cdp0.1_rvq6ns ]; then
|
||||
mkdir -p checkpoints/t2m && cd checkpoints/t2m
|
||||
../../../../venvs/momask/bin/gdown 1vXS7SHJBgWPt59wupQ5UUzhFObrnGkQ0
|
||||
unzip -q humanml3d_models.zip && rm humanml3d_models.zip
|
||||
cd ../..
|
||||
fi
|
||||
|
||||
# Patch 1: numpy.core.umath_tests was removed — np.matmul is the same op
|
||||
python3 - <<'PYEOF'
|
||||
import pathlib
|
||||
SHIM = ("from types import SimpleNamespace as _SN; "
|
||||
"import numpy as _np; ut = _SN(matrix_multiply=_np.matmul)")
|
||||
for f in ("visualization/Animation.py", "visualization/Quaternions.py"):
|
||||
p = pathlib.Path(f); s = p.read_text()
|
||||
if "umath_tests" in s:
|
||||
p.write_text(s.replace("import numpy.core.umath_tests as ut", SHIM))
|
||||
PYEOF
|
||||
|
||||
# Patch 2: matplotlib removed the ax.lines/ax.collections setters
|
||||
python3 - <<'PYEOF'
|
||||
import pathlib
|
||||
p = pathlib.Path("utils/plot_script.py"); s = p.read_text()
|
||||
s = s.replace("ax.lines = []", "[l.remove() for l in list(ax.lines)]")
|
||||
s = s.replace("ax.collections = []", "[c.remove() for c in list(ax.collections)]")
|
||||
p.write_text(s)
|
||||
PYEOF
|
||||
|
||||
# Wrapper: restore np.float/np.int/... aliases the old code expects
|
||||
cat > mb_gen.py <<'PYEOF'
|
||||
"""MODELBEAST wrapper: numpy>=1.24 compat aliases, then run gen_t2m.py."""
|
||||
import numpy as np
|
||||
for _n, _v in {"float": float, "int": int, "object": object, "bool": bool, "str": str}.items():
|
||||
if not hasattr(np, _n):
|
||||
setattr(np, _n, _v)
|
||||
import runpy
|
||||
runpy.run_path("gen_t2m.py", run_name="__main__")
|
||||
PYEOF
|
||||
|
||||
echo "momask installed"
|
||||
18
server/operators/motion_local/manifest.json
Normal file
18
server/operators/motion_local/manifest.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "motion_local",
|
||||
"name": "Motion (local, MoMask)",
|
||||
"category": "motion",
|
||||
"description": "Text → character motion as BVH (+ mp4 preview) via MoMask (HumanML3D). For the bespoke clips Mixamo doesn't have — describe the action in plain words. Output BVH retargets onto rigged characters in Blender (import BVH, retarget to the character_kit skeleton). CPU-only, ~1-2 min. Research-grade: expect roughly right, polish in Blender. Install: scripts/install_momask.sh",
|
||||
"accepts": [],
|
||||
"produces": ["motion"],
|
||||
"resources": "cpu",
|
||||
"entry": "run.py",
|
||||
"python": "venvs/momask/bin/python",
|
||||
"params_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {"type": "string", "default": "", "description": "The action: 'a person riffles through a crate of records, pulls one out and admires it'"},
|
||||
"length": {"type": "integer", "default": -1, "minimum": -1, "maximum": 196, "description": "Frames at 20fps (196 max ≈ 10s); -1 = let the model estimate"}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
server/operators/motion_local/run.py
Normal file
61
server/operators/motion_local/run.py
Normal file
@ -0,0 +1,61 @@
|
||||
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]
|
||||
MOMASK = ROOT / "vendor" / "momask"
|
||||
if not (MOMASK / "mb_gen.py").exists():
|
||||
print("ERROR: momask not installed — run scripts/install_momask.sh")
|
||||
sys.exit(1)
|
||||
|
||||
ext = f"mb_{uuid.uuid4().hex[:10]}" # unique run dir inside the shared vendor tree
|
||||
cmd = [sys.executable, "mb_gen.py", "--gpu_id", "-1", "--ext", ext, "--text_prompt", text]
|
||||
length = int(p.get("length", -1))
|
||||
if length > 0:
|
||||
cmd += ["--motion_length", str(length)]
|
||||
|
||||
t0 = time.time()
|
||||
r = subprocess.run(cmd, cwd=str(MOMASK), capture_output=True, text=True)
|
||||
gen_dir = MOMASK / "generation" / ext
|
||||
files = sorted(gen_dir.rglob("*")) if gen_dir.exists() else []
|
||||
bvh = [f for f in files if f.suffix == ".bvh"]
|
||||
if r.returncode != 0 or not bvh:
|
||||
print(r.stdout[-1000:])
|
||||
print(r.stderr[-1500:], file=sys.stderr)
|
||||
print("ERROR: momask generation failed")
|
||||
sys.exit(1)
|
||||
|
||||
outdir = Path(a.outdir)
|
||||
slug = "".join(c if c.isalnum() else "_" for c in text[:40]).strip("_")
|
||||
n = 0
|
||||
for f in files:
|
||||
if f.suffix == ".bvh":
|
||||
# _ik = foot-contact cleaned; that's the one to retarget
|
||||
kind = "ik" if "_ik" in f.stem else "raw"
|
||||
shutil.copy(f, outdir / f"motion_{slug}_{kind}.bvh")
|
||||
n += 1
|
||||
elif f.suffix == ".mp4" and "_ik" in f.stem:
|
||||
shutil.copy(f, outdir / f"motion_{slug}_preview.mp4")
|
||||
n += 1
|
||||
shutil.rmtree(gen_dir, ignore_errors=True)
|
||||
print(f"done in {time.time() - t0:.0f}s: {n} files for '{text[:60]}'", flush=True)
|
||||
21
server/operators/music_local/manifest.json
Normal file
21
server/operators/music_local/manifest.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"id": "music_local",
|
||||
"name": "Music (local, ACE-Step)",
|
||||
"category": "audio",
|
||||
"description": "Style tags → music WAV via ACE-Step 3.5B on MPS (Apache 2.0, ~7GB weights). Jingles, menu loops, shop muzak, boss themes; add lyrics for actual songs ([verse]/[chorus] markers work, '[instrumental]' for none). Runs on the primary only. Install: scripts/install_acestep.sh",
|
||||
"accepts": [],
|
||||
"produces": ["audio"],
|
||||
"resources": "gpu",
|
||||
"entry": "run.py",
|
||||
"python": "venvs/acestep/bin/python",
|
||||
"params_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt": {"type": "string", "default": "", "description": "Style tags, comma-separated: 'lo-fi synth funk, playful, toy robot bleeps, 90 bpm'"},
|
||||
"lyrics": {"type": "string", "default": "[instrumental]", "description": "Lyrics with [verse]/[chorus] markers, or [instrumental]"},
|
||||
"duration": {"type": "number", "default": 30, "minimum": 5, "maximum": 240, "description": "Seconds"},
|
||||
"steps": {"type": "integer", "default": 27, "minimum": 10, "maximum": 100, "description": "27 = fast/good, 60 = best"},
|
||||
"seed": {"type": "integer", "default": 42}
|
||||
}
|
||||
}
|
||||
}
|
||||
49
server/operators/music_local/run.py
Normal file
49
server/operators/music_local/run.py
Normal file
@ -0,0 +1,49 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
os.environ.setdefault("TQDM_DISABLE", "1")
|
||||
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)
|
||||
|
||||
prompt = (p.get("prompt") or "").strip()
|
||||
if not prompt:
|
||||
print("ERROR: prompt (style tags) is required")
|
||||
sys.exit(1)
|
||||
|
||||
from acestep.pipeline_ace_step import ACEStepPipeline # noqa: E402
|
||||
|
||||
seed = int(p.get("seed", 42))
|
||||
out = Path(a.outdir) / f"music_{seed}.wav"
|
||||
t0 = time.time()
|
||||
pipe = ACEStepPipeline(dtype="float32") # bfloat16 unsupported on MPS
|
||||
print(f"pipeline ready in {time.time() - t0:.1f}s", flush=True)
|
||||
|
||||
t1 = time.time()
|
||||
pipe(
|
||||
prompt=prompt,
|
||||
lyrics=(p.get("lyrics") or "[instrumental]").strip(),
|
||||
audio_duration=float(p.get("duration", 30)),
|
||||
infer_step=int(p.get("steps", 27)),
|
||||
guidance_scale=15.0,
|
||||
omega_scale=10.0,
|
||||
manual_seeds=[seed],
|
||||
save_path=str(out),
|
||||
)
|
||||
if not out.exists():
|
||||
# some versions append suffixes; register whatever wav landed in outdir
|
||||
wavs = list(Path(a.outdir).glob("*.wav"))
|
||||
if not wavs:
|
||||
print("ERROR: no wav produced")
|
||||
sys.exit(1)
|
||||
out = wavs[0]
|
||||
print(f"done in {time.time() - t1:.1f}s: {out.name}", flush=True)
|
||||
Loading…
Reference in New Issue
Block a user