Reverb commited on
Commit
87d0a58
·
verified ·
1 Parent(s): 942c27d

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -449
app.py DELETED
@@ -1,449 +0,0 @@
1
- """
2
- Open3DForge — Image-to-game-ready 3D asset pipeline
3
- ====================================================
4
-
5
- Milestone 1: Foundation
6
- -----------------------
7
- This is the scaffold. It validates that:
8
- - The HF Space builds with the right Gradio + ZeroGPU configuration
9
- - `@spaces.GPU` allocates and releases an H200 successfully
10
- - `gr.Model3D` renders GLB files inline
11
- - The 5-tab UI structure is sound
12
- - Workspace folder management works
13
- - Quota tracking persists across page reloads
14
-
15
- Subsequent milestones fill in the actual pipeline stages.
16
-
17
- Deployed exclusively on HF Spaces ZeroGPU — no local execution path.
18
- """
19
-
20
- from __future__ import annotations
21
-
22
- import time
23
- from pathlib import Path
24
-
25
- import gradio as gr
26
- import spaces # HF ZeroGPU
27
-
28
- from src import quota, ui_helpers, workspace
29
-
30
- # ---------------------------------------------------------------------------
31
- # ZeroGPU test function — proves that GPU allocation works.
32
- # Replaced in Milestone 2 with the actual TRELLIS.2 generator.
33
- # ---------------------------------------------------------------------------
34
-
35
- @spaces.GPU(duration=15)
36
- def zerogpu_smoke_test() -> str:
37
- """Allocate a GPU, run a trivial torch op, release. Reports timing."""
38
- start = time.time()
39
- try:
40
- import torch
41
- if not torch.cuda.is_available():
42
- return "❌ GPU not available inside @spaces.GPU. Something is wrong."
43
-
44
- device = torch.device("cuda")
45
- # Trivial GPU work
46
- a = torch.randn(1024, 1024, device=device)
47
- b = torch.randn(1024, 1024, device=device)
48
- c = a @ b
49
- torch.cuda.synchronize()
50
- result_sum = float(c.sum().item())
51
-
52
- gpu_name = torch.cuda.get_device_name(0)
53
- vram_total = torch.cuda.get_device_properties(0).total_memory / 1e9
54
- elapsed = time.time() - start
55
-
56
- quota.record_usage("smoke_test", elapsed)
57
-
58
- return (
59
- f"✅ **GPU allocation successful**\n\n"
60
- f"- Device: `{gpu_name}`\n"
61
- f"- VRAM: `{vram_total:.1f} GB`\n"
62
- f"- Test op (1024×1024 matmul): `{elapsed:.2f}s`\n"
63
- f"- Result sum: `{result_sum:.2f}` (sanity check, non-zero = ✓)\n\n"
64
- f"ZeroGPU integration is working. Ready for Milestone 2."
65
- )
66
- except Exception as e:
67
- elapsed = time.time() - start
68
- return f"❌ GPU test failed after {elapsed:.2f}s:\n```\n{type(e).__name__}: {e}\n```"
69
-
70
-
71
- # ---------------------------------------------------------------------------
72
- # Stage stubs — return placeholder messages until milestones implement them.
73
- # ---------------------------------------------------------------------------
74
-
75
- def _stub(stage: str) -> str:
76
- return (
77
- f"🚧 **{stage}** — not implemented yet.\n\n"
78
- f"This stub will be replaced in a future milestone. "
79
- f"See `PLAN.md` for the full pipeline spec."
80
- )
81
-
82
-
83
- def stub_generate(*_args, **_kwargs):
84
- return _stub("Stage 1: Generation")
85
-
86
-
87
- def stub_post_process(*_args, **_kwargs):
88
- return _stub("Stage 2: Post-Processing")
89
-
90
-
91
- def stub_auto_rig(*_args, **_kwargs):
92
- return _stub("Stage 3: Auto-Rigging")
93
-
94
-
95
- def stub_export(*_args, **_kwargs):
96
- return _stub("Stage 4: Export"), None # message, file
97
-
98
-
99
- # ---------------------------------------------------------------------------
100
- # Presets tab logic (this one is real even in Milestone 1)
101
- # ---------------------------------------------------------------------------
102
-
103
- def ui_refresh_presets() -> gr.Dropdown:
104
- return gr.Dropdown(choices=workspace.list_presets(), label="Saved presets")
105
-
106
-
107
- def ui_save_preset(name: str) -> tuple[str, gr.Dropdown]:
108
- if not name or not name.strip():
109
- return "❌ Preset name cannot be empty.", ui_refresh_presets()
110
- # Placeholder config — Milestone 9 wires in real settings from each tab
111
- config = {
112
- "name": name,
113
- "version": 1,
114
- "created_at": time.time(),
115
- "note": "Placeholder — real preset data wired in Milestone 9",
116
- }
117
- workspace.save_preset(name, config)
118
- return f"✅ Saved preset: `{name}`", ui_refresh_presets()
119
-
120
-
121
- def ui_delete_preset(name: str) -> tuple[str, gr.Dropdown]:
122
- if not name:
123
- return "❌ Select a preset to delete.", ui_refresh_presets()
124
- if workspace.delete_preset(name):
125
- return f"🗑️ Deleted preset: `{name}`", ui_refresh_presets()
126
- return f"❌ Preset not found: `{name}`", ui_refresh_presets()
127
-
128
-
129
- # ---------------------------------------------------------------------------
130
- # Layout
131
- # ---------------------------------------------------------------------------
132
-
133
- CUSTOM_CSS = """
134
- .status-bar {
135
- font-size: 0.85em;
136
- color: #888;
137
- padding: 8px 12px;
138
- border-top: 1px solid #333;
139
- margin-top: 12px;
140
- }
141
- .asset-summary {
142
- font-size: 0.9em;
143
- background: rgba(255,255,255,0.03);
144
- padding: 12px;
145
- border-radius: 6px;
146
- border: 1px solid rgba(255,255,255,0.08);
147
- }
148
- """
149
-
150
-
151
- def build_ui() -> gr.Blocks:
152
- with gr.Blocks(
153
- title="Open3DForge",
154
- ) as demo:
155
-
156
- # --- Header --------------------------------------------------------
157
- gr.Markdown(
158
- "# 🛠️ Open3DForge\n"
159
- "*Personal image-to-game-ready 3D asset pipeline · UE5-first · "
160
- "Built on HF ZeroGPU*"
161
- )
162
-
163
- # --- Tabs ----------------------------------------------------------
164
- with gr.Tabs() as tabs:
165
-
166
- # ============ Tab 1: Generate =================================
167
- with gr.Tab("1. Generate", id=1):
168
- gr.Markdown(
169
- "### Stage 1 — Image to 3D\n"
170
- "Upload 1–4 reference images. Multi-view dramatically "
171
- "improves quality for characters (front / 3-quarter / side / back)."
172
- )
173
- with gr.Row():
174
- with gr.Column(scale=1):
175
- gen_images = gr.File(
176
- label="Reference images (1–4)",
177
- file_count="multiple",
178
- file_types=["image"],
179
- )
180
- gen_model = gr.Radio(
181
- choices=[
182
- "TRELLIS.2 (Hard Surface)",
183
- "Hunyuan3D-2 (Organic / Characters)",
184
- ],
185
- value="TRELLIS.2 (Hard Surface)",
186
- label="Generation model",
187
- )
188
- gen_quality = gr.Radio(
189
- choices=["Fast (~30s)", "Balanced (~60s)", "Hero (~90s)"],
190
- value="Balanced (~60s)",
191
- label="Quality preset",
192
- )
193
- with gr.Accordion("Advanced", open=False):
194
- gen_seed = gr.Number(value=42, label="Seed", precision=0)
195
- gen_steps = gr.Slider(
196
- 20, 50, value=35, step=5,
197
- label="Inference steps",
198
- )
199
- gen_octree = gr.Dropdown(
200
- choices=[128, 192, 256], value=192,
201
- label="Octree resolution",
202
- )
203
- gen_tex_size = gr.Dropdown(
204
- choices=[1024, 2048, 4096], value=2048,
205
- label="Texture size",
206
- )
207
- gen_symmetry = gr.Radio(
208
- choices=["off", "bilateral", "radial"],
209
- value="off",
210
- label="Symmetry hint",
211
- )
212
- gen_btn = gr.Button("Generate", variant="primary")
213
- with gr.Column(scale=1):
214
- gen_status = gr.Markdown("*Awaiting input.*")
215
- gen_btn.click(
216
- fn=stub_generate,
217
- inputs=[gen_images, gen_model, gen_quality, gen_seed,
218
- gen_steps, gen_octree, gen_tex_size, gen_symmetry],
219
- outputs=gen_status,
220
- )
221
-
222
- # ============ Tab 2: Post-Process =============================
223
- with gr.Tab("2. Post-Process", id=2):
224
- gr.Markdown(
225
- "### Stage 2 — Mesh cleanup, UV unwrap, texture baking\n"
226
- "Toggle steps on/off. Decimation has a live preview, the "
227
- "rest run on confirm."
228
- )
229
- with gr.Row():
230
- with gr.Column(scale=1):
231
- pp_repair = gr.Checkbox(value=True, label="Mesh repair (pymeshfix)")
232
- pp_cleanup = gr.Checkbox(value=True, label="Geometry cleanup (PyMeshLab)")
233
- pp_decimate = gr.Checkbox(value=True, label="Decimation")
234
- pp_target_faces = gr.Slider(
235
- 1000, 200000, value=25000, step=1000,
236
- label="Target faces",
237
- )
238
- pp_symmetry = gr.Checkbox(value=False, label="Enforce bilateral symmetry")
239
- pp_unwrap = gr.Checkbox(value=True, label="UV unwrap (xatlas)")
240
- pp_normal_bake = gr.Checkbox(value=True, label="Normal bake (nvdiffrast)")
241
- pp_normal_format = gr.Radio(
242
- choices=["DirectX (UE5)", "OpenGL (Unity/Godot)"],
243
- value="DirectX (UE5)",
244
- label="Normal format",
245
- )
246
- pp_albedo_bake = gr.Checkbox(value=True, label="Albedo bake")
247
- pp_material_bake = gr.Checkbox(value=True, label="Material bake (TRELLIS.2 attrs)")
248
- pp_ao = gr.Checkbox(value=True, label="AO bake")
249
- pp_ao_quality = gr.Radio(
250
- choices=["Fast", "Standard", "High"],
251
- value="Standard",
252
- label="AO quality",
253
- )
254
- pp_inpaint = gr.Checkbox(value=False, label="SDXL inpaint hidden UVs (~30s GPU)")
255
- pp_lods = gr.Checkbox(value=True, label="Generate LODs (LOD0/1/2)")
256
- pp_collision = gr.Checkbox(value=True, label="Collision mesh (CoACD)")
257
- pp_pivot = gr.Radio(
258
- choices=["bottom_center", "geometric_center", "custom"],
259
- value="bottom_center",
260
- label="Pivot point",
261
- )
262
- pp_scale_m = gr.Number(value=1.8, label="Real-world height (meters)")
263
- pp_btn = gr.Button("Run Post-Processing", variant="primary")
264
- with gr.Column(scale=1):
265
- pp_status = gr.Markdown("*No asset to process. Generate one first.*")
266
- pp_btn.click(
267
- fn=stub_post_process,
268
- inputs=[pp_repair, pp_cleanup, pp_decimate, pp_target_faces,
269
- pp_symmetry, pp_unwrap, pp_normal_bake, pp_normal_format,
270
- pp_albedo_bake, pp_material_bake, pp_ao, pp_ao_quality,
271
- pp_inpaint, pp_lods, pp_collision, pp_pivot, pp_scale_m],
272
- outputs=pp_status,
273
- )
274
-
275
- # ============ Tab 3: Auto-Rig =================================
276
- with gr.Tab("3. Auto-Rig", id=3):
277
- gr.Markdown(
278
- "### Stage 3 — Auto-rigging (optional)\n"
279
- "Uses UniRig (VAST-AI). For characters and creatures. "
280
- "After rigging, drop the FBX into [Mixamo](https://mixamo.com) "
281
- "for free animation presets."
282
- )
283
- with gr.Row():
284
- with gr.Column(scale=1):
285
- rig_type = gr.Dropdown(
286
- choices=["Humanoid", "Quadruped", "Bird", "Insect", "Custom"],
287
- value="Humanoid",
288
- label="Character type",
289
- )
290
- rig_seed = gr.Number(value=0, label="Skeleton seed", precision=0)
291
- rig_spring = gr.Checkbox(value=False, label="Spring bones (hair/cloth/tail)")
292
- rig_format = gr.Radio(
293
- choices=["FBX (UE5 recommended)", "GLB"],
294
- value="FBX (UE5 recommended)",
295
- label="Export format",
296
- )
297
- rig_btn = gr.Button("Auto-Rig", variant="primary")
298
- with gr.Column(scale=1):
299
- rig_status = gr.Markdown("*Process an asset in Stage 2 first.*")
300
- rig_btn.click(
301
- fn=stub_auto_rig,
302
- inputs=[rig_type, rig_seed, rig_spring, rig_format],
303
- outputs=rig_status,
304
- )
305
-
306
- # ============ Tab 4: Export ===================================
307
- with gr.Tab("4. Export", id=4):
308
- gr.Markdown(
309
- "### Stage 4 — Engine-ready export\n"
310
- "UE5 default: FBX with DirectX normals + ORM-packed textures."
311
- )
312
- with gr.Row():
313
- with gr.Column(scale=1):
314
- ex_engine = gr.Dropdown(
315
- choices=["UE5", "Unity (HDRP)", "Godot 4", "Blender", "Web (Three.js)"],
316
- value="UE5",
317
- label="Target engine",
318
- )
319
- ex_name = gr.Textbox(value="Asset_01", label="Asset name")
320
- ex_type = gr.Radio(
321
- choices=["Character (SK_)", "Prop (SM_)", "Environment (SM_)"],
322
- value="Prop (SM_)",
323
- label="Asset type",
324
- )
325
- ex_include_lods = gr.Checkbox(value=True, label="Include LODs")
326
- ex_include_collision = gr.Checkbox(value=True, label="Include collision mesh")
327
- ex_btn = gr.Button("Export", variant="primary")
328
- with gr.Column(scale=1):
329
- ex_status = gr.Markdown("*Nothing to export yet.*")
330
- ex_file = gr.File(label="Download", visible=True)
331
- ex_btn.click(
332
- fn=stub_export,
333
- inputs=[ex_engine, ex_name, ex_type, ex_include_lods, ex_include_collision],
334
- outputs=[ex_status, ex_file],
335
- )
336
-
337
- # ============ Tab 5: Presets ==================================
338
- with gr.Tab("5. Presets", id=5):
339
- gr.Markdown(
340
- "### Saved configurations\n"
341
- "Save parameter sets for asset types you create repeatedly "
342
- "(e.g., \"character_UE5_hero\", \"prop_UE5_standard\"). "
343
- "Wired up properly in Milestone 9."
344
- )
345
- with gr.Row():
346
- with gr.Column():
347
- pr_list = gr.Dropdown(
348
- choices=workspace.list_presets(),
349
- label="Saved presets",
350
- )
351
- pr_refresh = gr.Button("Refresh list", size="sm")
352
- with gr.Row():
353
- pr_name = gr.Textbox(label="New preset name", scale=2)
354
- pr_save = gr.Button("Save current settings", variant="primary", scale=1)
355
- pr_delete = gr.Button("Delete selected", variant="stop")
356
- pr_status = gr.Markdown()
357
- pr_refresh.click(fn=ui_refresh_presets, outputs=pr_list)
358
- pr_save.click(fn=ui_save_preset, inputs=pr_name, outputs=[pr_status, pr_list])
359
- pr_delete.click(fn=ui_delete_preset, inputs=pr_list, outputs=[pr_status, pr_list])
360
-
361
- # ============ Tab 6: Diagnostics (hidden in prod, useful now) =
362
- with gr.Tab("Diagnostics", id=99):
363
- gr.Markdown(
364
- "### Milestone 1 — Foundation check\n"
365
- "Verify the Space environment is working correctly before "
366
- "building out the pipeline."
367
- )
368
- with gr.Row():
369
- with gr.Column():
370
- diag_btn = gr.Button("🧪 Run GPU smoke test", variant="primary")
371
- diag_out = gr.Markdown()
372
- with gr.Column():
373
- gr.Markdown("**Workspace state:**")
374
- diag_state = gr.JSON(value=workspace.get_state().to_dict())
375
- diag_refresh = gr.Button("Refresh state", size="sm")
376
- diag_btn.click(fn=zerogpu_smoke_test, outputs=diag_out)
377
- diag_refresh.click(
378
- fn=lambda: workspace.get_state().to_dict(),
379
- outputs=diag_state,
380
- )
381
-
382
- # --- Persistent right-side viewer + asset summary ------------------
383
- gr.Markdown("---")
384
- with gr.Row():
385
- with gr.Column(scale=2):
386
- viewer = gr.Model3D(
387
- label="3D viewer",
388
- value=ui_helpers.get_viewer_model_path(),
389
- clear_color=[0.1, 0.1, 0.12, 1.0],
390
- height=500,
391
- )
392
- with gr.Column(scale=1):
393
- summary = gr.Markdown(
394
- value=ui_helpers.get_asset_summary(),
395
- elem_classes=["asset-summary"],
396
- )
397
- refresh_summary = gr.Button("🔄 Refresh viewer", size="sm")
398
- refresh_summary.click(
399
- fn=lambda: (
400
- ui_helpers.get_viewer_model_path(),
401
- ui_helpers.get_asset_summary(),
402
- ),
403
- outputs=[viewer, summary],
404
- )
405
-
406
- # --- Global status bar --------------------------------------------
407
- status_bar = gr.Markdown(
408
- value=ui_helpers.get_status_bar(),
409
- elem_classes=["status-bar"],
410
- )
411
-
412
- # --- Global refresh: every pipeline action updates the viewer, summary,
413
- # and status bar. We chain .then() onto each button so the refresh runs
414
- # only after the action completes (and even if it fails — Gradio still
415
- # fires .then() after exceptions inside the main handler).
416
- def _global_refresh():
417
- return (
418
- ui_helpers.get_viewer_model_path(),
419
- ui_helpers.get_asset_summary(),
420
- ui_helpers.get_status_bar(),
421
- )
422
-
423
- for trigger_btn in (gen_btn, pp_btn, rig_btn, ex_btn,
424
- diag_btn, diag_refresh, refresh_summary):
425
- trigger_btn.click(
426
- fn=_global_refresh,
427
- outputs=[viewer, summary, status_bar],
428
- )
429
-
430
- return demo
431
-
432
-
433
- # ---------------------------------------------------------------------------
434
- # Entrypoint
435
- # ---------------------------------------------------------------------------
436
- # On HF Spaces, app.py is executed directly. We construct the demo and call
437
- # .launch() at module level. The HF Space runtime handles all networking;
438
- # we just need to bind to 0.0.0.0:7860.
439
-
440
- workspace.ensure_dirs()
441
- demo = build_ui()
442
- demo.queue(default_concurrency_limit=1).launch(
443
- server_name="0.0.0.0",
444
- server_port=7860,
445
- show_error=True,
446
- show_api=False,
447
- theme=gr.themes.Soft(primary_hue="indigo", neutral_hue="slate"),
448
- css=CUSTOM_CSS,
449
- )