noahlee1234 commited on
Commit
7a5533a
Β·
1 Parent(s): 7c53aed

NaturalCAD: Add LLM via HF Inference, GLB export, and update docs

Browse files
Files changed (2) hide show
  1. apps/cad-worker/main.py +222 -0
  2. docs/architecture-plan.md +67 -0
apps/cad-worker/main.py ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ NaturalCAD Modal Function
3
+ Takes user prompt, generates build123d code, runs it, returns STL.
4
+ """
5
+
6
+ import modal
7
+ from pathlib import Path
8
+ import tempfile
9
+
10
+ app = modal.App("naturalcad")
11
+
12
+ # Base image with Python 3.10 and graphics libraries
13
+ image = (
14
+ modal.Image.from_registry("python:3.10-slim")
15
+ .apt_install(
16
+ "libgl1",
17
+ "libglib2.0-0",
18
+ "libxrender1",
19
+ "libxext6",
20
+ "libxkbcommon0"
21
+ )
22
+ .pip_install("build123d==0.10.0", "trimesh", "huggingface_hub", "httpx")
23
+ )
24
+
25
+
26
+ def _upload_to_supabase(storage_key: str, file_data: bytes, content_type: str = "application/octet-stream") -> str:
27
+ import httpx
28
+ import urllib.parse
29
+ import os
30
+
31
+ url = os.environ.get("SUPABASE_URL", "").rstrip("/")
32
+ key = os.environ.get("SUPABASE_SERVICE_ROLE_KEY", "")
33
+ bucket = os.environ.get("SUPABASE_BUCKET", "naturalCAD-artifacts")
34
+
35
+ if not url or not key:
36
+ raise ValueError("Missing Supabase credentials in environment")
37
+
38
+ encoded_key = urllib.parse.quote(storage_key, safe="/")
39
+ endpoint = f"{url}/storage/v1/object/{bucket}/{encoded_key}"
40
+
41
+ headers = {
42
+ "Authorization": f"Bearer {key}",
43
+ "Content-Type": content_type,
44
+ "x-upsert": "true"
45
+ }
46
+
47
+ with httpx.Client() as client:
48
+ resp = client.post(endpoint, content=file_data, headers=headers)
49
+ if resp.status_code >= 400:
50
+ raise Exception(f"Supabase upload failed {resp.status_code}: {resp.text}")
51
+
52
+ return f"{url}/storage/v1/object/public/{bucket}/{encoded_key}"
53
+
54
+
55
+ @app.function(
56
+ image=image,
57
+ gpu="T4",
58
+ timeout=300,
59
+ secrets=[
60
+ modal.Secret.from_name("huggingface-secret"),
61
+ modal.Secret.from_name("supabase-secret")
62
+ ]
63
+ )
64
+ @modal.web_endpoint(method="POST")
65
+ def generate_cad_endpoint(prompt: str, output_format: str = "stl"):
66
+ return generate_cad.local(prompt, output_format)
67
+
68
+
69
+ @app.function(
70
+ image=image,
71
+ gpu="T4",
72
+ timeout=300,
73
+ secrets=[
74
+ modal.Secret.from_name("huggingface-secret"),
75
+ modal.Secret.from_name("supabase-secret")
76
+ ]
77
+ )
78
+ def generate_cad(prompt: str, output_format: str = "stl"):
79
+ """Main function: prompt -> LLM -> code -> build123d -> Supabase STL URL"""
80
+ import os
81
+ import uuid
82
+ from huggingface_hub import InferenceClient
83
+
84
+ # 1. LLM Code Generation
85
+ hf_token = os.environ.get("HF_TOKEN")
86
+ if not hf_token:
87
+ return {"error": "HF_TOKEN not found in environment secrets"}
88
+
89
+ client = InferenceClient(
90
+ model="Qwen/Qwen2.5-Coder-32B-Instruct",
91
+ token=hf_token
92
+ )
93
+
94
+ system_prompt = """You are an expert Python developer for CAD code generation using the build123d library.
95
+ Write Python code to create the 3D model requested by the user.
96
+
97
+ Rules:
98
+ 1. ONLY return valid Python code. No markdown formatting, no explanations.
99
+ 2. ALWAYS import build123d using: `from build123d import *`
100
+ 3. ALWAYS store the final resulting Shape/Part in a variable named `result`.
101
+ 4. Use standard primitives like Box, Cylinder, Rectangle, Circle, etc.
102
+ 5. Make sure the code is simple, correct and uses the modern builder API (with BuildPart() as bp, etc.).
103
+
104
+ Example:
105
+ from build123d import *
106
+ width = 60
107
+ height = 40
108
+ thickness = 6
109
+ with BuildPart() as bp:
110
+ with BuildSketch(Plane.XY) as base:
111
+ Rectangle(width, height)
112
+ extrude(amount=thickness)
113
+ result = bp.part
114
+ """
115
+
116
+ print(f"Calling LLM for prompt: {prompt}")
117
+ try:
118
+ response = client.chat.completions.create(
119
+ messages=[
120
+ {"role": "system", "content": system_prompt},
121
+ {"role": "user", "content": prompt}
122
+ ],
123
+ max_tokens=1024,
124
+ temperature=0.2,
125
+ )
126
+ generated_code = response.choices[0].message.content.strip()
127
+
128
+ # Clean up markdown
129
+ if generated_code.startswith("```python"):
130
+ generated_code = generated_code[9:]
131
+ elif generated_code.startswith("```"):
132
+ generated_code = generated_code[3:]
133
+ if generated_code.endswith("```"):
134
+ generated_code = generated_code[:-3]
135
+
136
+ generated_code = generated_code.strip()
137
+ except Exception as e:
138
+ return {"error": f"LLM code generation failed: {e}"}
139
+
140
+ print(f"Generated Code:\n{generated_code}")
141
+
142
+
143
+ # Run build123d
144
+ from build123d import export_stl, export_step
145
+
146
+ with tempfile.TemporaryDirectory() as tmpdir:
147
+ script_path = Path(tmpdir) / "script.py"
148
+ script_path.write_text(generated_code)
149
+
150
+ output_file = Path(tmpdir) / f"output.{output_format}"
151
+
152
+ # Execute
153
+ exec_globals = {}
154
+ exec(compile(generated_code, str(script_path), "exec"), exec_globals)
155
+ result_shape = exec_globals.get("result")
156
+
157
+ if not result_shape:
158
+ return {"error": "No geometry generated"}
159
+
160
+ # Get shape
161
+ shape = result_shape
162
+ # In newer build123d, parts don't need wrapping extracted for export
163
+ # We just pass the Part or Shape object directly
164
+
165
+ # Export and upload all formats
166
+ urls = {}
167
+ run_id = uuid.uuid4().hex[:8]
168
+
169
+ # Make STL and STEP
170
+ export_stl(shape, str(Path(tmpdir) / "output.stl"))
171
+ export_step(shape, str(Path(tmpdir) / "output.step"))
172
+
173
+ # Make GLB preview
174
+ from trimesh import load_mesh
175
+ import trimesh.transformations as tf
176
+ import math
177
+
178
+ mesh = load_mesh(str(Path(tmpdir) / "output.stl"), force="mesh")
179
+ # Rotate -90 degrees around X axis so Z is up in the browser
180
+ mesh.apply_transform(tf.rotation_matrix(-math.pi/2, [1, 0, 0]))
181
+ mesh.export(str(Path(tmpdir) / "output.glb"))
182
+
183
+ for fmt in ["stl", "step", "glb"]:
184
+ out_file = Path(tmpdir) / f"output.{fmt}"
185
+
186
+ if fmt == "stl":
187
+ content_type = "model/stl"
188
+ elif fmt == "step":
189
+ content_type = "application/octet-stream"
190
+ else:
191
+ content_type = "model/gltf-binary"
192
+
193
+ storage_key = f"runs/{run_id}/model.{fmt}"
194
+
195
+ print(f"Uploading {fmt} artifact to Supabase...")
196
+ file_bytes = out_file.read_bytes()
197
+
198
+ try:
199
+ public_url = _upload_to_supabase(storage_key, file_bytes, content_type)
200
+ urls[fmt] = public_url
201
+ except Exception as e:
202
+ return {"error": f"Supabase upload failed for {fmt}: {e}", "code": generated_code}
203
+
204
+ return {
205
+ "success": True,
206
+ "urls": urls,
207
+ "prompt": prompt,
208
+ "generated_code": generated_code
209
+ }
210
+
211
+
212
+ @app.function(image=image)
213
+ def health_check():
214
+ """Verify build123d works"""
215
+ from build123d import Box
216
+ return {"status": "ok", "build123d": "working"}
217
+
218
+
219
+ if __name__ == "__main__":
220
+ # Test locally in the container
221
+ result = generate_cad.call("a simple bracket plate")
222
+ print(result)
docs/architecture-plan.md ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # NaturalCAD Architecture Plan
2
+
3
+ ## Current State (2026-04-12)
4
+
5
+ We successfully pivoted away from a complex (and broken) Fly.io worker and Hugging Face Docker setup, replacing the backend engine entirely with **Modal**.
6
+
7
+ ### What's Working
8
+ - βœ… **LLM Code Generation**: Using `Qwen/Qwen2.5-Coder-32B-Instruct` via Hugging Face Serverless Inference API inside Modal.
9
+ - βœ… **CAD Execution Engine**: Modal spins up a `python:3.10-slim` container with proper Linux OpenGL libraries (`libgl1`, `libglib2.0-0`, etc). `build123d` runs locally on the T4 GPU container.
10
+ - βœ… **Artifact Upload**: Modal container runs CAD, creates STL, STEP, and a browser-ready GLB using `trimesh`, and uploads directly to Supabase Storage, returning public URLs.
11
+ - βœ… **HF Spaces UI**: Front-end UI remains on Hugging Face Spaces.
12
+
13
+ ### What's Deprecated
14
+ - ❌ Fly.io backend routing and worker loops (too much complexity/overhead for MVP).
15
+ - ❌ Hugging Face native Docker CAD execution (lacks host graphics libs for VTK).
16
+
17
+ ---
18
+
19
+ ## Target Architecture
20
+
21
+ ```
22
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
23
+ β”‚ User │────▢ HF Spaces (Gradio UI)
24
+ β”‚ Prompt β”‚
25
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
26
+ β”‚
27
+ β–Ό
28
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
29
+ β”‚ Modal Web Endpoint β”‚
30
+ β”‚ β”‚
31
+ β”‚ 1. Calls HF Inference API (Qwen 2.5) β”‚
32
+ β”‚ 2. LLM writes build123d Python script β”‚
33
+ β”‚ 3. Executes script on Modal Container β”‚
34
+ β”‚ 4. Generates STL + STEP + GLB preview β”‚
35
+ β”‚ 5. Uploads files to Supabase β”‚
36
+ β”‚ 6. Returns 3 URLs back to HF Space β”‚
37
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
38
+ β”‚
39
+ β–Ό
40
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
41
+ β”‚ Supabase β”‚
42
+ β”‚ Storage β”‚
43
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
44
+ ```
45
+
46
+ ## Services
47
+
48
+ | Service | Role | Cost | Status |
49
+ |---------|------|-----|--------|
50
+ | **HF Spaces** | UI/Frontend | Free tier | βœ… Ready |
51
+ | **Modal** | Web API + LLM call + CAD Execution | Pay-per-use GPU | βœ… Ready |
52
+ | **HF Inference API**| LLM (textβ†’code) | Free within limits | βœ… Ready |
53
+ | **Supabase** | DB + Storage | Free tier | βœ… Ready |
54
+
55
+ ---
56
+
57
+ ## Implementation Order
58
+
59
+ 1. βœ… **Create Modal function** for CAD execution
60
+ 2. βœ… **Add LLM generation via HF** to the Modal container
61
+ 3. βœ… **Add Supabase Artifact Upload** returning public URL
62
+ 4. πŸ”² **Deploy Modal Web Endpoint** to get a live URL
63
+ 5. πŸ”² **Wire HF Spaces** to hit the Modal endpoint, parsing out the STL, STEP, and GLB urls into the Gradio UI.
64
+
65
+ ---
66
+
67
+ *Updated: 2026-04-12*