JSCPPProgrammer commited on
Commit
6efbafb
·
verified ·
1 Parent(s): 54d85c9

Upload space/app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. space/app.py +335 -156
space/app.py CHANGED
@@ -130,9 +130,10 @@ import signal
130
  import subprocess
131
  import sys
132
  import time
 
133
  from datetime import datetime
134
  from pathlib import Path
135
- from typing import Tuple
136
 
137
  import gradio as gr
138
  import numpy as np
@@ -240,10 +241,95 @@ def get_seed(randomize_seed: bool, seed: int) -> int:
240
  return int(np.random.randint(0, MAX_SEED)) if randomize_seed else int(seed)
241
 
242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  # ---------------------------------------------------------------------------
244
  # Stage 1: Image -> textured GLB (TRELLIS.2-4B)
245
  # ---------------------------------------------------------------------------
246
- @spaces.GPU(duration=180)
247
  def generate_3d(
248
  image: Image.Image,
249
  seed: int,
@@ -252,71 +338,117 @@ def generate_3d(
252
  texture_size: int,
253
  req: gr.Request,
254
  progress=gr.Progress(track_tqdm=True),
255
- ) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  if image is None:
257
  raise gr.Error("Please upload an image first.")
258
 
259
- # Always preprocess here so generation works even if the upload handler
260
- # hasn't finished yet (and so the GPU worker has a clean RGBA crop).
261
- image = preprocess_image(image)
262
-
263
- outputs, latents = pipeline.run(
264
- image,
265
- seed=seed,
266
- preprocess_image=False,
267
- sparse_structure_sampler_params={
268
- "steps": 12,
269
- "guidance_strength": 7.5,
270
- "guidance_rescale": 0.7,
271
- "rescale_t": 5.0,
272
- },
273
- shape_slat_sampler_params={
274
- "steps": 12,
275
- "guidance_strength": 7.5,
276
- "guidance_rescale": 0.5,
277
- "rescale_t": 3.0,
278
- },
279
- tex_slat_sampler_params={
280
- "steps": 12,
281
- "guidance_strength": 1.0,
282
- "guidance_rescale": 0.0,
283
- "rescale_t": 3.0,
284
- },
285
- pipeline_type={
286
- "512": "512",
287
- "1024": "1024_cascade",
288
- "1536": "1536_cascade",
289
- }[resolution],
290
- return_latent=True,
291
- )
292
- mesh = outputs[0]
293
- mesh.simplify(16777216) # nvdiffrast limit
294
- res = latents[2]
295
-
296
- glb = o_voxel.postprocess.to_glb(
297
- vertices=mesh.vertices,
298
- faces=mesh.faces,
299
- attr_volume=mesh.attrs,
300
- coords=mesh.coords,
301
- attr_layout=pipeline.pbr_attr_layout,
302
- grid_size=res,
303
- aabb=[[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]],
304
- decimation_target=decimation_target,
305
- texture_size=texture_size,
306
- remesh=True,
307
- remesh_band=1,
308
- remesh_project=0,
309
- use_tqdm=True,
310
- )
311
 
312
- user_dir = os.path.join(TMP_DIR, str(req.session_hash))
313
- os.makedirs(user_dir, exist_ok=True)
314
- now = datetime.now()
315
- timestamp = now.strftime("%Y-%m-%dT%H%M%S") + f".{now.microsecond // 1000:03d}"
316
- glb_path = os.path.join(user_dir, f"model_{timestamp}.glb")
317
- glb.export(glb_path, extension_webp=False) # PNG textures: safest for Blender import
318
- torch.cuda.empty_cache()
319
- return glb_path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
 
321
 
322
  # ---------------------------------------------------------------------------
@@ -400,7 +532,11 @@ def load_rig_model():
400
  print("[app] TokenRig model loaded.", flush=True)
401
 
402
 
403
- @spaces.GPU(duration=320)
 
 
 
 
404
  def rig_3d(
405
  glb_path: str,
406
  num_beams: int,
@@ -410,81 +546,113 @@ def rig_3d(
410
  repetition_penalty: float,
411
  req: gr.Request,
412
  progress=gr.Progress(track_tqdm=True),
413
- ) -> Tuple[str, str, str]:
 
414
  if not glb_path or not os.path.exists(glb_path):
415
- raise gr.Error("Generate (or upload) a 3D model first.")
416
-
417
- ensure_bpy_server_started()
418
- load_rig_model()
419
 
420
- datapath = {
421
- "data_name": None,
422
- "loader": "bpy_server",
423
- "filepaths": {"articulation": [str(glb_path)]},
424
- }
425
- dataset_config = DatasetConfig.parse(
426
- shuffle=False,
427
- batch_size=1,
428
- num_workers=0,
429
- pin_memory=False,
430
- persistent_workers=False,
431
- datapath=datapath,
432
- ).split_by_cls()
433
-
434
- module = RigDatasetModule(
435
- predict_dataset_config=dataset_config,
436
- predict_transform=rig_transform,
437
- tokenizer=rig_tokenizer,
438
- process_fn=rig_model._process_fn,
439
- )
440
- dataloader = module.predict_dataloader()["articulation"]
441
- infer_device = rig_model.device if rig_model is not None else "cuda"
442
-
443
- user_dir = os.path.join(TMP_DIR, str(req.session_hash))
444
- os.makedirs(user_dir, exist_ok=True)
445
- stem = Path(glb_path).stem
446
- out_glb = os.path.join(user_dir, f"{stem}_rigged.glb")
447
- out_fbx = os.path.join(user_dir, f"{stem}_rigged.fbx")
448
-
449
- for batch in dataloader:
450
- batch = {
451
- k: v.to(infer_device) if isinstance(v, Tensor) else v for k, v in batch.items()
452
  }
453
- batch.pop("skeleton_tokens", None)
454
- batch.pop("skeleton_mask", None)
455
- batch["generate_kwargs"] = dict(
456
- max_length=2048,
457
- top_k=int(top_k),
458
- top_p=float(top_p),
459
- temperature=float(temperature),
460
- repetition_penalty=float(repetition_penalty),
461
- num_return_sequences=1,
462
- num_beams=int(num_beams),
463
- do_sample=True,
 
 
464
  )
465
-
466
- preds = rig_model.predict_step(batch, skeleton_tokens=None, make_asset=True)["results"]
467
- asset = preds[0].asset
468
- assert asset is not None
469
-
470
- # Transfer the predicted rig back onto the original mesh so PBR
471
- # textures and world scale survive, then export both formats.
472
- for export_path in (out_glb, out_fbx):
473
- payload = dict(
474
- source_asset=asset,
475
- target_path=asset.path,
476
- export_path=export_path,
477
- group_per_vertex=4,
478
- )
479
- res = bytes_to_object(
480
- requests.post(f"{BPY_SERVER}/transfer", data=object_to_bytes(payload)).content
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
481
  )
482
- if res != "ok":
483
- raise gr.Error(f"Rig export failed: {res}")
484
- print(f"[app] Exported: {export_path}", flush=True)
485
-
486
- torch.cuda.empty_cache()
487
- return out_glb, out_glb, out_fbx
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
488
 
489
 
490
  # ---------------------------------------------------------------------------
@@ -528,6 +696,16 @@ with gr.Blocks(title="Image → 3D → Rigged Model", delete_cache=(600, 600)) a
528
  sources=["upload", "clipboard"],
529
  height=320,
530
  )
 
 
 
 
 
 
 
 
 
 
531
  generate_btn = gr.Button("1️⃣ Generate 3D Model", variant="primary")
532
  rig_btn = gr.Button("2️⃣ Auto-Rig Model", variant="primary", interactive=False)
533
 
@@ -591,43 +769,44 @@ with gr.Blocks(title="Image → 3D → Rigged Model", delete_cache=(600, 600)) a
591
  demo.load(start_session)
592
  demo.unload(end_session)
593
 
594
- image_prompt.upload(
595
- preprocess_image,
596
- inputs=[image_prompt],
597
- outputs=[image_prompt],
598
- )
599
 
600
  generate_btn.click(
601
  get_seed,
602
  inputs=[randomize_seed, seed],
603
  outputs=[seed],
 
 
 
 
 
 
604
  ).then(
605
  generate_3d,
606
  inputs=[image_prompt, seed, resolution, decimation_target, texture_size],
607
- outputs=[glb_state],
 
608
  ).then(
609
- lambda p: (
610
- p,
611
- gr.DownloadButton(value=p, interactive=True),
612
- gr.Button(interactive=True),
613
- ),
614
- inputs=[glb_state],
615
- outputs=[model_output, download_glb, rig_btn],
616
  )
617
 
618
  rig_btn.click(
 
 
 
 
619
  rig_3d,
620
  inputs=[glb_state, num_beams, top_k, top_p, temperature, repetition_penalty],
621
- outputs=[rigged_output, download_rigged_glb, download_rigged_fbx],
622
- ).then(
623
- lambda g, f: (
624
- gr.DownloadButton(value=g, interactive=True),
625
- gr.DownloadButton(value=f, interactive=True),
626
- ),
627
- inputs=[download_rigged_glb, download_rigged_fbx],
628
- outputs=[download_rigged_glb, download_rigged_fbx],
629
  )
630
 
 
 
 
631
 
632
  # ---------------------------------------------------------------------------
633
  # Module-scope model load (ZeroGPU packs CUDA weights at boot, streams them
@@ -646,4 +825,4 @@ pipeline.cuda()
646
 
647
 
648
  if __name__ == "__main__":
649
- demo.launch(show_error=True)
 
130
  import subprocess
131
  import sys
132
  import time
133
+ import traceback
134
  from datetime import datetime
135
  from pathlib import Path
136
+ from typing import Dict, List, Tuple
137
 
138
  import gradio as gr
139
  import numpy as np
 
241
  return int(np.random.randint(0, MAX_SEED)) if randomize_seed else int(seed)
242
 
243
 
244
+ # ---------------------------------------------------------------------------
245
+ # Per-task progress UI (HTML bars streamed via generator yields)
246
+ # ---------------------------------------------------------------------------
247
+ GEN_STEP_NAMES = [
248
+ "1. Remove background",
249
+ "2. Encode image (DINOv3)",
250
+ "3. Sample sparse 3D structure",
251
+ "4. Generate mesh shape",
252
+ "5. Generate PBR textures",
253
+ "6. Export textured GLB",
254
+ ]
255
+
256
+ RIG_STEP_NAMES = [
257
+ "1. Start Blender server",
258
+ "2. Load TokenRig model",
259
+ "3. Load input mesh",
260
+ "4. Predict skeleton + skinning",
261
+ "5. Export rigged GLB",
262
+ "6. Export rigged FBX",
263
+ ]
264
+
265
+ _STEP_STYLE = {
266
+ "pending": ("#9ca3af", "○"),
267
+ "running": ("#7c3aed", "◐"),
268
+ "done": ("#22c55e", "✓"),
269
+ "error": ("#ef4444", "✗"),
270
+ }
271
+
272
+
273
+ def _init_steps(names: List[str]) -> List[Dict]:
274
+ return [{"name": n, "state": "pending", "pct": 0} for n in names]
275
+
276
+
277
+ def _render_steps(steps: List[Dict]) -> str:
278
+ rows = []
279
+ for s in steps:
280
+ color, icon = _STEP_STYLE[s["state"]]
281
+ pct = s.get("pct", 0)
282
+ rows.append(
283
+ f'<div style="margin:10px 0">'
284
+ f'<div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:4px">'
285
+ f"<span>{icon} {s['name']}</span><span>{pct}%</span></div>"
286
+ f'<div style="background:#e5e7eb;border-radius:6px;height:10px;overflow:hidden">'
287
+ f'<div style="width:{pct}%;background:{color};height:10px;border-radius:6px;'
288
+ f'transition:width .4s"></div></div></div>'
289
+ )
290
+ return f'<div style="padding:4px 0">{"".join(rows)}</div>'
291
+
292
+
293
+ def _mark(steps: List[Dict], idx: int, state: str, pct: int) -> str:
294
+ steps[idx]["state"] = state
295
+ steps[idx]["pct"] = pct
296
+ return _render_steps(steps)
297
+
298
+
299
+ def _noop_files():
300
+ return gr.update(), gr.update(), gr.update()
301
+
302
+
303
+ def preprocess_with_progress(image: Image.Image):
304
+ """Step 1 runs on CPU (not ZeroGPU) so BRIA RMBG doesn't eat the GPU quota."""
305
+ steps = _init_steps(GEN_STEP_NAMES)
306
+ if image is None:
307
+ raise gr.Error("Please upload an image first.")
308
+ yield image, _mark(steps, 0, "running", 10), "Step 1/6: Removing background (BRIA RMBG)…"
309
+ try:
310
+ processed = preprocess_image(image)
311
+ except Exception as e:
312
+ tb = traceback.format_exc()
313
+ print(tb, flush=True)
314
+ yield image, _mark(steps, 0, "error", 100), f"FAILED at step 1: {e}"
315
+ raise gr.Error(f"Background removal failed: {e}") from e
316
+ steps[0]["state"] = "done"
317
+ steps[0]["pct"] = 100
318
+ yield processed, _render_steps(steps), "Step 1/6 done. Queuing TRELLIS.2 on ZeroGPU…"
319
+
320
+
321
+ def _gpu_duration_generate(*args, **kwargs):
322
+ # ZeroGPU forwards the full handler arg list (including gr.Progress).
323
+ resolution = kwargs.get("resolution")
324
+ if resolution is None and len(args) >= 3:
325
+ resolution = args[2]
326
+ return {"512": 300, "1024": 480, "1536": 600}.get(str(resolution), 480)
327
+
328
+
329
  # ---------------------------------------------------------------------------
330
  # Stage 1: Image -> textured GLB (TRELLIS.2-4B)
331
  # ---------------------------------------------------------------------------
332
+ @spaces.GPU(duration=_gpu_duration_generate)
333
  def generate_3d(
334
  image: Image.Image,
335
  seed: int,
 
338
  texture_size: int,
339
  req: gr.Request,
340
  progress=gr.Progress(track_tqdm=True),
341
+ ):
342
+ steps = [{"name": GEN_STEP_NAMES[0], "state": "done", "pct": 100}]
343
+ steps += _init_steps(GEN_STEP_NAMES[1:])
344
+ ptype = {"512": "512", "1024": "1024_cascade", "1536": "1536_cascade"}[resolution]
345
+ ss_params = {
346
+ "steps": 12, "guidance_strength": 7.5, "guidance_rescale": 0.7, "rescale_t": 5.0,
347
+ }
348
+ shape_params = {
349
+ "steps": 12, "guidance_strength": 7.5, "guidance_rescale": 0.5, "rescale_t": 3.0,
350
+ }
351
+ tex_params = {
352
+ "steps": 12, "guidance_strength": 1.0, "guidance_rescale": 0.0, "rescale_t": 3.0,
353
+ }
354
+
355
  if image is None:
356
  raise gr.Error("Please upload an image first.")
357
 
358
+ try:
359
+ # Step 2 encode image features
360
+ yield (*_noop_files(), _mark(steps, 1, "running", 15), "Step 2/6: Encoding image (DINOv3)…")
361
+ torch.manual_seed(seed)
362
+ cond_512 = pipeline.get_cond([image], 512)
363
+ cond_1024 = pipeline.get_cond([image], 1024) if ptype != "512" else None
364
+ yield (*_noop_files(), _mark(steps, 1, "done", 100), "Step 2/6 done.")
365
+
366
+ # Step 3 — sparse structure
367
+ yield (*_noop_files(), _mark(steps, 2, "running", 20), "Step 3/6: Sampling sparse 3D structure…")
368
+ ss_res = {"512": 32, "1024": 64, "1024_cascade": 32, "1536_cascade": 32}[ptype]
369
+ coords = pipeline.sample_sparse_structure(cond_512, ss_res, 1, ss_params)
370
+ yield (*_noop_files(), _mark(steps, 2, "done", 100), "Step 3/6 done.")
371
+
372
+ # Step 4 — mesh shape
373
+ yield (*_noop_files(), _mark(steps, 3, "running", 30), "Step 4/6: Generating mesh shape…")
374
+ if ptype == "512":
375
+ shape_slat = pipeline.sample_shape_slat(
376
+ cond_512, pipeline.models["shape_slat_flow_model_512"], coords, shape_params
377
+ )
378
+ res = 512
379
+ elif ptype == "1024":
380
+ shape_slat = pipeline.sample_shape_slat(
381
+ cond_1024, pipeline.models["shape_slat_flow_model_1024"], coords, shape_params
382
+ )
383
+ res = 1024
384
+ elif ptype == "1024_cascade":
385
+ shape_slat, res = pipeline.sample_shape_slat_cascade(
386
+ cond_512, cond_1024,
387
+ pipeline.models["shape_slat_flow_model_512"],
388
+ pipeline.models["shape_slat_flow_model_1024"],
389
+ 512, 1024, coords, shape_params, 49152,
390
+ )
391
+ else: # 1536_cascade
392
+ shape_slat, res = pipeline.sample_shape_slat_cascade(
393
+ cond_512, cond_1024,
394
+ pipeline.models["shape_slat_flow_model_512"],
395
+ pipeline.models["shape_slat_flow_model_1024"],
396
+ 512, 1536, coords, shape_params, 49152,
397
+ )
398
+ yield (*_noop_files(), _mark(steps, 3, "done", 100), "Step 4/6 done.")
 
 
 
 
 
 
 
 
 
 
 
399
 
400
+ # Step 5 — PBR textures
401
+ yield (*_noop_files(), _mark(steps, 4, "running", 50), "Step 5/6: Generating PBR textures…")
402
+ if ptype == "512":
403
+ tex_slat = pipeline.sample_tex_slat(
404
+ cond_512, pipeline.models["tex_slat_flow_model_512"], shape_slat, tex_params
405
+ )
406
+ else:
407
+ tex_slat = pipeline.sample_tex_slat(
408
+ cond_1024, pipeline.models["tex_slat_flow_model_1024"], shape_slat, tex_params
409
+ )
410
+ mesh = pipeline.decode_latent(shape_slat, tex_slat, res)[0]
411
+ mesh.simplify(16777216)
412
+ yield (*_noop_files(), _mark(steps, 4, "done", 100), "Step 5/6 done.")
413
+
414
+ # Step 6 — export GLB
415
+ yield (*_noop_files(), _mark(steps, 5, "running", 70), "Step 6/6: Exporting textured GLB…")
416
+ glb = o_voxel.postprocess.to_glb(
417
+ vertices=mesh.vertices,
418
+ faces=mesh.faces,
419
+ attr_volume=mesh.attrs,
420
+ coords=mesh.coords,
421
+ attr_layout=pipeline.pbr_attr_layout,
422
+ grid_size=res,
423
+ aabb=[[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]],
424
+ decimation_target=decimation_target,
425
+ texture_size=texture_size,
426
+ remesh=True,
427
+ remesh_band=1,
428
+ remesh_project=0,
429
+ use_tqdm=True,
430
+ )
431
+ user_dir = os.path.join(TMP_DIR, str(req.session_hash))
432
+ os.makedirs(user_dir, exist_ok=True)
433
+ now = datetime.now()
434
+ timestamp = now.strftime("%Y-%m-%dT%H%M%S") + f".{now.microsecond // 1000:03d}"
435
+ glb_path = os.path.join(user_dir, f"model_{timestamp}.glb")
436
+ glb.export(glb_path, extension_webp=False)
437
+ torch.cuda.empty_cache()
438
+ yield (
439
+ glb_path, glb_path, glb_path,
440
+ _mark(steps, 5, "done", 100),
441
+ f"All 6 steps complete — {os.path.basename(glb_path)} ready. Click Auto-Rig.",
442
+ )
443
+ except Exception as e:
444
+ tb = traceback.format_exc()
445
+ print(tb, flush=True)
446
+ for s in steps:
447
+ if s["state"] == "running":
448
+ s["state"] = "error"
449
+ s["pct"] = 100
450
+ yield (*_noop_files(), _render_steps(steps), f"FAILED: {e}")
451
+ raise gr.Error(f"3D generation failed: {e}") from e
452
 
453
 
454
  # ---------------------------------------------------------------------------
 
532
  print("[app] TokenRig model loaded.", flush=True)
533
 
534
 
535
+ def _noop_rig_files():
536
+ return gr.update(), gr.update(), gr.update()
537
+
538
+
539
+ @spaces.GPU(duration=480)
540
  def rig_3d(
541
  glb_path: str,
542
  num_beams: int,
 
546
  repetition_penalty: float,
547
  req: gr.Request,
548
  progress=gr.Progress(track_tqdm=True),
549
+ ):
550
+ steps = _init_steps(RIG_STEP_NAMES)
551
  if not glb_path or not os.path.exists(glb_path):
552
+ raise gr.Error("Generate a 3D model first.")
 
 
 
553
 
554
+ try:
555
+ yield (*_noop_rig_files(), _mark(steps, 0, "running", 10), "Step 1/6: Starting Blender export server…")
556
+ ensure_bpy_server_started()
557
+ yield (*_noop_rig_files(), _mark(steps, 0, "done", 100), "Step 1/6 done.")
558
+
559
+ yield (*_noop_rig_files(), _mark(steps, 1, "running", 20), "Step 2/6: Loading TokenRig model…")
560
+ load_rig_model()
561
+ yield (*_noop_rig_files(), _mark(steps, 1, "done", 100), "Step 2/6 done.")
562
+
563
+ yield (*_noop_rig_files(), _mark(steps, 2, "running", 30), "Step 3/6: Loading input mesh…")
564
+ datapath = {
565
+ "data_name": None,
566
+ "loader": "bpy_server",
567
+ "filepaths": {"articulation": [str(glb_path)]},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
568
  }
569
+ dataset_config = DatasetConfig.parse(
570
+ shuffle=False,
571
+ batch_size=1,
572
+ num_workers=0,
573
+ pin_memory=False,
574
+ persistent_workers=False,
575
+ datapath=datapath,
576
+ ).split_by_cls()
577
+ module = RigDatasetModule(
578
+ predict_dataset_config=dataset_config,
579
+ predict_transform=rig_transform,
580
+ tokenizer=rig_tokenizer,
581
+ process_fn=rig_model._process_fn,
582
  )
583
+ dataloader = module.predict_dataloader()["articulation"]
584
+ infer_device = rig_model.device if rig_model is not None else "cuda"
585
+ yield (*_noop_rig_files(), _mark(steps, 2, "done", 100), "Step 3/6 done.")
586
+
587
+ user_dir = os.path.join(TMP_DIR, str(req.session_hash))
588
+ os.makedirs(user_dir, exist_ok=True)
589
+ stem = Path(glb_path).stem
590
+ out_glb = os.path.join(user_dir, f"{stem}_rigged.glb")
591
+ out_fbx = os.path.join(user_dir, f"{stem}_rigged.fbx")
592
+
593
+ yield (
594
+ *_noop_rig_files(),
595
+ _mark(steps, 3, "running", 45),
596
+ "Step 4/6: Predicting skeleton + skinning weights…",
597
+ )
598
+ for batch in dataloader:
599
+ batch = {
600
+ k: v.to(infer_device) if isinstance(v, Tensor) else v
601
+ for k, v in batch.items()
602
+ }
603
+ batch.pop("skeleton_tokens", None)
604
+ batch.pop("skeleton_mask", None)
605
+ batch["generate_kwargs"] = dict(
606
+ max_length=2048,
607
+ top_k=int(top_k),
608
+ top_p=float(top_p),
609
+ temperature=float(temperature),
610
+ repetition_penalty=float(repetition_penalty),
611
+ num_return_sequences=1,
612
+ num_beams=int(num_beams),
613
+ do_sample=True,
614
  )
615
+ preds = rig_model.predict_step(batch, skeleton_tokens=None, make_asset=True)["results"]
616
+ asset = preds[0].asset
617
+ assert asset is not None
618
+ yield (*_noop_rig_files(), _mark(steps, 3, "done", 100), "Step 4/6 done.")
619
+
620
+ yield (*_noop_rig_files(), _mark(steps, 4, "running", 70), "Step 5/6: Exporting rigged GLB…")
621
+ payload = dict(
622
+ source_asset=asset,
623
+ target_path=asset.path,
624
+ export_path=out_glb,
625
+ group_per_vertex=4,
626
+ )
627
+ res = bytes_to_object(
628
+ requests.post(f"{BPY_SERVER}/transfer", data=object_to_bytes(payload)).content
629
+ )
630
+ if res != "ok":
631
+ raise RuntimeError(f"GLB export failed: {res}")
632
+ yield (*_noop_rig_files(), _mark(steps, 4, "done", 100), "Step 5/6 done.")
633
+
634
+ yield (*_noop_rig_files(), _mark(steps, 5, "running", 85), "Step 6/6: Exporting rigged FBX…")
635
+ payload["export_path"] = out_fbx
636
+ res = bytes_to_object(
637
+ requests.post(f"{BPY_SERVER}/transfer", data=object_to_bytes(payload)).content
638
+ )
639
+ if res != "ok":
640
+ raise RuntimeError(f"FBX export failed: {res}")
641
+ torch.cuda.empty_cache()
642
+ yield (
643
+ out_glb, out_glb, out_fbx,
644
+ _mark(steps, 5, "done", 100),
645
+ "All 6 rigging steps complete — download GLB or FBX for Blender.",
646
+ )
647
+ except Exception as e:
648
+ tb = traceback.format_exc()
649
+ print(tb, flush=True)
650
+ for s in steps:
651
+ if s["state"] == "running":
652
+ s["state"] = "error"
653
+ s["pct"] = 100
654
+ yield (*_noop_rig_files(), _render_steps(steps), f"FAILED: {e}")
655
+ raise gr.Error(f"Auto-rigging failed: {e}") from e
656
 
657
 
658
  # ---------------------------------------------------------------------------
 
696
  sources=["upload", "clipboard"],
697
  height=320,
698
  )
699
+ status = gr.Textbox(
700
+ label="Status",
701
+ value="Upload an image, then click Generate.",
702
+ interactive=False,
703
+ lines=2,
704
+ )
705
+ task_progress = gr.HTML(
706
+ label="Task progress",
707
+ value=_render_steps(_init_steps(GEN_STEP_NAMES)),
708
+ )
709
  generate_btn = gr.Button("1️⃣ Generate 3D Model", variant="primary")
710
  rig_btn = gr.Button("2️⃣ Auto-Rig Model", variant="primary", interactive=False)
711
 
 
769
  demo.load(start_session)
770
  demo.unload(end_session)
771
 
772
+ def _reset_rig_progress():
773
+ return _render_steps(_init_steps(RIG_STEP_NAMES))
 
 
 
774
 
775
  generate_btn.click(
776
  get_seed,
777
  inputs=[randomize_seed, seed],
778
  outputs=[seed],
779
+ show_progress="hidden",
780
+ ).then(
781
+ preprocess_with_progress,
782
+ inputs=[image_prompt],
783
+ outputs=[image_prompt, task_progress, status],
784
+ show_progress="minimal",
785
  ).then(
786
  generate_3d,
787
  inputs=[image_prompt, seed, resolution, decimation_target, texture_size],
788
+ outputs=[glb_state, model_output, download_glb, task_progress, status],
789
+ show_progress="minimal",
790
  ).then(
791
+ lambda: gr.update(interactive=True),
792
+ outputs=[rig_btn],
793
+ show_progress="hidden",
 
 
 
 
794
  )
795
 
796
  rig_btn.click(
797
+ _reset_rig_progress,
798
+ outputs=[task_progress],
799
+ show_progress="hidden",
800
+ ).then(
801
  rig_3d,
802
  inputs=[glb_state, num_beams, top_k, top_p, temperature, repetition_penalty],
803
+ outputs=[rigged_output, download_rigged_glb, download_rigged_fbx, task_progress, status],
804
+ show_progress="minimal",
 
 
 
 
 
 
805
  )
806
 
807
+ # Required for loading spinners + ZeroGPU job queue on Spaces.
808
+ demo.queue(default_concurrency_limit=1)
809
+
810
 
811
  # ---------------------------------------------------------------------------
812
  # Module-scope model load (ZeroGPU packs CUDA weights at boot, streams them
 
825
 
826
 
827
  if __name__ == "__main__":
828
+ demo.launch(show_error=True, ssr_mode=False)