stardust-coder commited on
Commit
6ef0b79
·
1 Parent(s): 391db3d

[add] first commit

Browse files
Files changed (2) hide show
  1. app.py +302 -0
  2. requirements.txt +6 -0
app.py ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import sys
4
+ import base64
5
+ import shutil
6
+ import mimetypes
7
+ import subprocess
8
+ import tempfile
9
+ from pathlib import Path
10
+
11
+ # =========================
12
+ # Paths / environment
13
+ # =========================
14
+ BASE_DIR = Path.cwd().resolve()
15
+ GRADIO_TEMP_DIR = BASE_DIR / "gradio_tmp"
16
+ ARTIFACT_DIR = BASE_DIR / "outputs"
17
+
18
+ GRADIO_TEMP_DIR.mkdir(parents=True, exist_ok=True)
19
+ ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)
20
+
21
+ os.environ["GRADIO_TEMP_DIR"] = str(GRADIO_TEMP_DIR)
22
+
23
+ import gradio as gr
24
+ from anthropic import Anthropic
25
+ from openai import OpenAI
26
+
27
+ # =========================
28
+ # Clients
29
+ # =========================
30
+
31
+ ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
32
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
33
+
34
+ if not ANTHROPIC_API_KEY:
35
+ raise RuntimeError("ANTHROPIC_API_KEY is not set")
36
+
37
+ if not OPENAI_API_KEY:
38
+ raise RuntimeError("OPENAI_API_KEY is not set")
39
+
40
+ anthropic_client = Anthropic(api_key=ANTHROPIC_API_KEY)
41
+ openai_client = OpenAI(api_key=OPENAI_API_KEY)
42
+
43
+ # =========================
44
+ # Model options
45
+ # =========================
46
+
47
+ CLAUDE_MODELS = [
48
+ "claude-opus-4-6",
49
+ "claude-sonnet-4-6",
50
+ "claude-sonnet-4-5",
51
+ "claude-haiku-4-5-20251001"
52
+ ]
53
+
54
+ OPENAI_MODELS = [
55
+ "gpt-5.4-pro-2026-03-05",
56
+ "gpt-5.2-2025-12-11",
57
+ "gpt-5-nano-2025-08-07",
58
+ ]
59
+
60
+ DEFAULT_CLAUDE_MODEL = "claude-sonnet-4-5"
61
+ DEFAULT_OPENAI_MODEL = "gpt-5-nano-2025-08-07"
62
+
63
+ # =========================
64
+ # Helpers
65
+ # =========================
66
+
67
+ def strip_code_fences(text: str) -> str:
68
+ text = text.strip()
69
+ match = re.search(r"```(?:python)?\s*(.*?)```", text, re.DOTALL | re.IGNORECASE)
70
+ if match:
71
+ return match.group(1).strip()
72
+ return text
73
+
74
+
75
+ def detect_media_type(image_path: str) -> str:
76
+ media_type, _ = mimetypes.guess_type(image_path)
77
+ return media_type or "application/octet-stream"
78
+
79
+
80
+ def copy_to_artifact(src_path: Path, suffix: str) -> str:
81
+ dst = ARTIFACT_DIR / f"{next(tempfile._get_candidate_names())}{suffix}"
82
+ shutil.copy2(src_path, dst)
83
+ return str(dst.resolve())
84
+
85
+
86
+ def truncate_text(text: str, max_chars: int = 4000) -> str:
87
+ if not text:
88
+ return ""
89
+ return text[-max_chars:]
90
+
91
+
92
+ # =========================
93
+ # LLM steps
94
+ # =========================
95
+
96
+ def generate_cad_code(text_prompt, image_path, claude_model):
97
+ content = []
98
+
99
+ spec_text = (text_prompt or "").strip()
100
+ if spec_text:
101
+ content.append(
102
+ {
103
+ "type": "text",
104
+ "text": f"""Generate CadQuery Python code to build a 3D CAD model.
105
+
106
+ Requirements:
107
+ - Use CadQuery only
108
+ - Save files in the current working directory
109
+ - Export BOTH:
110
+ - STEP as ./output.step
111
+ - STL as ./output.stl
112
+ - The script must be directly executable with `python model.py`
113
+ - Do not use markdown fences
114
+ - Output ONLY valid Python code
115
+ - At the end, ensure the exports are actually executed
116
+
117
+ Specification:
118
+ {spec_text}
119
+ """,
120
+ }
121
+ )
122
+
123
+ if image_path:
124
+ with open(image_path, "rb") as f:
125
+ img_b64 = base64.b64encode(f.read()).decode("utf-8")
126
+
127
+ content.append(
128
+ {
129
+ "type": "image",
130
+ "source": {
131
+ "type": "base64",
132
+ "media_type": detect_media_type(image_path),
133
+ "data": img_b64,
134
+ },
135
+ }
136
+ )
137
+
138
+ if not content:
139
+ raise gr.Error("Text CAD specification or image is required.")
140
+
141
+ response = anthropic_client.messages.create(
142
+ model=claude_model,
143
+ max_tokens=2500,
144
+ messages=[{"role": "user", "content": content}],
145
+ )
146
+
147
+ code = "".join(
148
+ block.text
149
+ for block in response.content
150
+ if getattr(block, "type", None) == "text"
151
+ ).strip()
152
+
153
+ code = strip_code_fences(code)
154
+
155
+ if not code:
156
+ raise gr.Error("Claude returned empty code.")
157
+
158
+ return code
159
+
160
+
161
+ def run_cadquery(code):
162
+ with tempfile.TemporaryDirectory(dir=str(GRADIO_TEMP_DIR)) as tmpdir:
163
+ tmpdir = Path(tmpdir)
164
+ script_path = tmpdir / "model.py"
165
+
166
+ with open(script_path, "w", encoding="utf-8") as f:
167
+ f.write(code)
168
+
169
+ result = subprocess.run(
170
+ [sys.executable, str(script_path)],
171
+ cwd=str(tmpdir),
172
+ capture_output=True,
173
+ text=True,
174
+ timeout=180,
175
+ )
176
+
177
+ if result.returncode != 0:
178
+ raise gr.Error(
179
+ "CadQuery execution failed.\n\n"
180
+ f"STDOUT:\n{truncate_text(result.stdout)}\n\n"
181
+ f"STDERR:\n{truncate_text(result.stderr)}"
182
+ )
183
+
184
+ step_path = tmpdir / "output.step"
185
+ stl_path = tmpdir / "output.stl"
186
+
187
+ files = [p.name for p in tmpdir.iterdir()]
188
+
189
+ if not step_path.exists() and not stl_path.exists():
190
+ raise gr.Error(
191
+ "CAD script finished but did not generate output.step or output.stl.\n\n"
192
+ f"Files found in temp dir: {files}\n\n"
193
+ "Make sure the generated CadQuery code exports exactly "
194
+ "./output.step and ./output.stl"
195
+ )
196
+
197
+ if not step_path.exists():
198
+ raise gr.Error(
199
+ "output.step was not created.\n\n"
200
+ f"Files found in temp dir: {files}"
201
+ )
202
+
203
+ if not stl_path.exists():
204
+ raise gr.Error(
205
+ "output.stl was not created.\n\n"
206
+ f"Files found in temp dir: {files}"
207
+ )
208
+
209
+ final_step = copy_to_artifact(step_path, ".step")
210
+ final_stl = copy_to_artifact(stl_path, ".stl")
211
+
212
+ return final_step, final_stl
213
+
214
+
215
+ def gpt_check(step_path, openai_model):
216
+ if not step_path or not os.path.exists(step_path):
217
+ return "STEP file not found, so review was skipped."
218
+
219
+ size = os.path.getsize(step_path)
220
+
221
+ prompt = f"""A CAD model STEP file was generated.
222
+
223
+ File size: {size} bytes
224
+
225
+ Check if this seems reasonable for a CAD model and list possible issues.
226
+ Keep the answer concise.
227
+ """
228
+
229
+ response = openai_client.responses.create(
230
+ model=openai_model,
231
+ input=prompt,
232
+ )
233
+
234
+ return response.output_text.strip()
235
+
236
+
237
+ # =========================
238
+ # Pipeline
239
+ # =========================
240
+
241
+ def pipeline(text_prompt, image, claude_model, openai_model):
242
+ code = generate_cad_code(text_prompt, image, claude_model)
243
+ step_path, stl_path = run_cadquery(code)
244
+ review = gpt_check(step_path, openai_model)
245
+ return stl_path, code, review, step_path
246
+
247
+
248
+ # =========================
249
+ # UI
250
+ # =========================
251
+
252
+ with gr.Blocks(delete_cache=(86400, 86400)) as demo:
253
+ gr.Markdown("## CAD Generator")
254
+
255
+ with gr.Row():
256
+ with gr.Column(scale=1):
257
+ claude_model = gr.Radio(
258
+ choices=CLAUDE_MODELS,
259
+ value=DEFAULT_CLAUDE_MODEL,
260
+ label="Claude model for CAD code generation"
261
+ )
262
+
263
+ openai_model = gr.Radio(
264
+ choices=OPENAI_MODELS,
265
+ value=DEFAULT_OPENAI_MODEL,
266
+ label="OpenAI model for STEP review"
267
+ )
268
+
269
+ text_prompt = gr.Textbox(
270
+ label="Text CAD specification",
271
+ lines=10,
272
+ placeholder="e.g. A 60mm x 40mm x 10mm enclosure with 4 corner holes..."
273
+ )
274
+
275
+ image_input = gr.Image(
276
+ label="2D drawing (optional)",
277
+ type="filepath"
278
+ )
279
+
280
+ run_btn = gr.Button("Generate CAD")
281
+
282
+ with gr.Column(scale=3):
283
+ viewer = gr.Model3D(label="CAD Viewer (STL)")
284
+
285
+ code_box = gr.Code(label="Generated CAD Code", language="python")
286
+
287
+ review_box = gr.Textbox(
288
+ label="GPT Model Check",
289
+ lines=8
290
+ )
291
+
292
+ step_file = gr.File(label="Download STEP")
293
+
294
+ run_btn.click(
295
+ fn=pipeline,
296
+ inputs=[text_prompt, image_input, claude_model, openai_model],
297
+ outputs=[viewer, code_box, review_box, step_file]
298
+ )
299
+
300
+ demo.launch(
301
+ allowed_paths=[str(ARTIFACT_DIR), str(GRADIO_TEMP_DIR)]
302
+ )
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ gradio
2
+ anthropic
3
+ openai
4
+ cadquery
5
+ numpy
6
+ trimesh