An AI tool for rescue teams that takes LiDAR gaussian splat scans of collapsed buildings, measures structural damage like wall lean and debris volume, and weighs how many people might be trapped alive to rank which sites to reach first.
After an earthquake, a search-and-rescue team can face dozens of collapsed buildings and only a handful of crews. The order they work the sites in decides who lives. Right now that call is usually made by looking at a leaning wall, guessing, and writing site names on a whiteboard. None of the tools involved talk to each other, and none of them turn a scan into a number you can actually compare.
save-splat takes a phone scan of a collapsed structure and pulls real measurements out of it: how far each wall leans, how the slabs and floors are angled, and how much debris is piled up. It then ranks the sites so command can see where a crew buys the most lives per hour. It measures the damage and lays out the priority. A person still makes every decision.
You load a Scaniverse or Polycam gaussian-splat .ply (a .glb or .gltf mesh works too), and the app fits planes to the walls and floors, reads the lean off each wall as a drift number, finds the ground and measures debris volume above it, and lets you place sites and rank them. There is no autopilot here. The output is a ranked list for a human to review, not a dispatch order.
| Layer | Technology | What it handles |
|---|---|---|
| Frontend | Vite, React 18, TypeScript | The panel UI and the 3D scene, all in one page |
| State | useSyncExternalStore store |
Site records live here; their 3D markers live in the viewer, keyed by id |
| Renderer | Three.js | Camera, a hand-written orbit control, site markers, and the plane overlay. Splats draw as THREE.Points |
| Core logic | Plain TypeScript, no Three.js and no DOM | Parsing, RANSAC, ranking, and export. Runs and gets tested without a browser |
| Parsing | Custom .ply and gaussian-splat reader |
Reads per-Gaussian covariance from scale_* and rot_*, colour from f_dc_*, with streaming progress |
| Geometry | Opacity-weighted RANSAC, seeded (mulberry32) | Fits planes, reads wall drift bands, and measures debris volume |
| Validation | Zod (core/swarm/schema.ts) |
The gate for anything coming from outside. Bad values are rejected, not clamped |
| Reasoner | Node-only server/swarm/ |
Prompt, call the provider, run it through Zod, then verify |
| Providers | OpenRouter, OpenAI, Anthropic, Athena | One Reasoner interface. Whichever key is set gets used; the model comes from the account |
| Hosting | Vercel functions, plus Vite dev and preview | The same /api/swarm/* handlers run in all three |
| Run log | Supabase Postgres pooler (optional) | Appends each run to swarm_runs. A failed write logs a warning and never blocks a run |
The one rule worth knowing: core/ never imports Three.js or touches the DOM. The geometry functions take plain arrays and a matrix instead of a THREE.Points object, so the measurement code can be unit-tested without a renderer.
- Node.js 20 or newer
- npm
- The Stripe Projects CLI, if you want it to provision the reasoner and run-log credentials for you (optional)
git clone https://github.com/your-org/save-splat.git
cd save-splat
npm installnpm run dev # http://localhost:5173The synthetic scene loads on open, so this works with nothing else set up. Click "View demo" or open #viewer to go straight to the viewer.
The reasoner needs one key, set server-side in .env.local. See .env.example. The quickest path pulls everything at once:
stripe projects env --pull # writes .env.local with the real valuesOr add a single key by hand:
# any one of these is enough. OpenRouter is what Stripe Projects provisions.
OPENROUTER_API_KEY=your_key
# OPENAI_API_KEY=your_key
# ANTHROPIC_API_KEY=your_key
# optional tuning
# SWARM_PROVIDER=openrouter # pin this when more than one key is set
# SWARM_MODEL=anthropic/claude-opus-5 # skip the catalogue lookup
# optional run log (a server-side Postgres connection string)
# SUPABASE_POOLER_URL=...
# SUPABASE_DB_PASS=...Keep the VITE_ prefix off every secret. Vite inlines any VITE_* variable into the client bundle, so reasoner keys are read only inside server/swarm/ from process.env. The browser only ever calls /api/swarm/*.
/api/swarm/status reports what is configured, and the RUN SWARM button explains why it is disabled when nothing is set. Athena is an extra agent that Stripe Projects can provision. It works as both a reasoner and an incident reviewer, and it gets picked first when its key and MCP URL are present.
npm run dev # vite dev server
npm run check # typecheck, lint, and test. Run this before you call something done
npm run test # vitest. The core is testable without a browser
npm run build # tsc -b && vite build, output in dist/
npm run preview # serve the production build


