modelbeast/server/operators/brush_train/run.py
MODELBEAST 605b1ae347 Phase 1 + framework: settings/secrets, queue lanes, job mgmt, inbox, 8 new operators
Framework:
- server/settings.py: key/value settings + secrets, env-injected into operator
  subprocesses, secret values masked in API and redacted from job logs
- runner: gpu/cpu/net concurrency lanes, job cancel/retry/delete, multi-input,
  graceful 'not installed' error when a tool venv is missing
- db: settings table, asset_ids column (migrated), MODELBEAST_DATA test override
- main: settings + job-action endpoints, inbox watch folder auto-ingest
- store: operators can tag output asset kind (splat, colmap_dataset)

Operators (11 total):
- fal_trellis/trellis2/hunyuan3d/rodin via shared _lib/fal_common.py (verified
  params + endpoint ids; recursive result-URL extractor handles per-endpoint keys)
- sf3d, trellis_mac: local MPS image-to-3D, installed with Metal kernels built,
  gated on owner HuggingFace auth
- colmap_poses (COLMAP 4.x + GLOMAP global mapper), brush_train (native Metal 3DGS)
- Scan pipeline validated end-to-end through the UI: frames -> colmap (48/48
  registered, 0.6px) -> brush -> splat.ply -> in-app SplatViewer

Frontend:
- Settings modal, operator gating (lock + disabled run when requires_env unmet),
  job cancel/retry/delete, Compare grid (multi-select side-by-side viewers),
  SplatViewer (gaussian-splats-3d, Ply format forced for extensionless URLs)

Tooling: scripts/install_{colmap,brush,sf3d,trellis_mac}.sh; vendor/ + venvs/
gitignored; tests/smoke.sh (12 checks passing); BENCHMARKS.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 21:42:27 +10:00

59 lines
1.8 KiB
Python

import argparse
import json
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
BRUSH = ROOT / "bin" / "brush"
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)
if not a.input:
print("ERROR: no input colmap dataset")
sys.exit(1)
dataset = Path(a.input[0])
if not BRUSH.exists():
print(f"ERROR: brush not installed at {BRUSH}. Run scripts/install_brush.sh")
sys.exit(1)
# brush wants the dataset root containing images/ + sparse/
if not (dataset / "images").exists():
inner = dataset / "colmap_dataset"
if (inner / "images").exists():
dataset = inner
outdir = Path(a.outdir)
export = outdir / "export"
export.mkdir(parents=True, exist_ok=True)
steps = int(p.get("total_steps", 30000))
cmd = [
str(BRUSH), str(dataset.resolve()),
"--total-steps", str(steps),
"--max-splats", str(p.get("max_splats", 10000000)),
"--sh-degree", str(p.get("sh_degree", 3)),
"--max-resolution", str(p.get("max_resolution", 1920)),
"--seed", str(p.get("seed", 42)),
"--export-every", str(steps), # export once at the end
"--export-path", str(export.resolve()),
"--export-name", "splat.ply",
]
print("+", " ".join(cmd), flush=True)
res = subprocess.run(cmd, stdout=sys.stdout, stderr=subprocess.STDOUT)
if res.returncode != 0:
sys.exit(res.returncode)
plys = sorted(export.rglob("*.ply"))
if not plys:
print("ERROR: brush produced no .ply splat")
sys.exit(1)
(outdir / "result.json").write_text(json.dumps({"outputs": [
{"path": str(plys[-1]), "name": f"{dataset.name}_splat.ply",
"meta": {"kind": "splat", "tool": "brush", "steps": steps}}]}))
print("done:", plys[-1], flush=True)