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>
54 lines
1.9 KiB
JavaScript
54 lines
1.9 KiB
JavaScript
const BASE = "";
|
|
|
|
export async function api(path, opts = {}) {
|
|
const res = await fetch(BASE + path, opts);
|
|
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
|
|
const ct = res.headers.get("content-type") || "";
|
|
return ct.includes("json") ? res.json() : res;
|
|
}
|
|
|
|
export const listAssets = () => api("/api/assets");
|
|
export const listOperators = () => api("/api/operators");
|
|
export const listJobs = () => api("/api/jobs");
|
|
export const deleteAsset = (id) => api(`/api/assets/${id}`, { method: "DELETE" });
|
|
|
|
export async function uploadFile(file) {
|
|
const form = new FormData();
|
|
form.append("file", file, file.name || "pasted.png");
|
|
return api("/api/assets", { method: "POST", body: form });
|
|
}
|
|
|
|
export const runJob = (operator, asset_id, params) =>
|
|
api("/api/jobs", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ operator, asset_id, params }),
|
|
});
|
|
|
|
export const cancelJob = (id) => api(`/api/jobs/${id}/cancel`, { method: "POST" });
|
|
export const retryJob = (id) => api(`/api/jobs/${id}/retry`, { method: "POST" });
|
|
export const deleteJob = (id) => api(`/api/jobs/${id}`, { method: "DELETE" });
|
|
|
|
export const getSettings = () => api("/api/settings");
|
|
export const putSettings = (updates) =>
|
|
api("/api/settings", {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(updates),
|
|
});
|
|
|
|
export function connectWS(onMessage) {
|
|
const proto = location.protocol === "https:" ? "wss" : "ws";
|
|
const ws = new WebSocket(`${proto}://${location.host}/ws`);
|
|
ws.onmessage = (e) => onMessage(JSON.parse(e.data));
|
|
const ping = setInterval(() => ws.readyState === 1 && ws.send("ping"), 20000);
|
|
ws.onclose = () => {
|
|
clearInterval(ping);
|
|
setTimeout(() => connectWS(onMessage), 2000);
|
|
};
|
|
return ws;
|
|
}
|
|
|
|
export const assetFileURL = (id, member) =>
|
|
`/api/assets/${id}/file` + (member ? `?member=${encodeURIComponent(member)}` : "");
|