Spaces:
Running
Running
| # 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 `<script>` tags for security and HF Spaces CSP blocks external CDN imports in `js_on_load`. Any WebGL/Three.js/A-Frame output piped through `gr.HTML` will fail on the live Space β scripts get stripped, nothing renders. This is a confirmed, known issue. | |
| ### How `gradio.Server` Fixes It | |
| `gradio.Server` extends FastAPI. `@app.get("/")` serves `index.html` as a first-class trusted FastAPI response β the browser receives a full page with no stripping, no sandboxing, no CSP conflicts from Gradio's component system. A-Frame and Three.js CDN scripts load normally. | |
| ```python | |
| from gradio import Server | |
| app = Server() | |
| @app.get("/") | |
| async def homepage(): | |
| with open("index.html") as f: | |
| return HTMLResponse(f.read()) | |
| @app.api(name="analyze_image") | |
| def analyze_image(image_b64: str) -> dict: | |
| return backend.run_analysis(image_b64) # calls Modal or local placeholder | |
| @app.api(name="generate_scene") | |
| def generate_scene(mechanism_json: dict) -> str: | |
| return backend.run_scene_gen(mechanism_json) # returns A-Frame HTML string | |
| app.launch() | |
| ``` | |
| ### What to Change in the Scaffold | |
| | File | Action | | |
| |---|---| | |
| | `app.py` | `gradio.Server` app serving `index.html` and API routes | | |
| | `index.html` | Trusted HTML/CSS/JS shell with pipeline orchestration | | |
| | `modal_app.py` | `generate_scene` endpoints and A-Frame prompt | | |
| | `snap2sim/backend.py` | `generate_scene` backend method | | |
| | `snap2sim/prompts.py` | A-Frame scene-generation prompt | | |
| | `snap2sim/aframe_scene.py` | Deterministic A-Frame placeholder scene | | |
| --- | |
| ## Rendering Stack β Two Layers | |
| ### Layer 1 β Model-Generated Scene: A-Frame (declarative HTML) | |
| A-Frame is a web framework built on Three.js that uses declarative HTML tags for 3D scenes. The model outputs HTML, not JavaScript β far more reliable for LLM generation. | |
| **Why A-Frame for model output:** | |
| - LLMs generate HTML tags far more reliably than imperative JS | |
| - Injected via `innerHTML`, not `eval()` β no script execution risk | |
| - A-Frame runtime (already loaded in `<head>`) renders injected tags automatically | |
| - Built-in `animation` attribute handles motion without JS animation loops | |
| - Camera, lighting, and sky added automatically β less boilerplate to get wrong | |
| **Loading A-Frame in `index.html`:** | |
| ```html | |
| <head> | |
| <script src="https://aframe.io/releases/1.6.0/aframe.min.js"></script> | |
| </head> | |
| ``` | |
| > **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 <a-scene> tags automatically | |
| ``` | |
| **Example of what the model should output:** | |
| ```html | |
| <a-scene> | |
| <a-sky color="#0F1318"></a-sky> | |
| <a-cylinder color="#E8A33D" radius="0.3" height="1" position="0 1 -3" | |
| animation="property: rotation; to: 0 360 0; loop: true; dur: 2000; easing: linear"> | |
| </a-cylinder> | |
| <a-box color="#5FD4D0" position="0.8 0.5 -3" | |
| animation="property: position; to: 0.8 1 -3; dir: alternate; loop: true; dur: 1000"> | |
| </a-box> | |
| <a-text value="Drive Shaft" position="0 2 -3" color="#5FD4D0" scale="0.5 0.5 0.5"> | |
| </a-text> | |
| </a-scene> | |
| ``` | |
| **Prompt engineering for `generate_scene` endpoint:** | |
| - Instruct the model to output **only** the `<a-scene>...</a-scene>` block β no preamble, no markdown fences, no explanation | |
| - A-Frame primitives to use: `<a-box>`, `<a-cylinder>`, `<a-sphere>`, `<a-torus>`, `<a-cone>`, `<a-entity>` | |
| - 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 `<a-sky color="#0F1318">` 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 | |
| > <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| > ``` | |
| --- | |
| ## 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 (<a-scene>...</a-scene>) | |
| β | |
| 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 `<a-sky color="#0F1318">` 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. | |