twarner commited on
Commit
1349d75
·
1 Parent(s): 0073cbe

Add visual gcode preview with SVG rendering

Browse files
Files changed (1) hide show
  1. app.py +189 -44
app.py CHANGED
@@ -1,4 +1,4 @@
1
- """dcode Gradio Space - Text to Gcode inference."""
2
 
3
  import re
4
  import gradio as gr
@@ -45,25 +45,119 @@ def validate_gcode(gcode: str) -> str:
45
 
46
  x_match = re.search(r"X([-\d.]+)", line, re.IGNORECASE)
47
  if x_match:
48
- x = float(x_match.group(1))
49
- x = max(BOUNDS["left"], min(BOUNDS["right"], x))
50
- corrected = re.sub(r"X[-\d.]+", f"X{x:.2f}", corrected, flags=re.IGNORECASE)
 
 
 
51
 
52
  y_match = re.search(r"Y([-\d.]+)", line, re.IGNORECASE)
53
  if y_match:
54
- y = float(y_match.group(1))
55
- y = max(BOUNDS["bottom"], min(BOUNDS["top"], y))
56
- corrected = re.sub(r"Y[-\d.]+", f"Y{y:.2f}", corrected, flags=re.IGNORECASE)
 
 
 
57
 
58
  lines.append(corrected)
59
 
60
  return "\n".join(lines)
61
 
62
 
63
- def generate(prompt: str, model_name: str, temperature: float, max_tokens: int) -> str:
64
- """Generate gcode from prompt."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  if not prompt or not prompt.strip():
66
- return "Enter a prompt to generate gcode"
 
67
 
68
  try:
69
  model, tokenizer, device = get_model(model_name)
@@ -90,41 +184,92 @@ def generate(prompt: str, model_name: str, temperature: float, max_tokens: int)
90
 
91
  gcode = validate_gcode(gcode)
92
  line_count = len(gcode.split("\n"))
93
- return f"; dcode output - {line_count} lines\n; Model: {model_name}\n; Machine validated\n\n{gcode}"
 
 
 
 
 
 
94
  except Exception as e:
95
- return f"; Error: {e}"
96
-
97
-
98
- demo = gr.Interface(
99
- fn=generate,
100
- inputs=[
101
- gr.Textbox(label="Prompt", placeholder="drawing of a cat...", lines=2),
102
- gr.Dropdown(choices=list(MODELS.keys()), value="flan-t5-base (best)", label="Model"),
103
- gr.Slider(0.1, 1.5, value=0.8, label="Temperature", info="Higher = more creative"),
104
- gr.Slider(256, 2048, value=1024, step=256, label="Max Tokens"),
105
- ],
106
- outputs=gr.Code(label="Gcode", language=None, lines=25),
107
- title="dcode",
108
- description="**Text → Polargraph Gcode** | Generate machine-compatible gcode from natural language. [GitHub](https://github.com/Twarner491/dcode) | [Model](https://huggingface.co/twarner/dcode-flan-t5-base) | [Dataset](https://huggingface.co/datasets/twarner/dcode-polargraph-gcode)",
109
- examples=[
110
- ["drawing of a cat", "flan-t5-base (best)", 0.8, 1024],
111
- ["abstract spiral pattern", "flan-t5-base (best)", 0.9, 1024],
112
- ["simple house with chimney", "flan-t5-base (best)", 0.7, 512],
113
- ["portrait of a woman", "flan-t5-base (best)", 0.8, 1024],
114
- ],
115
- theme=gr.themes.Soft(primary_hue="emerald"),
116
- article="""
117
- ## About
118
-
119
- dcode finetunes text-to-text models to directly output polargraph-compatible gcode from natural language descriptions.
120
-
121
- **Training**: Flan-T5-base trained on 175,952 art-caption-gcode triplets for 20 epochs on H100.
122
-
123
- **Machine Bounds**: X: ±420.5mm, Y: ±594.5mm | Pen servo: 40° (down) / 90° (up)
124
-
125
- **License**: MIT
126
- """,
127
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
  if __name__ == "__main__":
130
  demo.launch()
 
1
+ """dcode Gradio Space - Text to Gcode inference with visual preview."""
2
 
3
  import re
4
  import gradio as gr
 
45
 
46
  x_match = re.search(r"X([-\d.]+)", line, re.IGNORECASE)
47
  if x_match:
48
+ try:
49
+ x = float(x_match.group(1))
50
+ x = max(BOUNDS["left"], min(BOUNDS["right"], x))
51
+ corrected = re.sub(r"X[-\d.]+", f"X{x:.2f}", corrected, flags=re.IGNORECASE)
52
+ except ValueError:
53
+ pass
54
 
55
  y_match = re.search(r"Y([-\d.]+)", line, re.IGNORECASE)
56
  if y_match:
57
+ try:
58
+ y = float(y_match.group(1))
59
+ y = max(BOUNDS["bottom"], min(BOUNDS["top"], y))
60
+ corrected = re.sub(r"Y[-\d.]+", f"Y{y:.2f}", corrected, flags=re.IGNORECASE)
61
+ except ValueError:
62
+ pass
63
 
64
  lines.append(corrected)
65
 
66
  return "\n".join(lines)
67
 
68
 
69
+ def gcode_to_svg(gcode: str) -> str:
70
+ """Convert gcode to SVG for visual preview."""
71
+ paths = []
72
+ current_path = []
73
+ x, y = 0.0, 0.0
74
+ pen_down = False
75
+
76
+ for line in gcode.split("\n"):
77
+ line = line.strip()
78
+ if not line or line.startswith(";"):
79
+ continue
80
+
81
+ # Pen state from M280 servo commands
82
+ if "M280" in line.upper():
83
+ match = re.search(r"S(\d+)", line, re.IGNORECASE)
84
+ if match:
85
+ angle = int(match.group(1))
86
+ was_down = pen_down
87
+ pen_down = angle < 50 # 40 = down, 90 = up
88
+ if was_down and not pen_down and len(current_path) > 1:
89
+ paths.append(current_path[:])
90
+ current_path = []
91
+
92
+ # Position from G0/G1 commands
93
+ x_match = re.search(r"X([-\d.]+)", line, re.IGNORECASE)
94
+ y_match = re.search(r"Y([-\d.]+)", line, re.IGNORECASE)
95
+
96
+ if x_match:
97
+ try:
98
+ x = float(x_match.group(1))
99
+ except ValueError:
100
+ pass
101
+ if y_match:
102
+ try:
103
+ y = float(y_match.group(1))
104
+ except ValueError:
105
+ pass
106
+
107
+ if (x_match or y_match) and pen_down:
108
+ current_path.append((x, y))
109
+
110
+ if len(current_path) > 1:
111
+ paths.append(current_path)
112
+
113
+ # Build SVG
114
+ w = BOUNDS["right"] - BOUNDS["left"]
115
+ h = BOUNDS["top"] - BOUNDS["bottom"]
116
+ padding = 20
117
+
118
+ svg = f'''<svg xmlns="http://www.w3.org/2000/svg"
119
+ viewBox="{BOUNDS["left"] - padding} {-BOUNDS["top"] - padding} {w + 2*padding} {h + 2*padding}"
120
+ style="background: #0a0a0a; width: 100%; height: 500px; border-radius: 8px;">
121
+ <!-- Work area border -->
122
+ <rect x="{BOUNDS["left"]}" y="{-BOUNDS["top"]}" width="{w}" height="{h}"
123
+ fill="none" stroke="#333" stroke-width="2" stroke-dasharray="10,5"/>
124
+ <!-- Center crosshair -->
125
+ <line x1="0" y1="{-BOUNDS["top"]}" x2="0" y2="{-BOUNDS["bottom"]}" stroke="#222" stroke-width="1"/>
126
+ <line x1="{BOUNDS["left"]}" y1="0" x2="{BOUNDS["right"]}" y2="0" stroke="#222" stroke-width="1"/>
127
+ <!-- Grid -->
128
+ <defs>
129
+ <pattern id="grid" width="100" height="100" patternUnits="userSpaceOnUse">
130
+ <path d="M 100 0 L 0 0 0 100" fill="none" stroke="#1a1a1a" stroke-width="0.5"/>
131
+ </pattern>
132
+ </defs>
133
+ <rect x="{BOUNDS["left"]}" y="{-BOUNDS["top"]}" width="{w}" height="{h}" fill="url(#grid)"/>
134
+ '''
135
+
136
+ # Draw paths
137
+ for path in paths:
138
+ if len(path) < 2:
139
+ continue
140
+ # SVG Y is inverted
141
+ d = " ".join(f"{'M' if i == 0 else 'L'}{p[0]:.1f},{-p[1]:.1f}" for i, p in enumerate(path))
142
+ svg += f'<path d="{d}" fill="none" stroke="#10b981" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>'
143
+
144
+ # Stats
145
+ total_points = sum(len(p) for p in paths)
146
+ svg += f'''
147
+ <text x="{BOUNDS["left"] + 10}" y="{-BOUNDS["top"] + 25}" fill="#666" font-family="monospace" font-size="14">
148
+ Paths: {len(paths)} | Points: {total_points}
149
+ </text>
150
+ '''
151
+
152
+ svg += "</svg>"
153
+ return svg
154
+
155
+
156
+ def generate(prompt: str, model_name: str, temperature: float, max_tokens: int):
157
+ """Generate gcode from prompt and return both code and visualization."""
158
  if not prompt or not prompt.strip():
159
+ empty_svg = gcode_to_svg("")
160
+ return "Enter a prompt to generate gcode", empty_svg
161
 
162
  try:
163
  model, tokenizer, device = get_model(model_name)
 
184
 
185
  gcode = validate_gcode(gcode)
186
  line_count = len(gcode.split("\n"))
187
+
188
+ # Generate SVG preview
189
+ svg = gcode_to_svg(gcode)
190
+
191
+ gcode_with_header = f"; dcode output - {line_count} lines\n; Model: {model_name}\n; Machine validated\n\n{gcode}"
192
+ return gcode_with_header, svg
193
+
194
  except Exception as e:
195
+ error_svg = gcode_to_svg("")
196
+ return f"; Error: {e}", error_svg
197
+
198
+
199
+ # Custom CSS
200
+ custom_css = """
201
+ #preview-container {
202
+ background: #0a0a0a;
203
+ border-radius: 8px;
204
+ padding: 0;
205
+ }
206
+ .gradio-container {
207
+ max-width: 1200px !important;
208
+ }
209
+ """
210
+
211
+ with gr.Blocks(css=custom_css, theme=gr.themes.Soft(primary_hue="emerald")) as demo:
212
+ gr.Markdown("""
213
+ # dcode
214
+ **Text → Polargraph Gcode** | Generate machine-compatible gcode from natural language.
215
+
216
+ [GitHub](https://github.com/Twarner491/dcode) | [Model](https://huggingface.co/twarner/dcode-flan-t5-base) | [Dataset](https://huggingface.co/datasets/twarner/dcode-polargraph-gcode)
217
+ """)
218
+
219
+ with gr.Row():
220
+ with gr.Column(scale=1):
221
+ prompt = gr.Textbox(
222
+ label="Prompt",
223
+ placeholder="drawing of a cat, abstract spiral, portrait...",
224
+ lines=2
225
+ )
226
+ model_dropdown = gr.Dropdown(
227
+ choices=list(MODELS.keys()),
228
+ value="flan-t5-base (best)",
229
+ label="Model"
230
+ )
231
+ with gr.Row():
232
+ temperature = gr.Slider(0.1, 1.5, value=0.8, label="Temperature", info="Higher = more creative")
233
+ max_tokens = gr.Slider(256, 2048, value=1024, step=256, label="Max Tokens")
234
+
235
+ generate_btn = gr.Button("Generate", variant="primary", size="lg")
236
+
237
+ gr.Examples(
238
+ examples=[
239
+ ["drawing of a cat"],
240
+ ["abstract spiral pattern"],
241
+ ["simple house with chimney"],
242
+ ["portrait of a woman"],
243
+ ["geometric shapes"],
244
+ ],
245
+ inputs=prompt,
246
+ )
247
+
248
+ with gr.Column(scale=2):
249
+ preview = gr.HTML(
250
+ value=gcode_to_svg(""),
251
+ label="Preview",
252
+ elem_id="preview-container"
253
+ )
254
+
255
+ with gr.Accordion("Gcode Output", open=False):
256
+ gcode_output = gr.Code(label="Gcode", language=None, lines=15)
257
+
258
+ gr.Markdown("""
259
+ ---
260
+ **Machine Bounds**: X: ±420.5mm, Y: ±594.5mm | Pen servo: 40° (down) / 90° (up) | **License**: MIT
261
+ """)
262
+
263
+ generate_btn.click(
264
+ generate,
265
+ [prompt, model_dropdown, temperature, max_tokens],
266
+ [gcode_output, preview]
267
+ )
268
+ prompt.submit(
269
+ generate,
270
+ [prompt, model_dropdown, temperature, max_tokens],
271
+ [gcode_output, preview]
272
+ )
273
 
274
  if __name__ == "__main__":
275
  demo.launch()