Upload space/app.py with huggingface_hub
Browse files- 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=
|
| 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 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
if image is None:
|
| 257 |
raise gr.Error("Please upload an image first.")
|
| 258 |
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
image,
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 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 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
|
| 321 |
|
| 322 |
# ---------------------------------------------------------------------------
|
|
@@ -400,7 +532,11 @@ def load_rig_model():
|
|
| 400 |
print("[app] TokenRig model loaded.", flush=True)
|
| 401 |
|
| 402 |
|
| 403 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
)
|
|
|
|
| 414 |
if not glb_path or not os.path.exists(glb_path):
|
| 415 |
-
raise gr.Error("Generate
|
| 416 |
-
|
| 417 |
-
ensure_bpy_server_started()
|
| 418 |
-
load_rig_model()
|
| 419 |
|
| 420 |
-
|
| 421 |
-
"
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 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 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
|
|
|
|
|
|
| 464 |
)
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 481 |
)
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 595 |
-
|
| 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
|
| 610 |
-
|
| 611 |
-
|
| 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 |
-
|
| 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)
|