# Snap2Sim — "Inside the Machine"
**Build Small Hackathon** · Backyard AI Track · [huggingface.co/build-small-hackathon](https://huggingface.co/build-small-hackathon)
---
## Final Status (June 15, 2026)
This file is the original build prompt and contains historical implementation
directions, including early A-Frame and private-Space assumptions that were
superseded during development. The finished public submission uses deterministic
browser-side Three.js from validated JSON, is hosted under
`build-small-hackathon/Snap2Sim`, and is linked from the README.
- Public Space: https://huggingface.co/spaces/build-small-hackathon/Snap2Sim
- App host: https://build-small-hackathon-snap2sim.hf.space
- Demo video: https://youtu.be/nuisDKMyyF8
- X post: https://x.com/Ryno67114241/status/2066660199558152411
---
## Goal
Build a Gradio app deployed as a Hugging Face Space that takes a photo of a hardware component (gear, valve, pump, lock, engine part, etc.) and produces an animated 3D visualization showing how that component works internally — "open it up and show me the moving parts and the mechanism."
---
## Hard Constraints
- Total model parameters across the entire pipeline **≤ 32B**
- Must be a **Gradio app** hosted as a **Hugging Face Space**
- No cloud AI APIs at inference time where possible (targets "Off the Grid" bonus)
- **Plain HTML/CSS/JS frontend** — no React, no build step, no bundler
---
## Target User — The Curious Tinkerer / Maker
Someone who pulls apart old electronics, finds a mystery component at a thrift store or salvage yard, or cracks open a broken appliance wondering: *"what does this actually do and how does it work inside?"* Not an engineer — a curious, hands-on person who learns by taking things apart.
> **README must open with:** *"You find a small metal cylinder at a flea market. What is it? How does it work inside?"* — before any technical description.
---
## Archived Current State (June 13, 2026)
Scaffold existed at [github.com/Bigstonks1/Snap2Sim](https://github.com/Bigstonks1/Snap2Sim), initially synced to HF Space `jasondo111/Snap2Sim`. The finished project was later transferred to the public Build Small Hackathon Space listed above.
**Confirmed working:**
- Modal deployment: `snap2sim-inside-the-machine` (bigstonks1 workspace)
- `smoke_test_llamacpp_image` → `"ok": true` for `UD-Q4_K_M` + `mmproj-F16.gguf`
- `analyze_image_llamacpp` and scene-generation Modal endpoints deployed
- GitHub → HF Space sync workflow live and passing
- Local app code now uses `gradio.Server` plus a trusted `index.html`
**Archived primary tasks (completed):**
1. Deploy the current branch through GitHub → HF sync
2. Confirm the deployed Space loads the trusted `index.html`
3. Confirm `/analyze_image` and `/generate_scene` respond through the secured
Modal bearer-token flow
Final result: deployed through GitHub-to-Hugging Face sync, verified on the
public org-owned Space, and completed with public demo video and X post links.
---
## Model Stack
**Primary model:** NVIDIA Nemotron 3 Nano Omni (30B-A3B, MoE, ~3B active params)
Used for both vision analysis and A-Frame scene generation (two prompt turns, same model). Targets the **NVIDIA Nemotron Quest** sponsor award. ~31B total — under the 32B cap.
### GGUF Path (confirmed working)
| Setting | Value |
|---|---|
| Repo | `unsloth/NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-GGUF` |
| Primary quant | `UD-Q4_K_M` (~24 GB) + `mmproj-F16.gguf` |
| Fallback quant | `UD-IQ2_M` (~18.5 GB) |
| Runtime | `llama-mtmd-cli` via llama.cpp on Modal GPU |
### Fallback Split Pipeline
Only use if primary model code-gen quality is too weak:
- **Vision:** NVIDIA Nemotron Nano V2 VL (12B)
- **Scene gen:** Qwen2.5-Coder-14B
- Total ~26B · still qualifies for Nemotron Quest
---
## Model Runtime
- Inference via **llama.cpp / GGUF** → targets **"Llama Champion"** bonus
- Modal GPU endpoints called over HTTP with Bearer token auth (`SNAP2SIM_API_TOKEN`)
- Backend swappable via `INFERENCE_BACKEND=modal | zerogpu | local`
- **If Modal deployed:** "Off the Grid" not claimed, but Llama Champion + Nemotron Quest + Modal Award all apply
- **If ZeroGPU sufficient:** "Off the Grid" additionally claimable
---
## Deployment Architecture
```
[HF Space — CPU tier] [Modal — GPU tier]
gradio.Server analyze_image_llamacpp
@app.get("/") → index.html ←→ generate_scene
@app.api() → /analyze_image (weights cached in Modal Volume)
@app.api() → /generate_scene
```
- Gradio app calls Modal endpoints over HTTP via `requests`, image passed as base64
- Modal cold starts on 30B model can take tens of seconds → show `"WAKING THE WORKSHOP..."` loading state
---
## Architectural Shift — `gr.Blocks` → `gradio.Server`
> **This is the core change. Do not skip or partially implement it.**
### Why
`gr.HTML` strips `
```
> **CDN fallback:** If HF Spaces blocks `aframe.io`, vendor the minified A-Frame JS (~1.1MB) as a static file served via `gradio.Server`'s FastAPI static file mounting.
**Injecting model output:**
```javascript
document.getElementById('viewport').innerHTML = modelGeneratedAframeHTML;
// A-Frame runtime picks up the new tags automatically
```
**Example of what the model should output:**
```html
```
**Prompt engineering for `generate_scene` endpoint:**
- Instruct the model to output **only** the `...` block — no preamble, no markdown fences, no explanation
- A-Frame primitives to use: ``, ``, ``, ``, ``, ``
- Animation format: `animation="property: rotation; to: 0 360 0; loop: true; dur: 2000; easing: linear"`
- Keep scenes to **3–6 parts maximum** for clarity
- Set `` to match the page background
### Layer 2 — Deterministic Fallback: Three.js (human-written)
If A-Frame output is empty, malformed, or renders blank after 3 seconds, immediately swap to `buildDeterministicScene(json)` — a JS function that reads the mechanism JSON and builds a reliable Three.js scene from geometric primitives.
```javascript
function buildDeterministicScene(mechanismJson) {
// Human-written. Always works given valid JSON.
// Uses: BoxGeometry, CylinderGeometry, TorusGeometry per part shape
// Applies rotation/translation per motion_type
// Adds OrbitControls, annotation labels
// The viewport must never be blank or show an error
}
```
> Load Three.js in `index.html` alongside A-Frame:
> ```html
>
> ```
---
## Pipeline / Application Flow
```
1. User uploads photo
↓
2. Frontend encodes as base64 → calls /analyze_image (Gradio JS client)
↓
3. Nemotron vision step → structured JSON:
{
"component_name": "Solenoid Valve",
"parts": [
{ "name": "Coil", "shape": "cylinder", "color": "#E8A33D",
"position": [0, 0, 0], "motion_type": "none", "motion_params": {} },
{ "name": "Plunger", "shape": "cylinder", "color": "#5FD4D0",
"position": [0, 0.5, 0], "motion_type": "translate",
"motion_params": { "axis": "y", "range": 0.3, "dur": 800 } }
],
"summary": "When current flows through the coil, it generates a magnetic
field that pulls the plunger upward, opening the valve port."
}
↓
4. Frontend populates analysis panel (name, part list, summary)
→ immediately calls /generate_scene with JSON
↓
5. Nemotron scene gen step → A-Frame HTML string (...)
↓
6. Frontend injects A-Frame HTML → innerHTML of #viewport
A-Frame runtime renders automatically
↓
7. If A-Frame blank/failed after 3s → buildDeterministicScene(json)
Viewport is NEVER blank
```
---
## Visual Design — "Industrial Instrument Panel / Field Cutaway"
> Implement the shell and CSS in Step 2. Do **not** work on `[POLISH]` items until Steps 1–4 are done.
### Color System
```css
:root {
--bg: #0F1318;
--bg-panel: #161B22;
--bg-lift: #1E2530;
--amber: #E8A33D;
--amber-dim: #7A5420;
--cyan: #5FD4D0;
--cyan-dim: #2A5E5C;
--text: #C8C0AC;
--text-muted: #6B7280;
--grid: rgba(255,255,255,0.04);
}
```
### Typography
Load from **Bunny Fonts** (not Google Fonts):
- **Display / headings / UI labels:** `Chakra Petch` — technical, instrument-panel character
- **Monospace / data / callouts:** `Fira Code`
- **Never use:** Inter, Roboto, Arial, Space Grotesk, or any system font
### Layout
- Two-pane asymmetric split: **63% viewport** (left) · **37% analysis panel** (right)
- Blueprint grid: 1px lines at `--grid` opacity, 32px spacing, on `--bg` base
- Panel separator: 1px vertical line in `--amber-dim`
- Upload drop zone: fills viewport · dashed 1px `--amber-dim` border (no `border-radius`) · centered `"DROP COMPONENT PHOTO"` in Chakra Petch uppercase `--text-muted` · on drag-over: border → `--amber`, text → `--amber`
- Play/pause: minimal amber rectangle (no `border-radius`), Chakra Petch uppercase `"PAUSE"` / `"RESUME"`, controls A-Frame animation playback via JS
### Loading States
All in Chakra Petch uppercase, `--amber` color, with thin `--amber` indeterminate progress bar across viewport top:
| State | Message |
|---|---|
| Modal cold start | `WAKING THE WORKSHOP...` |
| Vision inference | `ANALYZING ASSEMBLY...` |
| Scene generation | `RENDERING CUTAWAY...` |
### [POLISH] — Only After Steps 1–4 Work
Implement in this sub-order:
1. **Component name watermark** — large (`clamp(4rem, 8vw, 9rem)`) Chakra Petch uppercase in `--bg-lift`, absolutely positioned bleeding across both panes from bottom-left, `z-index` below content. Populated from `component_name` in JSON.
2. **Noise texture** — SVG `feTurbulence` grain at 3% opacity on viewport pane, inline `data:` URI, no external file. Makes the surface feel physical.
3. **Vignette** — radial gradient overlay on viewport edges, `pointer-events: none` so it floats above the A-Frame canvas without blocking interaction.
4. **Scan-line reveal** — when A-Frame scene first loads:
- 2px `--cyan` scan-line sweeps top→bottom over 0.7s
- Each A-Frame entity fades in as line passes it (`opacity 0→1`, `translateY 12px→0`, 0.35s ease-out, staggered by part index via `animation-delay`)
- Part labels fade in together (0.25s)
- Progress bar dissolves (0.2s)
- Total: ~1.2s · this is the signature moment
---
## Deliverables
- `app.py` — `gradio.Server` app (~50 lines)
- `index.html` — self-contained HTML/CSS/JS; A-Frame + Three.js from CDN; Gradio JS client from CDN
- `snap2sim/backend.py` — `generate_scene`
- `modal_app.py` — `generate_scene`; A-Frame prompt
- `snap2sim/prompts.py` — updated A-Frame scene generation prompt
- `requirements.txt` — updated if needed for `gradio.Server`
- `README.md` — tinkerer/maker story hook → project description → model stack with exact parameter breakdown (≤32B) → rendering stack rationale → bonus quest claims:
| Quest | Status |
|---|---|
| Llama Champion | ✅ Confirmed |
| NVIDIA Nemotron Quest | ✅ Confirmed |
| Off-Brand | ✅ Confirmed |
| Modal Award | ✅ Confirmed |
| Off the Grid | ⚡ If ZeroGPU used in final deploy |
| Field Notes | 🎯 Stretch |
---
## Original Start Order (completed)
> Follow this exactly. Do not skip ahead.
### Step 1 — Critical Path
Deploy the current `gradio.Server` + `index.html` implementation through
GitHub -> HF sync. Confirm the deployed Space loads and both `/analyze_image`
and `/generate_scene` respond.
### Step 2 — Make It Functional
Build `buildDeterministicScene(json)` in JS — Three.js scene from geometric primitives, always works given valid JSON. Wire the full pipeline: upload → `/analyze_image` → populate analysis panel → `/generate_scene` → inject A-Frame HTML via `innerHTML` → fallback to `buildDeterministicScene(json)` if A-Frame fails or blanks after 3 seconds. **Confirm end-to-end with a real image and live Modal endpoints.**
### Step 3 — Apply the Design Shell
Add CSS variable system, Chakra Petch + Fira Code from Bunny Fonts, two-pane asymmetric layout, blueprint grid, loading states with progress bar, upload drop zone, panel separator. App should match the design direction above — minus `[POLISH]` items.
### Step 4 — Harden
Error handling, 3-second blank-scene timeout before fallback triggers, Modal cold-start messaging, A-Frame `` matching page background, play/pause toggle wired to A-Frame animation playback. Verify GitHub -> HF Space sync pushes cleanly and the deployed Space runs correctly.
### Step 5 — [POLISH] Only if Time Remains
Component name watermark → noise texture → vignette → scan-line reveal. In that order.