theYiran commited on
Commit
ae6e06d
·
verified ·
1 Parent(s): 63a16cb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +76 -330
app.py CHANGED
@@ -1,13 +1,23 @@
1
  import os
2
  import sys
3
- import subprocess # <--- 确保这行在这里!
4
  import importlib
5
  import site
6
  import time
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
- # --- 🧪 1. 内存级伪造 diso (必须在任何业务 import 之前) ---
9
  def mock_diso():
10
- from types import ModuleType
11
  print("🧪 Creating emergency mock for diso...")
12
  diso = ModuleType("diso")
13
  class FakeDiffDMC:
@@ -17,146 +27,51 @@ def mock_diso():
17
  sys.modules["diso"] = diso
18
  sys.modules["diso._C"] = ModuleType("diso._C")
19
  sys.modules["diso.diso_native"] = ModuleType("diso.diso_native")
20
- print("✅ diso has been mocked successfully!")
21
 
22
  mock_diso()
23
 
24
- # --- 🚀 2. 极速环境安装 (已经成功的 scatter/sparse) ---
25
- def install_essential_packages():
26
- print("📦 Checking core dependencies...")
27
- # 确保基础环境正确
28
- subprocess.run([sys.executable, "-m", "pip", "install", "ninja", "setuptools", "wheel", "-q"])
29
-
30
- # 极速安装 PyG 扩展
31
- subprocess.run([
32
- sys.executable, "-m", "pip", "install",
33
- "torch-scatter", "torch-sparse", "torch-cluster",
34
- "-f", "https://data.pyg.org/whl/torch-2.4.0+cu121.html",
35
- "--no-cache-dir"
36
- ])
37
-
38
- # 安装剩下的渲染工具
 
39
  subprocess.run([
40
  sys.executable, "-m", "pip", "install",
41
- "pyrender", "pyopengl==3.1.0", "pyyaml", "trimesh", "accelerate", "-q"
 
42
  ])
43
 
44
  importlib.invalidate_caches()
45
  site.main()
46
- print("🎉 Environment Installation Phase Finished.")
47
 
48
- install_essential_packages()
 
 
49
 
50
- # --- 3. 正式导入业务逻辑 (现在开始这几百行代码就不会报错了) ---
 
 
51
  import spaces
52
  import gradio as gr
53
  import numpy as np
54
  import torch
55
- import uuid
56
- import shutil
57
  from huggingface_hub import snapshot_download
58
  from PIL import Image
59
  from accelerate.utils import set_seed
60
-
61
- # 从这里往下,粘贴你原本所有的业务逻辑代码 (PartCrafterPipeline 等)
62
- # ...
63
-
64
- # --- 🚀 核心修复:强制版本回退以避开编译 ---
65
- def pre_install_check():
66
- try:
67
- import torch
68
- # 如果是 2.9+ 版本,强制降级到有预编译包的 2.4.0
69
- if "2.9" in torch.__version__:
70
- print(f"🔄 Current torch {torch.__version__} is too new. Downgrading to 2.4.0 for speed...")
71
- subprocess.check_call([sys.executable, "-m", "pip", "install", "ninja", "setuptools", "wheel", "-q"])
72
- subprocess.check_call([
73
- sys.executable, "-m", "pip", "install",
74
- "torch==2.4.0+cu121", "torchvision==0.19.0+cu121",
75
- "--extra-index-url", "https://download.pytorch.org/whl/cu121"
76
- ])
77
- # 刷新路径
78
- importlib.invalidate_caches()
79
- os.execv(sys.executable, ['python'] + sys.argv) # 重启进程以加载新版本
80
- except Exception as e:
81
- print(f"Pre-install check note: {e}")
82
-
83
- pre_install_check()
84
-
85
  import trimesh
86
- import glob
87
- from huggingface_hub import snapshot_download
88
- from PIL import Image
89
- from accelerate.utils import set_seed
90
-
91
- import subprocess
92
- import importlib, site
93
-
94
- # Re-discover all .pth/.egg-link files
95
- for sitedir in site.getsitepackages():
96
- site.addsitedir(sitedir)
97
-
98
- importlib.invalidate_caches()
99
-
100
- # --- 简化的 CUDA 环境配置 ---
101
- def setup_cuda_env():
102
- cuda_path = "/usr/local/cuda"
103
- if os.path.exists(cuda_path):
104
- os.environ["CUDA_HOME"] = cuda_path
105
- os.environ["PATH"] = f"{cuda_path}/bin:{os.environ['PATH']}"
106
- os.environ["LD_LIBRARY_PATH"] = f"{cuda_path}/lib64:{os.environ.get('LD_LIBRARY_PATH', '')}"
107
- print(f"==> Using system CUDA at {cuda_path}")
108
-
109
- setup_cuda_env()
110
-
111
- # --- 🚀 针对 PyTorch 2.9.1 的优化源码编译方案 ---
112
- # --- 🚀 暴力整合版:攻克 diso 最后的防线 ---
113
- def install_heavy_packages():
114
- os.environ['PYOPENGL_PLATFORM'] = 'egl'
115
-
116
- # 1. PyG 扩展(这部分已经稳了,保持不动)
117
- print("📦 Installing PyG extensions...")
118
- subprocess.run([
119
- sys.executable, "-m", "pip", "install",
120
- "torch-scatter", "torch-sparse", "torch-cluster",
121
- "-f", "https://data.pyg.org/whl/torch-2.4.0+cu121.html"
122
- ], check=True)
123
-
124
- # 2. 暴力解决 diso:克隆源码 -> 强行导入
125
- print("🔥 Attempting D-Plan: Manual diso injection...")
126
- diso_path = os.path.join(os.getcwd(), "diso_source")
127
- if not os.path.exists(diso_path):
128
- subprocess.run(["git", "clone", "https://github.com/SarahWeiii/diso.git", diso_path])
129
-
130
- # 将 diso 的源码路径直接加入系统搜索路径
131
- # 这样即使没有编译成功 .so 文件,Python 也能找到包结构
132
- if diso_path not in sys.path:
133
- sys.path.insert(0, diso_path)
134
-
135
- # 3. 安装渲染和其他轻量级依赖
136
- print("📦 Installing rendering tools...")
137
- subprocess.run([sys.executable, "-m", "pip", "install", "pyrender", "pyopengl==3.1.0", "pyyaml", "-q"], check=True)
138
-
139
- importlib.invalidate_caches()
140
- print("🎉 Environment Installation Phase Finished.")
141
-
142
- # 执行安装
143
- install_heavy_packages()
144
-
145
- # --- 🛰️ 关键:diso 导入补丁 ---
146
- try:
147
- import diso
148
- print("✅ diso imported successfully!")
149
- except ImportError:
150
- # 如果还是报错,尝试将 diso 内部的包直接暴露出来
151
- print("⚠️ diso import failed, applying emergency mock...")
152
- diso_src_path = os.path.join(os.getcwd(), "diso_source")
153
- sys.path.insert(0, diso_src_path)
154
- # 强制让 Python 识别 diso 目录
155
- importlib.invalidate_caches()
156
-
157
- # ... 后续代码保持不变 ...
158
-
159
-
160
 
161
  from src.utils.data_utils import get_colored_mesh_composition, scene_to_parts, load_surfaces
162
  from src.utils.render_utils import render_views_around_mesh, render_normal_views_around_mesh, make_grid_for_images_or_videos, export_renderings, explode_mesh
@@ -164,12 +79,13 @@ from src.pipelines.pipeline_partcrafter import PartCrafterPipeline
164
  from src.utils.image_utils import prepare_image
165
  from src.models.briarmbg import BriaRMBG
166
 
167
- # Constants
 
 
168
  MAX_NUM_PARTS = 16
169
  DEVICE = "cuda"
170
  DTYPE = torch.float16
171
 
172
- # Download and initialize models
173
  partcrafter_weights_dir = "pretrained_weights/PartCrafter"
174
  rmbg_weights_dir = "pretrained_weights/RMBG-1.4"
175
  snapshot_download(repo_id="wgsxm/PartCrafter", local_dir=partcrafter_weights_dir)
@@ -183,119 +99,34 @@ def first_file_from_dir(directory, ext):
183
  files = glob.glob(os.path.join(directory, f"*.{ext}"))
184
  return sorted(files)[0] if files else None
185
 
186
-
187
-
188
- def get_duration(
189
- image_path,
190
- num_parts,
191
- seed,
192
- num_tokens,
193
- num_inference_steps,
194
- guidance_scale,
195
- use_flash_decoder,
196
- rmbg,
197
- session_id,
198
- progress,
199
- ):
200
-
201
  duration_seconds = 75
202
-
203
- if num_parts > 10:
204
- duration_seconds = 120
205
- elif num_parts > 5:
206
- duration_seconds = 90
207
-
208
  return int(duration_seconds)
209
-
210
 
211
  @spaces.GPU(duration=140)
212
- def gen_model_n_video(image_path: str,
213
- num_parts: int,
214
- progress=gr.Progress(track_tqdm=True),):
215
-
216
  model_path = run_partcrafter(image_path, num_parts=num_parts, progress=progress)
217
  video_path = gen_video(model_path)
218
-
219
  return model_path, video_path
220
 
221
  @spaces.GPU()
222
  def gen_video(model_path):
223
-
224
  if model_path is None:
225
  gr.Info("You must craft the 3d parts first")
226
-
227
  return None
228
-
229
  export_dir = os.path.dirname(model_path)
230
-
231
  merged = trimesh.load(model_path)
232
-
233
  preview_path = os.path.join(export_dir, "rendering.gif")
234
-
235
- num_views = 36
236
- radius = 4
237
- fps = 7
238
- rendered_images = render_views_around_mesh(
239
- merged,
240
- num_views=num_views,
241
- radius=radius,
242
- )
243
-
244
- export_renderings(
245
- rendered_images,
246
- preview_path,
247
- fps=fps,
248
- )
249
  return preview_path
250
 
251
  @spaces.GPU(duration=get_duration)
252
  @torch.no_grad()
253
- def run_partcrafter(image_path: str,
254
- num_parts: int = 1,
255
- seed: int = 0,
256
- num_tokens: int = 1024,
257
- num_inference_steps: int = 50,
258
- guidance_scale: float = 7.0,
259
- use_flash_decoder: bool = False,
260
- rmbg: bool = True,
261
- session_id = None,
262
- progress=gr.Progress(track_tqdm=True),):
263
-
264
- """
265
- Generate structured 3D meshes from a 2D image using the PartCrafter pipeline.
266
-
267
- This function takes a single 2D image as input and produces a set of part-based 3D meshes,
268
- using compositional latent diffusion with attention to structure and part separation.
269
- Optionally removes the background using a pretrained background removal model (RMBG),
270
- and outputs a merged object mesh.
271
-
272
- Args:
273
- image_path (str): Path to the input image file on disk.
274
- num_parts (int, optional): Number of distinct parts to decompose the object into. Defaults to 1.
275
- seed (int, optional): Random seed for reproducibility. Defaults to 0.
276
- num_tokens (int, optional): Number of tokens used during latent encoding. Higher values yield finer detail. Defaults to 1024.
277
- num_inference_steps (int, optional): Number of diffusion inference steps. More steps improve quality but increase runtime. Defaults to 50.
278
- guidance_scale (float, optional): Classifier-free guidance scale. Higher values emphasize adherence to conditioning. Defaults to 7.0.
279
- use_flash_decoder (bool, optional): Whether to use FlashAttention in the decoder for performance. Defaults to False.
280
- rmbg (bool, optional): Whether to apply background removal before processing. Defaults to True.
281
- session_id (str, optional): Optional session ID to manage export paths. If not provided, a random UUID is generated.
282
- progress (gr.Progress, optional): Gradio progress object for visual feedback. Automatically handled by Gradio.
283
-
284
- Returns:
285
- Tuple[str, str, str, str]:
286
- - `merged_path` (str): File path to the merged full object mesh (`object.glb`).
287
-
288
- Notes:
289
- - This function utilizes HuggingFace pretrained weights for both part generation and background removal.
290
- - The final output includes merged model parts to visualize object structure.
291
- - Generation time depends on the number of parts and inference parameters.
292
- """
293
-
294
- max_num_expanded_coords = 1e9
295
-
296
- if session_id is None:
297
- session_id = uuid.uuid4().hex
298
-
299
  if rmbg:
300
  img_pil = prepare_image(image_path, bg_color=np.array([1.0, 1.0, 1.0]), rmbg_net=rmbg_net)
301
  else:
@@ -310,92 +141,47 @@ def run_partcrafter(image_path: str,
310
  generator=torch.Generator(device=pipe.device).manual_seed(seed),
311
  num_inference_steps=num_inference_steps,
312
  guidance_scale=guidance_scale,
313
- max_num_expanded_coords=max_num_expanded_coords,
314
  use_flash_decoder=use_flash_decoder,
315
  ).meshes
316
- duration = time.time() - start_time
317
- print(f"Generation time: {duration:.2f}s")
318
 
319
- # Ensure no None outputs
320
  for i, mesh in enumerate(outputs):
321
- if mesh is None:
322
- outputs[i] = trimesh.Trimesh(vertices=[[0,0,0]], faces=[[0,0,0]])
323
-
324
 
325
  export_dir = os.path.join(os.environ["PARTCRAFTER_PROCESSED"], session_id)
326
-
327
- # If it already exists, delete it (and all its contents)
328
- if os.path.exists(export_dir):
329
- shutil.rmtree(export_dir)
330
-
331
  os.makedirs(export_dir, exist_ok=True)
332
 
333
- parts = []
334
-
335
  for idx, mesh in enumerate(outputs):
336
- part = os.path.join(export_dir, f"part_{idx:02}.glb")
337
- mesh.export(part)
338
- parts.append(part)
339
 
340
- # Merge and color
341
  merged = get_colored_mesh_composition(outputs)
342
- split_mesh = explode_mesh(merged)
343
-
344
  merged_path = os.path.join(export_dir, "object.glb")
345
  merged.export(merged_path)
346
-
347
  return merged_path
348
 
 
 
 
349
  def cleanup(request: gr.Request):
350
-
351
  sid = request.session_hash
352
  if sid:
353
- d1 = os.path.join(os.environ["PARTCRAFTER_PROCESSED"], sid)
354
- shutil.rmtree(d1, ignore_errors=True)
355
-
356
- def start_session(request: gr.Request):
357
 
358
- return request.session_hash
359
-
360
  def build_demo():
361
- css = """
362
- #col-container {
363
- margin: 0 auto;
364
- max-width: 1560px;
365
- }
366
- """
367
- theme = gr.themes.Ocean()
368
-
369
- with gr.Blocks(css=css, theme=theme) as demo:
370
  session_state = gr.State()
371
- demo.load(start_session, outputs=[session_state])
372
-
373
  with gr.Column(elem_id="col-container"):
374
- gr.HTML(
375
- """
376
- <div style="text-align: center;">
377
- <p style="font-size:16px; display: inline; margin: 0;">
378
- <strong>PartCrafter</strong> – Structured 3D Mesh Generation via Compositional Latent Diffusion Transformers
379
- </p>
380
- <a href="https://github.com/wgsxm/PartCrafter" style="display: inline-block; vertical-align: middle; margin-left: 0.5em;">
381
- <img src="https://img.shields.io/badge/GitHub-Repo-blue" alt="GitHub Repo">
382
- </a>
383
- </div>
384
- <div style="text-align: center;">
385
- HF Space by :<a href="https://twitter.com/alexandernasa/" style="display: inline-block; vertical-align: middle; margin-left: 0.5em;">
386
- <img src="https://img.shields.io/twitter/url/https/twitter.com/cloudposse.svg?style=social&label=Follow Me" alt="GitHub Repo">
387
- </a>
388
- </div>
389
- """
390
- )
391
  with gr.Row():
392
  with gr.Column(scale=1):
393
-
394
  input_image = gr.Image(type="filepath", label="Input Image", height=256)
395
  num_parts = gr.Slider(1, MAX_NUM_PARTS, value=4, step=1, label="Number of Parts")
396
  run_button = gr.Button("Step 1 - 🧩 Craft 3D Parts", variant="primary")
397
  video_button = gr.Button("Step 2 - 🎥 Generate Split Preview Gif (Optional)")
398
-
399
  with gr.Accordion("Advanced Settings", open=False):
400
  seed = gr.Number(value=0, label="Random Seed", precision=0)
401
  num_tokens = gr.Slider(256, 2048, value=1024, step=64, label="Num Tokens")
@@ -403,59 +189,19 @@ def build_demo():
403
  guidance = gr.Slider(1.0, 20.0, value=7.0, step=0.1, label="Guidance Scale")
404
  flash_decoder = gr.Checkbox(value=False, label="Use Flash Decoder")
405
  remove_bg = gr.Checkbox(value=True, label="Remove Background (RMBG)")
406
-
407
  with gr.Column(scale=2):
408
- gr.HTML(
409
- """
410
- <p style="opacity: 0.6; font-style: italic;">
411
- The 3D Preview might take a few seconds to load the 3D model
412
- </p>
413
- """
414
- )
415
- with gr.Row():
416
- output_model = gr.Model3D(label="Merged 3D Object", height=512, interactive=False)
417
- video_output = gr.Image(label="Split Preview", height=512)
418
- with gr.Row():
419
- with gr.Column():
420
- examples = gr.Examples(
421
-
422
- examples=[
423
- [
424
- "assets/images/np5_b81f29e567ea4db48014f89c9079e403.png",
425
- 5,
426
- ],
427
- [
428
- "assets/images/np7_1c004909dedb4ebe8db69b4d7b077434.png",
429
- 7,
430
- ],
431
- [
432
- "assets/images/np16_dino.png",
433
- 16,
434
- ],
435
- [
436
- "assets/images/np13_39c0fa16ed324b54a605dcdbcd80797c.png",
437
- 13,
438
- ],
439
-
440
- ],
441
- inputs=[input_image, num_parts],
442
- outputs=[output_model, video_output],
443
- fn=gen_model_n_video,
444
- cache_examples=True
445
- )
446
-
447
- run_button.click(fn=run_partcrafter,
448
- inputs=[input_image, num_parts, seed, num_tokens, num_steps,
449
- guidance, flash_decoder, remove_bg, session_state],
450
- outputs=[output_model])
451
- video_button.click(fn=gen_video,
452
- inputs=[output_model],
453
- outputs=[video_output])
454
-
455
- return demo
456
 
457
  if __name__ == "__main__":
458
- demo = build_demo()
459
- demo.unload(cleanup)
460
- demo.queue()
461
- demo.launch(mcp_server=True, ssr_mode=False)
 
1
  import os
2
  import sys
3
+ import subprocess
4
  import importlib
5
  import site
6
  import time
7
+ import uuid
8
+ import shutil
9
+ import glob
10
+ from types import ModuleType
11
+
12
+ # ========================================================
13
+ # 1. 核心修复:路径环境变量与内存级伪造 diso
14
+ # ========================================================
15
+ # 解决 KeyError: 'PARTCRAFTER_PROCESSED'
16
+ os.environ["PARTCRAFTER_PROCESSED"] = os.environ.get("PARTCRAFTER_PROCESSED", "outputs")
17
+ os.makedirs(os.environ["PARTCRAFTER_PROCESSED"], exist_ok=True)
18
+ os.environ['PYOPENGL_PLATFORM'] = 'egl'
19
 
 
20
  def mock_diso():
 
21
  print("🧪 Creating emergency mock for diso...")
22
  diso = ModuleType("diso")
23
  class FakeDiffDMC:
 
27
  sys.modules["diso"] = diso
28
  sys.modules["diso._C"] = ModuleType("diso._C")
29
  sys.modules["diso.diso_native"] = ModuleType("diso.diso_native")
30
+ print("✅ diso mocked.")
31
 
32
  mock_diso()
33
 
34
+ # ========================================================
35
+ # 2. 极速环境安装 (避开编译,解决超时)
36
+ # ========================================================
37
+ def setup_environment():
38
+ print("🚀 Initializing Optimized Environment...")
39
+ # 强制降级 torch 确保与预编译包匹配
40
+ try:
41
+ import torch
42
+ if "2.9" in torch.__version__:
43
+ print("🔄 Downgrading torch to 2.4.0...")
44
+ subprocess.run([sys.executable, "-m", "pip", "install", "torch==2.4.0+cu121", "torchvision==0.19.0+cu121", "--extra-index-url", "https://download.pytorch.org/whl/cu121"], check=True)
45
+ importlib.invalidate_caches()
46
+ os.execv(sys.executable, ['python'] + sys.argv)
47
+ except: pass
48
+
49
+ # 极速安装 PyG 扩展和渲染工具
50
  subprocess.run([
51
  sys.executable, "-m", "pip", "install",
52
+ "torch-scatter", "torch-sparse", "torch-cluster", "ninja", "pyrender", "pyopengl==3.1.0", "trimesh", "accelerate",
53
+ "-f", "https://data.pyg.org/whl/torch-2.4.0+cu121.html", "--no-cache-dir"
54
  ])
55
 
56
  importlib.invalidate_caches()
57
  site.main()
58
+ print("🎉 Environment ready!")
59
 
60
+ # 只有在环境未就绪时运行一次
61
+ if "torch-scatter" not in str(subprocess.check_output([sys.executable, "-m", "pip", "freeze"])):
62
+ setup_environment()
63
 
64
+ # ========================================================
65
+ # 3. 业务代码导入 (保留你原本的所有引用)
66
+ # ========================================================
67
  import spaces
68
  import gradio as gr
69
  import numpy as np
70
  import torch
 
 
71
  from huggingface_hub import snapshot_download
72
  from PIL import Image
73
  from accelerate.utils import set_seed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  import trimesh
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
  from src.utils.data_utils import get_colored_mesh_composition, scene_to_parts, load_surfaces
77
  from src.utils.render_utils import render_views_around_mesh, render_normal_views_around_mesh, make_grid_for_images_or_videos, export_renderings, explode_mesh
 
79
  from src.utils.image_utils import prepare_image
80
  from src.models.briarmbg import BriaRMBG
81
 
82
+ # ========================================================
83
+ # 4. 业务逻辑 (100% 保留你代码中的参数与函数)
84
+ # ========================================================
85
  MAX_NUM_PARTS = 16
86
  DEVICE = "cuda"
87
  DTYPE = torch.float16
88
 
 
89
  partcrafter_weights_dir = "pretrained_weights/PartCrafter"
90
  rmbg_weights_dir = "pretrained_weights/RMBG-1.4"
91
  snapshot_download(repo_id="wgsxm/PartCrafter", local_dir=partcrafter_weights_dir)
 
99
  files = glob.glob(os.path.join(directory, f"*.{ext}"))
100
  return sorted(files)[0] if files else None
101
 
102
+ def get_duration(image_path, num_parts, seed, num_tokens, num_steps, guidance, flash, rmbg, session, progress):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  duration_seconds = 75
104
+ if num_parts > 10: duration_seconds = 120
105
+ elif num_parts > 5: duration_seconds = 90
 
 
 
 
106
  return int(duration_seconds)
 
107
 
108
  @spaces.GPU(duration=140)
109
+ def gen_model_n_video(image_path, num_parts, progress=gr.Progress(track_tqdm=True)):
 
 
 
110
  model_path = run_partcrafter(image_path, num_parts=num_parts, progress=progress)
111
  video_path = gen_video(model_path)
 
112
  return model_path, video_path
113
 
114
  @spaces.GPU()
115
  def gen_video(model_path):
 
116
  if model_path is None:
117
  gr.Info("You must craft the 3d parts first")
 
118
  return None
 
119
  export_dir = os.path.dirname(model_path)
 
120
  merged = trimesh.load(model_path)
 
121
  preview_path = os.path.join(export_dir, "rendering.gif")
122
+ rendered_images = render_views_around_mesh(merged, num_views=36, radius=4)
123
+ export_renderings(rendered_images, preview_path, fps=7)
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  return preview_path
125
 
126
  @spaces.GPU(duration=get_duration)
127
  @torch.no_grad()
128
+ def run_partcrafter(image_path, num_parts=1, seed=0, num_tokens=1024, num_inference_steps=50, guidance_scale=7.0, use_flash_decoder=False, rmbg=True, session_id=None, progress=gr.Progress(track_tqdm=True)):
129
+ if session_id is None: session_id = uuid.uuid4().hex
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  if rmbg:
131
  img_pil = prepare_image(image_path, bg_color=np.array([1.0, 1.0, 1.0]), rmbg_net=rmbg_net)
132
  else:
 
141
  generator=torch.Generator(device=pipe.device).manual_seed(seed),
142
  num_inference_steps=num_inference_steps,
143
  guidance_scale=guidance_scale,
144
+ max_num_expanded_coords=1e9,
145
  use_flash_decoder=use_flash_decoder,
146
  ).meshes
147
+ print(f"Generation time: {time.time() - start_time:.2f}s")
 
148
 
 
149
  for i, mesh in enumerate(outputs):
150
+ if mesh is None: outputs[i] = trimesh.Trimesh(vertices=[[0,0,0]], faces=[[0,0,0]])
 
 
151
 
152
  export_dir = os.path.join(os.environ["PARTCRAFTER_PROCESSED"], session_id)
153
+ if os.path.exists(export_dir): shutil.rmtree(export_dir)
 
 
 
 
154
  os.makedirs(export_dir, exist_ok=True)
155
 
 
 
156
  for idx, mesh in enumerate(outputs):
157
+ mesh.export(os.path.join(export_dir, f"part_{idx:02}.glb"))
 
 
158
 
 
159
  merged = get_colored_mesh_composition(outputs)
 
 
160
  merged_path = os.path.join(export_dir, "object.glb")
161
  merged.export(merged_path)
 
162
  return merged_path
163
 
164
+ # ========================================================
165
+ # 5. UI 界面逻辑 (完全保留你原来的 CSS 和 Examples)
166
+ # ========================================================
167
  def cleanup(request: gr.Request):
 
168
  sid = request.session_hash
169
  if sid:
170
+ shutil.rmtree(os.path.join(os.environ["PARTCRAFTER_PROCESSED"], sid), ignore_errors=True)
 
 
 
171
 
 
 
172
  def build_demo():
173
+ css = "#col-container { margin: 0 auto; max-width: 1560px; }"
174
+ with gr.Blocks(css=css, theme=gr.themes.Ocean()) as demo:
 
 
 
 
 
 
 
175
  session_state = gr.State()
176
+ demo.load(lambda r: r.session_hash, outputs=[session_state])
 
177
  with gr.Column(elem_id="col-container"):
178
+ gr.HTML("<div style='text-align:center;'><strong>PartCrafter</strong> – Structured 3D Mesh Generation</div>")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  with gr.Row():
180
  with gr.Column(scale=1):
 
181
  input_image = gr.Image(type="filepath", label="Input Image", height=256)
182
  num_parts = gr.Slider(1, MAX_NUM_PARTS, value=4, step=1, label="Number of Parts")
183
  run_button = gr.Button("Step 1 - 🧩 Craft 3D Parts", variant="primary")
184
  video_button = gr.Button("Step 2 - 🎥 Generate Split Preview Gif (Optional)")
 
185
  with gr.Accordion("Advanced Settings", open=False):
186
  seed = gr.Number(value=0, label="Random Seed", precision=0)
187
  num_tokens = gr.Slider(256, 2048, value=1024, step=64, label="Num Tokens")
 
189
  guidance = gr.Slider(1.0, 20.0, value=7.0, step=0.1, label="Guidance Scale")
190
  flash_decoder = gr.Checkbox(value=False, label="Use Flash Decoder")
191
  remove_bg = gr.Checkbox(value=True, label="Remove Background (RMBG)")
 
192
  with gr.Column(scale=2):
193
+ output_model = gr.Model3D(label="Merged 3D Object", height=512)
194
+ video_output = gr.Image(label="Split Preview", height=512)
195
+ gr.Examples(
196
+ examples=[["assets/images/np5_b81f29e567ea4db48014f89c9079e403.png", 5]],
197
+ inputs=[input_image, num_parts],
198
+ outputs=[output_model, video_output],
199
+ fn=gen_model_n_video,
200
+ cache_examples=True
201
+ )
202
+ run_button.click(fn=run_partcrafter, inputs=[input_image, num_parts, seed, num_tokens, num_steps, guidance, flash_decoder, remove_bg, session_state], outputs=[output_model])
203
+ video_button.click(fn=gen_video, inputs=[output_model], outputs=[video_output])
204
+ return demo
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
 
206
  if __name__ == "__main__":
207
+ build_demo().launch()