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>
77 lines
2.7 KiB
Python
77 lines
2.7 KiB
Python
import argparse
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import 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)
|
|
|
|
if not a.input:
|
|
print("ERROR: no input frames folder")
|
|
sys.exit(1)
|
|
src = Path(a.input[0])
|
|
if not src.is_dir():
|
|
print(f"ERROR: input must be a frames folder, got {src}")
|
|
sys.exit(1)
|
|
|
|
outdir = Path(a.outdir)
|
|
dataset = outdir / "colmap_dataset"
|
|
images = dataset / "images"
|
|
images.mkdir(parents=True, exist_ok=True)
|
|
frames = sorted([f for f in src.iterdir() if f.suffix.lower() in (".jpg", ".jpeg", ".png")])
|
|
if len(frames) < 3:
|
|
print(f"ERROR: need >=3 frames, found {len(frames)}")
|
|
sys.exit(1)
|
|
for f in frames:
|
|
shutil.copy2(f, images / f.name)
|
|
print(f"copied {len(frames)} frames", flush=True)
|
|
|
|
db_path = dataset / "database.db"
|
|
sparse = dataset / "sparse"
|
|
sparse.mkdir(exist_ok=True)
|
|
|
|
|
|
def run(*cmd):
|
|
print("+", " ".join(str(c) for c in cmd), flush=True)
|
|
r = subprocess.run([str(c) for c in cmd], stdout=sys.stdout, stderr=subprocess.STDOUT)
|
|
if r.returncode != 0:
|
|
print(f"ERROR: command failed ({r.returncode})")
|
|
sys.exit(r.returncode)
|
|
|
|
|
|
run("colmap", "feature_extractor", "--database_path", db_path, "--image_path", images,
|
|
"--ImageReader.single_camera", "1" if p.get("single_camera", True) else "0",
|
|
"--ImageReader.camera_model", p.get("camera_model", "OPENCV"),
|
|
"--FeatureExtraction.use_gpu", "0")
|
|
|
|
matcher = "sequential_matcher" if p.get("matcher", "sequential") == "sequential" else "exhaustive_matcher"
|
|
run("colmap", matcher, "--database_path", db_path, "--FeatureMatching.use_gpu", "0")
|
|
|
|
if p.get("mapper", "global") == "global":
|
|
run("colmap", "global_mapper", "--database_path", db_path,
|
|
"--image_path", images, "--output_path", sparse)
|
|
else:
|
|
run("colmap", "mapper", "--database_path", db_path,
|
|
"--image_path", images, "--output_path", sparse)
|
|
|
|
# report registered images
|
|
recon = sparse / "0"
|
|
n_reg = 0
|
|
if (recon / "images.bin").exists():
|
|
n_reg = (recon / "images.bin").stat().st_size # crude presence signal
|
|
print(f"reconstruction at {recon}, images.bin present: {(recon / 'images.bin').exists()}", flush=True)
|
|
if not (recon / "images.bin").exists() and not (recon / "images.txt").exists():
|
|
print("ERROR: COLMAP produced no reconstruction (too few features/matches?)")
|
|
sys.exit(1)
|
|
|
|
(outdir / "result.json").write_text(json.dumps({"outputs": [
|
|
{"path": str(dataset), "name": f"{src.name}_colmap",
|
|
"meta": {"kind": "colmap_dataset", "frames": len(frames)}}]}))
|
|
print("done", flush=True)
|