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>
78 lines
4.1 KiB
Bash
Executable File
78 lines
4.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# End-to-end smoke test against a throwaway data dir on a scratch port.
|
|
# Validates: operators load, settings CRUD, upload, ffprobe/ffmpeg_frames/
|
|
# blender_convert run to completion, outputs register, cancel + retry work.
|
|
set -euo pipefail
|
|
cd "$(dirname "$0")/.."
|
|
|
|
PORT=${PORT:-8799}
|
|
TMP=$(mktemp -d)
|
|
BLENDER="/Applications/Blender.app/Contents/MacOS/Blender"
|
|
UV=/opt/homebrew/bin/uv
|
|
pass=0; fail=0
|
|
say() { printf "\n\033[1m== %s ==\033[0m\n" "$1"; }
|
|
ok() { printf " \033[32mPASS\033[0m %s\n" "$1"; pass=$((pass+1)); }
|
|
bad() { printf " \033[31mFAIL\033[0m %s\n" "$1"; fail=$((fail+1)); }
|
|
j() { python3 -c "import json,sys;d=json.load(sys.stdin);print($1)"; }
|
|
|
|
cleanup() { kill "${SRV:-0}" 2>/dev/null || true; rm -rf "$TMP"; }
|
|
trap cleanup EXIT
|
|
|
|
say "fixtures"
|
|
ffmpeg -y -f lavfi -i "testsrc2=size=640x360:rate=30:duration=3" -pix_fmt yuv420p "$TMP/clip.mp4" >/dev/null 2>&1
|
|
ok "test video"
|
|
"$BLENDER" -b --python-expr "
|
|
import bpy
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
bpy.ops.mesh.primitive_monkey_add()
|
|
bpy.ops.export_scene.gltf(filepath='$TMP/mesh.glb', export_format='GLB')" >/dev/null 2>&1
|
|
ok "test glb"
|
|
|
|
say "start server (MODELBEAST_DATA=$TMP/data, port $PORT)"
|
|
MODELBEAST_DATA="$TMP/data" "$UV" run uvicorn server.main:app --host 127.0.0.1 --port "$PORT" >"$TMP/server.log" 2>&1 &
|
|
SRV=$!
|
|
for i in $(seq 1 40); do curl -sf "http://127.0.0.1:$PORT/api/operators" >/dev/null 2>&1 && break; sleep 0.5; done
|
|
B="http://127.0.0.1:$PORT"
|
|
|
|
say "operators + settings"
|
|
NOPS=$(curl -s "$B/api/operators" | j "len(d)")
|
|
[ "$NOPS" -ge 3 ] && ok "operators loaded ($NOPS)" || bad "operators ($NOPS)"
|
|
curl -s -X PUT "$B/api/settings" -H 'Content-Type: application/json' -d '{"archive_path":"/tmp/xyz"}' >/dev/null
|
|
AP=$(curl -s "$B/api/settings" | j "d['archive_path']")
|
|
[ "$AP" = "/tmp/xyz" ] && ok "settings persist" || bad "settings ($AP)"
|
|
# secret masking
|
|
curl -s -X PUT "$B/api/settings" -H 'Content-Type: application/json' -d '{"fal_key":"sk-secret-123456"}' >/dev/null
|
|
FK=$(curl -s "$B/api/settings" | j "d['fal_key']")
|
|
[ "$FK" = "••••••••" ] && ok "secret masked in GET" || bad "secret leak ($FK)"
|
|
|
|
say "upload + ffprobe"
|
|
VID=$(curl -s -F "file=@$TMP/clip.mp4" "$B/api/assets" | j "d['id']")
|
|
GLB=$(curl -s -F "file=@$TMP/mesh.glb" "$B/api/assets" | j "d['id']")
|
|
JOB=$(curl -s -X POST "$B/api/jobs" -H 'Content-Type: application/json' -d "{\"operator\":\"ffprobe\",\"asset_id\":\"$VID\"}" | j "d['id']")
|
|
for i in $(seq 1 30); do S=$(curl -s "$B/api/jobs/$JOB" | j "d['status']"); [ "$S" = done ] || [ "$S" = error ] && break; sleep 0.5; done
|
|
[ "$S" = done ] && ok "ffprobe done" || bad "ffprobe ($S)"
|
|
|
|
say "ffmpeg_frames"
|
|
JOB=$(curl -s -X POST "$B/api/jobs" -H 'Content-Type: application/json' -d "{\"operator\":\"ffmpeg_frames\",\"asset_id\":\"$VID\",\"params\":{\"fps\":4,\"max_frames\":8}}" | j "d['id']")
|
|
for i in $(seq 1 40); do S=$(curl -s "$B/api/jobs/$JOB" | j "d['status']"); [ "$S" = done ] || [ "$S" = error ] && break; sleep 0.5; done
|
|
[ "$S" = done ] && ok "ffmpeg_frames done" || bad "ffmpeg_frames ($S)"
|
|
NFR=$(curl -s "$B/api/assets" | j "sum(1 for a in d if a['kind']=='frames')")
|
|
[ "$NFR" -ge 1 ] && ok "frames asset registered" || bad "no frames asset"
|
|
|
|
say "blender_convert glb->fbx (bool param regression)"
|
|
JOB=$(curl -s -X POST "$B/api/jobs" -H 'Content-Type: application/json' -d "{\"operator\":\"blender_convert\",\"asset_id\":\"$GLB\",\"params\":{\"target\":\"fbx\",\"apply_transforms\":true}}" | j "d['id']")
|
|
for i in $(seq 1 60); do S=$(curl -s "$B/api/jobs/$JOB" | j "d['status']"); [ "$S" = done ] || [ "$S" = error ] && break; sleep 0.5; done
|
|
[ "$S" = done ] && ok "blender_convert done" || bad "blender_convert ($S)"
|
|
NFBX=$(curl -s "$B/api/assets" | j "sum(1 for a in d if a['name'].endswith('.fbx'))")
|
|
[ "$NFBX" -ge 1 ] && ok "fbx registered" || bad "no fbx"
|
|
|
|
say "retry + delete"
|
|
NEW=$(curl -s -X POST "$B/api/jobs/$JOB/retry" | j "d['id']")
|
|
[ -n "$NEW" ] && [ "$NEW" != "$JOB" ] && ok "retry spawns new job" || bad "retry"
|
|
sleep 1
|
|
DEL=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE "$B/api/jobs/$JOB")
|
|
[ "$DEL" = 200 ] && ok "delete job" || bad "delete ($DEL)"
|
|
|
|
printf "\n\033[1mResult: %d passed, %d failed\033[0m\n" "$pass" "$fail"
|
|
[ "$fail" -eq 0 ]
|