tomiconic commited on
Commit
e838a27
·
verified ·
1 Parent(s): 52fd31f

Delete generator.py

Browse files
Files changed (1) hide show
  1. generator.py +0 -409
generator.py DELETED
@@ -1,409 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import math
4
- import tempfile
5
- from dataclasses import dataclass
6
- from pathlib import Path
7
- from typing import Generator
8
-
9
- import numpy as np
10
- import trimesh
11
- from scipy import ndimage
12
- from skimage import measure
13
-
14
- from llm_parser import DEFAULT_LOCAL_MODEL, parse_prompt_with_local_llm
15
- from model_runtime import TARGET_OMNI_MODEL, ensure_target_model_cached
16
- from parser import PromptSpec, parse_prompt
17
-
18
-
19
- @dataclass
20
- class BuildArtifacts:
21
- ply_path: str
22
- glb_path: str
23
- summary: dict
24
-
25
-
26
- SCALE_FACTORS = {
27
- "small": 1.0,
28
- "medium": 1.35,
29
- "large": 1.85,
30
- }
31
-
32
-
33
- def _sample_box_surface(center, size, density: int, label: int) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
34
- cx, cy, cz = center
35
- sx, sy, sz = size
36
- n = max(4, density)
37
- u = np.linspace(-0.5, 0.5, n)
38
- vv = np.linspace(-0.5, 0.5, n)
39
- pts = []
40
- normals = []
41
- labels = []
42
- for ax in (-1, 1):
43
- x = np.full((n, n), cx + ax * sx / 2)
44
- y, z = np.meshgrid(u * sy + cy, vv * sz + cz)
45
- pts.append(np.column_stack([x.ravel(), y.ravel(), z.ravel()]))
46
- normals.append(np.tile([ax, 0, 0], (n * n, 1)))
47
- labels.append(np.full(n * n, label))
48
- for ay in (-1, 1):
49
- y = np.full((n, n), cy + ay * sy / 2)
50
- x, z = np.meshgrid(u * sx + cx, vv * sz + cz)
51
- pts.append(np.column_stack([x.ravel(), y.ravel(), z.ravel()]))
52
- normals.append(np.tile([0, ay, 0], (n * n, 1)))
53
- labels.append(np.full(n * n, label))
54
- for az in (-1, 1):
55
- z = np.full((n, n), cz + az * sz / 2)
56
- x, y = np.meshgrid(u * sx + cx, vv * sy + cy)
57
- pts.append(np.column_stack([x.ravel(), y.ravel(), z.ravel()]))
58
- normals.append(np.tile([0, 0, az], (n * n, 1)))
59
- labels.append(np.full(n * n, label))
60
- return np.vstack(pts), np.vstack(normals), np.concatenate(labels)
61
-
62
-
63
- def _sample_ellipsoid_surface(center, radii, density: int, label: int) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
64
- cx, cy, cz = center
65
- rx, ry, rz = radii
66
- nu = max(16, density * 3)
67
- nv = max(10, density * 2)
68
- u = np.linspace(0, 2 * math.pi, nu, endpoint=False)
69
- v = np.linspace(-math.pi / 2, math.pi / 2, nv)
70
- uu, vv = np.meshgrid(u, v)
71
- x = cx + rx * np.cos(vv) * np.cos(uu)
72
- y = cy + ry * np.cos(vv) * np.sin(uu)
73
- z = cz + rz * np.sin(vv)
74
- pts = np.column_stack([x.ravel(), y.ravel(), z.ravel()])
75
- normals = np.column_stack([
76
- (x - cx).ravel() / max(rx, 1e-6),
77
- (y - cy).ravel() / max(ry, 1e-6),
78
- (z - cz).ravel() / max(rz, 1e-6),
79
- ])
80
- normals /= np.linalg.norm(normals, axis=1, keepdims=True) + 1e-8
81
- labels = np.full(len(pts), label)
82
- return pts, normals, labels
83
-
84
-
85
- def _sample_cylinder_surface(center, radius, length, axis: str, density: int, label: int) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
86
- cx, cy, cz = center
87
- nt = max(18, density * 4)
88
- nl = max(6, density)
89
- theta = np.linspace(0, 2 * math.pi, nt, endpoint=False)
90
- line = np.linspace(-length / 2, length / 2, nl)
91
- tt, ll = np.meshgrid(theta, line)
92
- if axis == "x":
93
- x = cx + ll
94
- y = cy + radius * np.cos(tt)
95
- z = cz + radius * np.sin(tt)
96
- normals = np.column_stack([np.zeros(x.size), np.cos(tt).ravel(), np.sin(tt).ravel()])
97
- elif axis == "y":
98
- x = cx + radius * np.cos(tt)
99
- y = cy + ll
100
- z = cz + radius * np.sin(tt)
101
- normals = np.column_stack([np.cos(tt).ravel(), np.zeros(x.size), np.sin(tt).ravel()])
102
- else:
103
- x = cx + radius * np.cos(tt)
104
- y = cy + radius * np.sin(tt)
105
- z = cz + ll
106
- normals = np.column_stack([np.cos(tt).ravel(), np.sin(tt).ravel(), np.zeros(x.size)])
107
- pts = np.column_stack([x.ravel(), y.ravel(), z.ravel()])
108
- labels = np.full(len(pts), label)
109
- return pts, normals, labels
110
-
111
-
112
- def export_point_cloud_as_ply(points: np.ndarray, labels: np.ndarray, path: str) -> str:
113
- colors = np.array([
114
- [170, 170, 180],
115
- [120, 180, 255],
116
- [255, 190, 120],
117
- [180, 180, 255],
118
- [255, 120, 120],
119
- [200, 255, 180],
120
- [255, 255, 180],
121
- ], dtype=np.uint8)
122
- c = colors[labels % len(colors)]
123
- pc = trimesh.points.PointCloud(vertices=points, colors=c)
124
- pc.export(path)
125
- return path
126
-
127
-
128
- def export_mesh_as_glb(mesh: trimesh.Trimesh, path: str) -> str:
129
- mesh.visual.vertex_colors = np.tile(np.array([[185, 190, 200, 255]], dtype=np.uint8), (len(mesh.vertices), 1))
130
- mesh.export(path)
131
- return path
132
-
133
-
134
- def _resolve_spec(prompt: str, parser_mode: str, model_id: str | None = None) -> tuple[PromptSpec, str]:
135
- parser_mode = (parser_mode or "heuristic").strip().lower()
136
- if parser_mode.startswith("local"):
137
- spec = parse_prompt_with_local_llm(prompt, model_id=model_id or DEFAULT_LOCAL_MODEL)
138
- return spec, f"local_llm:{model_id or DEFAULT_LOCAL_MODEL}"
139
- return parse_prompt(prompt), "heuristic"
140
-
141
-
142
- def _iter_part_specs(spec: PromptSpec, detail: int):
143
- scale = SCALE_FACTORS[spec.scale]
144
- density = max(6, detail)
145
-
146
- hull_len = 2.8 * scale
147
- hull_w = 1.2 * scale
148
- hull_h = 0.8 * scale
149
-
150
- if spec.hull_style == "rounded":
151
- yield "Hull", *_sample_ellipsoid_surface((0.0, 0.0, 0.0), (hull_len / 2, hull_w / 2, hull_h / 2), density, 0)
152
- elif spec.hull_style == "sleek":
153
- p1, n1, l1 = _sample_ellipsoid_surface((0.12 * scale, 0.0, 0.0), (hull_len / 2.3, hull_w / 2.8, hull_h / 2.6), density, 0)
154
- p2, n2, l2 = _sample_box_surface((-0.15 * scale, 0.0, -0.02 * scale), (hull_len * 0.52, hull_w * 0.5, hull_h * 0.55), max(4, density // 2), 0)
155
- yield "Hull", np.vstack([p1, p2]), np.vstack([n1, n2]), np.concatenate([l1, l2])
156
- else:
157
- yield "Hull", *_sample_box_surface((0.0, 0.0, 0.0), (hull_len, hull_w, hull_h), density, 0)
158
-
159
- cockpit_center = (hull_len / 2 - hull_len * spec.cockpit_ratio * 0.8, 0.0, hull_h * 0.14)
160
- yield "Cockpit", *_sample_ellipsoid_surface(cockpit_center, (hull_len * spec.cockpit_ratio, hull_w * 0.22, hull_h * 0.24), max(4, density // 2), 1)
161
-
162
- if spec.cargo_ratio > 0.16:
163
- cargo_center = (-hull_len * 0.18, 0.0, -hull_h * 0.06)
164
- cargo_size = (hull_len * spec.cargo_ratio, hull_w * 0.76, hull_h * 0.6)
165
- yield "Cargo bay", *_sample_box_surface(cargo_center, cargo_size, max(4, density // 2), 2)
166
-
167
- if spec.wing_span > 0:
168
- wing_length = hull_len * 0.34
169
- wing_width = hull_w * 0.18
170
- wing_height = hull_h * 0.08
171
- yoff = hull_w * 0.45 + wing_width * 0.6
172
- wing_parts = []
173
- wing_normals = []
174
- wing_labels = []
175
- for side in (-1, 1):
176
- wc = (-0.1 * scale, side * yoff, -0.04 * scale)
177
- pp, pn, pl = _sample_box_surface(wc, (wing_length, wing_width, wing_height), max(6, density // 3), 3)
178
- wing_parts.append(pp)
179
- wing_normals.append(pn)
180
- wing_labels.append(pl)
181
- yield "Wings", np.vstack(wing_parts), np.vstack(wing_normals), np.concatenate(wing_labels)
182
-
183
- engine_radius = 0.14 * scale if spec.object_type != "fighter" else 0.1 * scale
184
- engine_length = 0.48 * scale
185
- engine_y_positions = np.linspace(-hull_w * 0.32, hull_w * 0.32, spec.engine_count)
186
- engine_parts = []
187
- engine_normals = []
188
- engine_labels = []
189
- for ypos in engine_y_positions:
190
- ec = (-hull_len / 2 + engine_length * 0.3, ypos, 0.0)
191
- pp, pn, pl = _sample_cylinder_surface(ec, engine_radius, engine_length, "x", max(6, density // 3), 4)
192
- engine_parts.append(pp)
193
- engine_normals.append(pn)
194
- engine_labels.append(pl)
195
- yield "Engines", np.vstack(engine_parts), np.vstack(engine_normals), np.concatenate(engine_labels)
196
-
197
- if spec.fin_height > 0:
198
- fin_center = (-hull_len * 0.25, 0.0, hull_h * 0.42)
199
- fin_size = (hull_len * 0.18, hull_w * 0.1, hull_h * max(spec.fin_height, 0.12))
200
- yield "Fin", *_sample_box_surface(fin_center, fin_size, max(6, density // 3), 5)
201
-
202
- if spec.landing_gear:
203
- gear_x = np.array([-hull_len * 0.18, hull_len * 0.12])
204
- gear_y = np.array([-hull_w * 0.28, hull_w * 0.28])
205
- gear_parts = []
206
- gear_normals = []
207
- gear_labels = []
208
- for gx in gear_x:
209
- for gy in gear_y:
210
- gc = (gx, gy, -hull_h * 0.45)
211
- pp, pn, pl = _sample_cylinder_surface(gc, 0.04 * scale, 0.22 * scale, "z", max(5, density // 5), 6)
212
- gear_parts.append(pp)
213
- gear_normals.append(pn)
214
- gear_labels.append(pl)
215
- yield "Landing gear", np.vstack(gear_parts), np.vstack(gear_normals), np.concatenate(gear_labels)
216
-
217
-
218
- def iter_blueprint_session(
219
- prompt: str,
220
- detail: int = 24,
221
- parser_mode: str = "heuristic",
222
- model_id: str | None = None,
223
- ) -> Generator[dict, None, dict]:
224
- prompt = (prompt or "").strip()
225
- if not prompt:
226
- raise ValueError("Enter a prompt first.")
227
-
228
- out_dir = Path(tempfile.mkdtemp(prefix="particle_blueprint_session_"))
229
- yield {"status": "Parsing prompt and planning shape…", "stage_index": 0, "stage_count": 1, "session_dir": str(out_dir)}
230
-
231
- spec, parser_backend = _resolve_spec(prompt, parser_mode=parser_mode, model_id=model_id)
232
- stages = list(_iter_part_specs(spec, detail=detail))
233
-
234
- all_points = []
235
- all_normals = []
236
- all_labels = []
237
-
238
- for idx, (stage_name, points, normals, labels) in enumerate(stages, start=1):
239
- if spec.asymmetry > 0 and stage_name in {"Hull", "Cockpit", "Cargo bay"}:
240
- mask = points[:, 1] > 0
241
- points = points.copy()
242
- points[mask, 2] += spec.asymmetry * np.sin(points[mask, 0] * 2.0)
243
-
244
- all_points.append(points)
245
- all_normals.append(normals)
246
- all_labels.append(labels)
247
-
248
- merged_points = np.vstack(all_points).astype(np.float32)
249
- merged_normals = np.vstack(all_normals).astype(np.float32)
250
- merged_labels = np.concatenate(all_labels).astype(np.int32)
251
-
252
- preview_path = str(out_dir / f"blueprint_stage_{idx:02d}.ply")
253
- export_point_cloud_as_ply(merged_points, merged_labels, preview_path)
254
-
255
- summary = {
256
- "prompt": prompt,
257
- "parser_backend": parser_backend,
258
- "spec": spec.to_dict(),
259
- "stage": stage_name,
260
- "stage_index": idx,
261
- "stage_count": len(stages),
262
- "point_count": int(len(merged_points)),
263
- }
264
- yield {
265
- "status": f"{stage_name} added ({idx}/{len(stages)})",
266
- "blueprint_path": preview_path,
267
- "summary": summary,
268
- "stage_index": idx,
269
- "stage_count": len(stages),
270
- "session_dir": str(out_dir),
271
- }
272
-
273
- final_points = np.vstack(all_points).astype(np.float32)
274
- final_normals = np.vstack(all_normals).astype(np.float32)
275
- final_labels = np.concatenate(all_labels).astype(np.int32)
276
-
277
- npz_path = str(out_dir / "blueprint_data.npz")
278
- np.savez_compressed(npz_path, points=final_points, normals=final_normals, labels=final_labels)
279
-
280
- final_ply = str(out_dir / "blueprint_final.ply")
281
- export_point_cloud_as_ply(final_points, final_labels, final_ply)
282
-
283
- state = {
284
- "prompt": prompt,
285
- "parser_backend": parser_backend,
286
- "spec": spec.to_dict(),
287
- "point_count": int(len(final_points)),
288
- "session_dir": str(out_dir),
289
- "npz_path": npz_path,
290
- "blueprint_path": final_ply,
291
- "target_model": TARGET_OMNI_MODEL,
292
- }
293
- yield {
294
- "status": "Blueprint ready. Inspect it, then run mesh generation when happy.",
295
- "blueprint_path": final_ply,
296
- "summary": {
297
- **state,
298
- "stage": "complete",
299
- },
300
- "stage_index": len(stages),
301
- "stage_count": len(stages),
302
- "state": state,
303
- "session_dir": str(out_dir),
304
- }
305
- return state
306
-
307
-
308
- def points_to_mesh(points: np.ndarray, pitch: float = 0.08, padding: int = 5, sigma: float = 1.2, level: float = 0.11) -> trimesh.Trimesh:
309
- mins = points.min(axis=0) - padding * pitch
310
- maxs = points.max(axis=0) + padding * pitch
311
- dims = np.ceil((maxs - mins) / pitch).astype(int) + 1
312
- dims = np.clip(dims, 24, 192)
313
-
314
- grid = np.zeros(tuple(dims.tolist()), dtype=np.float32)
315
- coords = ((points - mins) / pitch).astype(int)
316
- coords = np.clip(coords, 0, dims - 1)
317
- np.add.at(grid, (coords[:, 0], coords[:, 1], coords[:, 2]), 1.0)
318
-
319
- grid = ndimage.gaussian_filter(grid, sigma=sigma)
320
- verts, faces, normals, _ = measure.marching_cubes(grid, level=level)
321
- verts = verts * pitch + mins
322
-
323
- mesh = trimesh.Trimesh(vertices=verts, faces=faces, vertex_normals=normals, process=True)
324
- mesh.update_faces(mesh.nondegenerate_faces())
325
- mesh.update_faces(mesh.unique_faces())
326
- mesh.remove_unreferenced_vertices()
327
- try:
328
- mesh.fill_holes()
329
- except Exception:
330
- pass
331
- try:
332
- trimesh.smoothing.filter_humphrey(mesh, iterations=2)
333
- except Exception:
334
- pass
335
- return mesh
336
-
337
-
338
- def iter_meshify_session(
339
- state: dict,
340
- voxel_pitch: float = 0.08,
341
- use_target_model_cache: bool = True,
342
- ) -> Generator[dict, None, dict]:
343
- if not state or not state.get("npz_path"):
344
- raise ValueError("Generate a blueprint first.")
345
-
346
- data = np.load(state["npz_path"])
347
- points = data["points"].astype(np.float32)
348
- labels = data["labels"].astype(np.int32)
349
- session_dir = Path(state["session_dir"])
350
-
351
- model_note = None
352
- if use_target_model_cache:
353
- yield {"status": f"Preparing target model cache for {TARGET_OMNI_MODEL}…"}
354
- model_cache = ensure_target_model_cached(TARGET_OMNI_MODEL)
355
- model_note = model_cache["message"]
356
- yield {"status": model_note}
357
-
358
- yield {"status": "Converting blueprint into a watertight mesh…"}
359
- mesh = points_to_mesh(points, pitch=voxel_pitch)
360
-
361
- yield {"status": "Exporting GLB…"}
362
- glb_path = str(session_dir / "mesh_final.glb")
363
- export_mesh_as_glb(mesh, glb_path)
364
-
365
- summary = {
366
- **state,
367
- "mesh_backend": "local_voxel_mesher",
368
- "target_model_cached": bool(use_target_model_cache),
369
- "target_model": TARGET_OMNI_MODEL,
370
- "target_model_note": model_note,
371
- "vertex_count": int(len(mesh.vertices)),
372
- "face_count": int(len(mesh.faces)),
373
- "bounds": mesh.bounds.round(3).tolist(),
374
- "voxel_pitch": voxel_pitch,
375
- "mesh_path": glb_path,
376
- }
377
- yield {
378
- "status": "Mesh ready.",
379
- "mesh_path": glb_path,
380
- "summary": summary,
381
- "mesh_file": glb_path,
382
- }
383
- return summary
384
-
385
-
386
- # Backward-compatible helper for older single-click flow.
387
- def run_pipeline(
388
- prompt: str,
389
- detail: int = 24,
390
- voxel_pitch: float = 0.08,
391
- parser_mode: str = "heuristic",
392
- model_id: str | None = None,
393
- ) -> BuildArtifacts:
394
- final_state = None
395
- final_summary = None
396
- blueprint_path = None
397
- for update in iter_blueprint_session(prompt, detail=detail, parser_mode=parser_mode, model_id=model_id):
398
- blueprint_path = update.get("blueprint_path", blueprint_path)
399
- final_state = update.get("state", final_state)
400
- final_summary = update.get("summary", final_summary)
401
- mesh_summary = None
402
- mesh_path = None
403
- if final_state is None:
404
- raise RuntimeError("Blueprint generation failed.")
405
- for update in iter_meshify_session(final_state, voxel_pitch=voxel_pitch, use_target_model_cache=False):
406
- mesh_path = update.get("mesh_path", mesh_path)
407
- mesh_summary = update.get("summary", mesh_summary)
408
- summary = mesh_summary or final_summary or {}
409
- return BuildArtifacts(ply_path=blueprint_path or "", glb_path=mesh_path or "", summary=summary)