Spaces:
Running on Zero
Running on Zero
update
Browse files
app.py
CHANGED
|
@@ -23,13 +23,17 @@ gradio_tmp_dir = os.path.join(
|
|
| 23 |
os.makedirs(gradio_tmp_dir, exist_ok=True)
|
| 24 |
os.environ["GRADIO_TEMP_DIR"] = gradio_tmp_dir
|
| 25 |
|
|
|
|
| 26 |
import shutil
|
| 27 |
import uuid
|
| 28 |
import xml.etree.ElementTree as ET
|
|
|
|
| 29 |
from pathlib import Path
|
| 30 |
|
| 31 |
import gradio as gr
|
|
|
|
| 32 |
import pandas as pd
|
|
|
|
| 33 |
from app_style import custom_theme, lighting_css
|
| 34 |
from embodied_gen.utils.tags import VERSION
|
| 35 |
|
|
@@ -49,9 +53,16 @@ except Exception as e:
|
|
| 49 |
# --- Configuration & Data Loading ---
|
| 50 |
RUNNING_MODE = "hf_remote" # local or hf_remote
|
| 51 |
CSV_FILE = "dataset_index.csv"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
# Compatible with huggingface space zero GPU
|
| 54 |
import spaces
|
|
|
|
| 55 |
|
| 56 |
|
| 57 |
@spaces.GPU
|
|
@@ -62,23 +73,47 @@ def fake_gpu_init():
|
|
| 62 |
fake_gpu_init()
|
| 63 |
|
| 64 |
if RUNNING_MODE == "local":
|
| 65 |
-
DATA_ROOT = "/horizon-bucket/robot_lab/datasets/embodiedgen/
|
| 66 |
elif RUNNING_MODE == "hf_remote":
|
| 67 |
-
|
| 68 |
-
|
| 69 |
snapshot_download(
|
| 70 |
-
repo_id=
|
| 71 |
repo_type="dataset",
|
| 72 |
-
allow_patterns=
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
| 74 |
local_dir_use_symlinks=False,
|
| 75 |
)
|
| 76 |
-
DATA_ROOT = "
|
| 77 |
else:
|
| 78 |
raise ValueError(
|
| 79 |
f"Unknown RUNNING_MODE: {RUNNING_MODE}, must be 'local' or 'hf_remote'."
|
| 80 |
)
|
| 81 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
csv_path = os.path.join(DATA_ROOT, CSV_FILE)
|
| 83 |
df = pd.read_csv(csv_path)
|
| 84 |
TMP_DIR = os.path.join(
|
|
@@ -131,6 +166,82 @@ def _unique_path(
|
|
| 131 |
return str(dst)
|
| 132 |
|
| 133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
# --- Helper Functions (data filtering) ---
|
| 135 |
def get_primary_categories():
|
| 136 |
return sorted(df["primary_category"].dropna().unique())
|
|
@@ -356,6 +467,18 @@ def show_asset_from_gallery(
|
|
| 356 |
)
|
| 357 |
|
| 358 |
row = subset.iloc[index]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
visual_path, collision_path, desc = _extract_mesh_paths(row)
|
| 360 |
|
| 361 |
urdf_path = os.path.join(DATA_ROOT, row["urdf_path"])
|
|
@@ -427,41 +550,146 @@ def render_meshes(
|
|
| 427 |
if visual_path
|
| 428 |
else None
|
| 429 |
)
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
|
| 436 |
if switch_viewer:
|
| 437 |
yield (
|
| 438 |
-
gr.update(value=visual_unique),
|
| 439 |
gr.update(value=None, visible=False),
|
| 440 |
-
gr.update(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
False,
|
| 442 |
)
|
| 443 |
else:
|
| 444 |
yield (
|
| 445 |
-
gr.update(value=visual_unique),
|
| 446 |
-
gr.update(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
gr.update(value=None, visible=False),
|
| 448 |
True,
|
| 449 |
)
|
| 450 |
|
| 451 |
|
| 452 |
-
def
|
| 453 |
-
|
| 454 |
-
os.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
|
| 456 |
-
asset_folder_name = os.path.basename(os.path.normpath(asset_dir))
|
| 457 |
-
zip_path_base = os.path.join(user_dir, asset_folder_name)
|
| 458 |
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
)
|
| 462 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 463 |
|
| 464 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 465 |
|
| 466 |
|
| 467 |
def start_session(req: gr.Request) -> None:
|
|
@@ -481,7 +709,7 @@ with gr.Blocks(
|
|
| 481 |
css=css,
|
| 482 |
title="3D Asset Library",
|
| 483 |
) as demo:
|
| 484 |
-
|
| 485 |
gr.Markdown(
|
| 486 |
"""
|
| 487 |
## 🏛️ ***EmbodiedGen***: 3D Asset Gallery Explorer
|
|
@@ -582,21 +810,21 @@ with gr.Blocks(
|
|
| 582 |
with gr.TabItem("Visual Mesh") as t1:
|
| 583 |
viewer = gr.Model3D(
|
| 584 |
label="🧊 3D Model Viewer",
|
| 585 |
-
height=
|
| 586 |
clear_color=[0.95, 0.95, 0.95],
|
| 587 |
elem_id="visual_mesh",
|
| 588 |
)
|
| 589 |
with gr.TabItem("Collision Mesh") as t2:
|
| 590 |
collision_viewer_a = gr.Model3D(
|
| 591 |
label="🧊 Collision Mesh",
|
| 592 |
-
height=
|
| 593 |
clear_color=[0.95, 0.95, 0.95],
|
| 594 |
elem_id="collision_mesh_a",
|
| 595 |
visible=True,
|
| 596 |
)
|
| 597 |
collision_viewer_b = gr.Model3D(
|
| 598 |
label="🧊 Collision Mesh",
|
| 599 |
-
height=
|
| 600 |
clear_color=[0.95, 0.95, 0.95],
|
| 601 |
elem_id="collision_mesh_b",
|
| 602 |
visible=False,
|
|
@@ -633,13 +861,18 @@ with gr.Blocks(
|
|
| 633 |
label="URDF File Path", interactive=False, lines=2
|
| 634 |
)
|
| 635 |
with gr.Row():
|
| 636 |
-
|
| 637 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 638 |
variant="primary",
|
| 639 |
interactive=False,
|
| 640 |
)
|
| 641 |
-
|
| 642 |
-
label="⬇️ Download
|
| 643 |
variant="primary",
|
| 644 |
interactive=False,
|
| 645 |
)
|
|
@@ -720,13 +953,23 @@ with gr.Blocks(
|
|
| 720 |
switch_viewer_state,
|
| 721 |
],
|
| 722 |
).success(
|
| 723 |
-
lambda: (
|
| 724 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 725 |
)
|
| 726 |
|
| 727 |
-
|
| 728 |
-
fn=
|
| 729 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 730 |
|
| 731 |
demo.load(start_session)
|
| 732 |
demo.unload(end_session)
|
|
|
|
| 23 |
os.makedirs(gradio_tmp_dir, exist_ok=True)
|
| 24 |
os.environ["GRADIO_TEMP_DIR"] = gradio_tmp_dir
|
| 25 |
|
| 26 |
+
import colorsys
|
| 27 |
import shutil
|
| 28 |
import uuid
|
| 29 |
import xml.etree.ElementTree as ET
|
| 30 |
+
import zipfile
|
| 31 |
from pathlib import Path
|
| 32 |
|
| 33 |
import gradio as gr
|
| 34 |
+
import numpy as np
|
| 35 |
import pandas as pd
|
| 36 |
+
import trimesh
|
| 37 |
from app_style import custom_theme, lighting_css
|
| 38 |
from embodied_gen.utils.tags import VERSION
|
| 39 |
|
|
|
|
| 53 |
# --- Configuration & Data Loading ---
|
| 54 |
RUNNING_MODE = "hf_remote" # local or hf_remote
|
| 55 |
CSV_FILE = "dataset_index.csv"
|
| 56 |
+
HF_REPO_ID = "HorizonRobotics/EmbodiedGenData"
|
| 57 |
+
HF_LOCAL_DIR = "EmbodiedGenData"
|
| 58 |
+
# Camera distance as a multiple of each mesh's bounding radius. Meshes are in
|
| 59 |
+
# real-world meters (sizes vary widely), so the camera radius must scale per
|
| 60 |
+
# asset. Larger -> camera further away -> object appears smaller on load.
|
| 61 |
+
CAMERA_ZOOM = 3.2
|
| 62 |
|
| 63 |
# Compatible with huggingface space zero GPU
|
| 64 |
import spaces
|
| 65 |
+
from huggingface_hub import snapshot_download
|
| 66 |
|
| 67 |
|
| 68 |
@spaces.GPU
|
|
|
|
| 73 |
fake_gpu_init()
|
| 74 |
|
| 75 |
if RUNNING_MODE == "local":
|
| 76 |
+
DATA_ROOT = "/horizon-bucket/robot_lab/datasets/embodiedgen/assets_v2"
|
| 77 |
elif RUNNING_MODE == "hf_remote":
|
| 78 |
+
# Only fetch the index and preview videos up front; per-asset mesh/urdf/
|
| 79 |
+
# usd/mjcf files are pulled lazily on demand (see `ensure_hf_files`).
|
| 80 |
snapshot_download(
|
| 81 |
+
repo_id=HF_REPO_ID,
|
| 82 |
repo_type="dataset",
|
| 83 |
+
allow_patterns=[
|
| 84 |
+
f"dataset/{CSV_FILE}",
|
| 85 |
+
"dataset/**/*.mp4",
|
| 86 |
+
],
|
| 87 |
+
local_dir=HF_LOCAL_DIR,
|
| 88 |
local_dir_use_symlinks=False,
|
| 89 |
)
|
| 90 |
+
DATA_ROOT = os.path.join(HF_LOCAL_DIR, "dataset")
|
| 91 |
else:
|
| 92 |
raise ValueError(
|
| 93 |
f"Unknown RUNNING_MODE: {RUNNING_MODE}, must be 'local' or 'hf_remote'."
|
| 94 |
)
|
| 95 |
|
| 96 |
+
|
| 97 |
+
def ensure_hf_files(rel_patterns: list[str]) -> None:
|
| 98 |
+
"""Lazily download files for one asset in hf_remote mode.
|
| 99 |
+
|
| 100 |
+
Args:
|
| 101 |
+
rel_patterns: Glob patterns relative to ``DATA_ROOT`` (the repo's
|
| 102 |
+
``dataset/`` folder), e.g. ``"cat/sub/uid/mesh/**"``.
|
| 103 |
+
|
| 104 |
+
No-op when running locally, where all files already exist on disk.
|
| 105 |
+
"""
|
| 106 |
+
if RUNNING_MODE != "hf_remote":
|
| 107 |
+
return
|
| 108 |
+
snapshot_download(
|
| 109 |
+
repo_id=HF_REPO_ID,
|
| 110 |
+
repo_type="dataset",
|
| 111 |
+
allow_patterns=[f"dataset/{p}" for p in rel_patterns],
|
| 112 |
+
local_dir=HF_LOCAL_DIR,
|
| 113 |
+
local_dir_use_symlinks=False,
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
csv_path = os.path.join(DATA_ROOT, CSV_FILE)
|
| 118 |
df = pd.read_csv(csv_path)
|
| 119 |
TMP_DIR = os.path.join(
|
|
|
|
| 166 |
return str(dst)
|
| 167 |
|
| 168 |
|
| 169 |
+
def _bounding_radius(mesh: trimesh.Trimesh | trimesh.Scene) -> float:
|
| 170 |
+
"""Radius of the mesh/scene bounding sphere (half the diagonal)."""
|
| 171 |
+
lo, hi = mesh.bounds
|
| 172 |
+
return float(np.linalg.norm(np.asarray(hi) - np.asarray(lo)) / 2)
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def _camera_position(radius: float | None) -> tuple:
|
| 176 |
+
"""Initial camera `(alpha, beta, radius)`; only push the distance."""
|
| 177 |
+
if not radius or radius <= 0:
|
| 178 |
+
return (None, None, None)
|
| 179 |
+
return (None, None, CAMERA_ZOOM * radius)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def _visual_radius(visual_path: str | None) -> float | None:
|
| 183 |
+
if not visual_path:
|
| 184 |
+
return None
|
| 185 |
+
return _bounding_radius(trimesh.load(visual_path, process=False))
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def _pastel_color(i: int) -> list[int]:
|
| 189 |
+
"""Distinct pastel RGBA for the i-th convex piece (golden-ratio hues)."""
|
| 190 |
+
h = (i * 0.6180339887498949) % 1.0
|
| 191 |
+
r, g, b = colorsys.hsv_to_rgb(h, 0.45, 0.98)
|
| 192 |
+
return [int(r * 255), int(g * 255), int(b * 255), 255]
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def _colored_collision_path(
|
| 196 |
+
collision_path: str | None, session_hash: str
|
| 197 |
+
) -> tuple[str | None, float | None]:
|
| 198 |
+
"""Export the collision mesh as a GLB with one color per convex piece.
|
| 199 |
+
|
| 200 |
+
The convex-decomposition pieces are stored as separate objects inside the
|
| 201 |
+
single collision ``.obj``; they are recovered via connected-component split
|
| 202 |
+
and each is tinted a distinct pastel color for visualization.
|
| 203 |
+
|
| 204 |
+
Returns ``(glb_path, bounding_radius)`` for camera framing.
|
| 205 |
+
"""
|
| 206 |
+
if not collision_path:
|
| 207 |
+
return None, None
|
| 208 |
+
|
| 209 |
+
tmp_root = (
|
| 210 |
+
Path(os.environ.get("GRADIO_TEMP_DIR", "/tmp"))
|
| 211 |
+
/ "model3d-cache"
|
| 212 |
+
/ session_hash
|
| 213 |
+
)
|
| 214 |
+
tmp_root.mkdir(parents=True, exist_ok=True)
|
| 215 |
+
|
| 216 |
+
# rolling cleanup for same kind
|
| 217 |
+
prev = _prev_temp.get(session_hash, {})
|
| 218 |
+
old = prev.get("collision")
|
| 219 |
+
if old and old.exists():
|
| 220 |
+
old.unlink()
|
| 221 |
+
|
| 222 |
+
loaded = trimesh.load(collision_path, process=False)
|
| 223 |
+
if isinstance(loaded, trimesh.Scene):
|
| 224 |
+
parts = list(loaded.geometry.values())
|
| 225 |
+
else:
|
| 226 |
+
parts = loaded.split(only_watertight=False)
|
| 227 |
+
if len(parts) == 0:
|
| 228 |
+
parts = [loaded]
|
| 229 |
+
|
| 230 |
+
scene = trimesh.Scene()
|
| 231 |
+
for i, part in enumerate(parts):
|
| 232 |
+
part.visual = trimesh.visual.ColorVisuals(
|
| 233 |
+
mesh=part, vertex_colors=_pastel_color(i)
|
| 234 |
+
)
|
| 235 |
+
scene.add_geometry(part)
|
| 236 |
+
|
| 237 |
+
dst = tmp_root / f"collision-{uuid.uuid4().hex}.glb"
|
| 238 |
+
scene.export(str(dst))
|
| 239 |
+
|
| 240 |
+
prev["collision"] = dst
|
| 241 |
+
_prev_temp[session_hash] = prev
|
| 242 |
+
return str(dst), _bounding_radius(scene)
|
| 243 |
+
|
| 244 |
+
|
| 245 |
# --- Helper Functions (data filtering) ---
|
| 246 |
def get_primary_categories():
|
| 247 |
return sorted(df["primary_category"].dropna().unique())
|
|
|
|
| 467 |
)
|
| 468 |
|
| 469 |
row = subset.iloc[index]
|
| 470 |
+
|
| 471 |
+
# In hf_remote mode, pull only what the viewer needs for this asset.
|
| 472 |
+
rel_dir = row["asset_dir"]
|
| 473 |
+
urdf_name = os.path.basename(row["urdf_path"])
|
| 474 |
+
ensure_hf_files(
|
| 475 |
+
[
|
| 476 |
+
f"{rel_dir}/{urdf_name}",
|
| 477 |
+
f"{rel_dir}/mesh/**",
|
| 478 |
+
f"{rel_dir}/textures/**",
|
| 479 |
+
]
|
| 480 |
+
)
|
| 481 |
+
|
| 482 |
visual_path, collision_path, desc = _extract_mesh_paths(row)
|
| 483 |
|
| 484 |
urdf_path = os.path.join(DATA_ROOT, row["urdf_path"])
|
|
|
|
| 550 |
if visual_path
|
| 551 |
else None
|
| 552 |
)
|
| 553 |
+
visual_cam = _camera_position(_visual_radius(visual_path))
|
| 554 |
+
|
| 555 |
+
if collision_path:
|
| 556 |
+
collision_unique, collision_r = _colored_collision_path(
|
| 557 |
+
collision_path, session_hash
|
| 558 |
+
)
|
| 559 |
+
else:
|
| 560 |
+
collision_unique, collision_r = None, None
|
| 561 |
+
collision_cam = _camera_position(collision_r)
|
| 562 |
|
| 563 |
if switch_viewer:
|
| 564 |
yield (
|
| 565 |
+
gr.update(value=visual_unique, camera_position=visual_cam),
|
| 566 |
gr.update(value=None, visible=False),
|
| 567 |
+
gr.update(
|
| 568 |
+
value=collision_unique,
|
| 569 |
+
visible=True,
|
| 570 |
+
camera_position=collision_cam,
|
| 571 |
+
),
|
| 572 |
False,
|
| 573 |
)
|
| 574 |
else:
|
| 575 |
yield (
|
| 576 |
+
gr.update(value=visual_unique, camera_position=visual_cam),
|
| 577 |
+
gr.update(
|
| 578 |
+
value=collision_unique,
|
| 579 |
+
visible=True,
|
| 580 |
+
camera_position=collision_cam,
|
| 581 |
+
),
|
| 582 |
gr.update(value=None, visible=False),
|
| 583 |
True,
|
| 584 |
)
|
| 585 |
|
| 586 |
|
| 587 |
+
def _rel_dir(asset_dir: str) -> str:
|
| 588 |
+
"""Repo-relative asset dir (path under DATA_ROOT), for HF glob patterns."""
|
| 589 |
+
return os.path.relpath(asset_dir, DATA_ROOT)
|
| 590 |
+
|
| 591 |
+
|
| 592 |
+
def _find_urdf_stem(asset_dir: str) -> str | None:
|
| 593 |
+
for f in os.listdir(asset_dir):
|
| 594 |
+
if f.lower().endswith(".urdf"):
|
| 595 |
+
return os.path.splitext(f)[0]
|
| 596 |
+
return None
|
| 597 |
+
|
| 598 |
+
|
| 599 |
+
def _zip_items(zip_path: str, items: list[tuple[str, str]]) -> str:
|
| 600 |
+
"""Write files/dirs into a zip.
|
| 601 |
+
|
| 602 |
+
Args:
|
| 603 |
+
zip_path: Output archive path.
|
| 604 |
+
items: ``(src_abspath, arcname)`` pairs. Directories are walked and
|
| 605 |
+
stored under ``arcname``.
|
| 606 |
+
"""
|
| 607 |
+
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
| 608 |
+
for src, arcname in items:
|
| 609 |
+
if os.path.isdir(src):
|
| 610 |
+
for root_d, _, files in os.walk(src):
|
| 611 |
+
for fn in files:
|
| 612 |
+
fp = os.path.join(root_d, fn)
|
| 613 |
+
rel = os.path.relpath(fp, src)
|
| 614 |
+
zf.write(fp, os.path.join(arcname, rel))
|
| 615 |
+
elif os.path.isfile(src):
|
| 616 |
+
zf.write(src, arcname)
|
| 617 |
+
return zip_path
|
| 618 |
+
|
| 619 |
+
|
| 620 |
+
def _fmt_zip_path(
|
| 621 |
+
asset_dir: str, fmt: str, req: gr.Request
|
| 622 |
+
) -> tuple[str, str]:
|
| 623 |
+
"""Build the per-format output zip path named ``<uid>.zip``.
|
| 624 |
+
|
| 625 |
+
Returns ``(zip_path, uid)``. Each format gets its own subdir so the
|
| 626 |
+
downloaded filename stays ``<uid>.zip`` without colliding across formats.
|
| 627 |
+
"""
|
| 628 |
+
uid = os.path.basename(os.path.normpath(asset_dir))
|
| 629 |
+
fmt_dir = os.path.join(TMP_DIR, str(req.session_hash), fmt)
|
| 630 |
+
os.makedirs(fmt_dir, exist_ok=True)
|
| 631 |
+
return os.path.join(fmt_dir, f"{uid}.zip"), uid
|
| 632 |
+
|
| 633 |
+
|
| 634 |
+
def download_urdf(asset_dir: str, req: gr.Request) -> str | None:
|
| 635 |
+
"""Package the full asset folder (URDF + all referenced files) as-is."""
|
| 636 |
+
if not asset_dir or not os.path.isdir(asset_dir):
|
| 637 |
+
gr.Warning("Please select an asset first.")
|
| 638 |
+
return None
|
| 639 |
+
ensure_hf_files([f"{_rel_dir(asset_dir)}/**"])
|
| 640 |
+
zip_path, uid = _fmt_zip_path(asset_dir, "urdf", req)
|
| 641 |
+
_zip_items(zip_path, [(asset_dir, "")])
|
| 642 |
+
gr.Info(f"✅ {uid}.zip (URDF) is ready.")
|
| 643 |
+
return zip_path
|
| 644 |
|
|
|
|
|
|
|
| 645 |
|
| 646 |
+
def download_usd(asset_dir: str, req: gr.Request) -> str | None:
|
| 647 |
+
"""Package the ``.usd`` file together with its ``textures/`` folder."""
|
| 648 |
+
if not asset_dir or not os.path.isdir(asset_dir):
|
| 649 |
+
gr.Warning("Please select an asset first.")
|
| 650 |
+
return None
|
| 651 |
+
|
| 652 |
+
rel = _rel_dir(asset_dir)
|
| 653 |
+
stem = _find_urdf_stem(asset_dir)
|
| 654 |
+
if stem is not None:
|
| 655 |
+
ensure_hf_files([f"{rel}/{stem}.usd", f"{rel}/textures/**"])
|
| 656 |
+
else:
|
| 657 |
+
ensure_hf_files([f"{rel}/*.usd", f"{rel}/textures/**"])
|
| 658 |
+
|
| 659 |
+
items: list[tuple[str, str]] = []
|
| 660 |
+
for f in os.listdir(asset_dir):
|
| 661 |
+
if f.lower().endswith(".usd"):
|
| 662 |
+
items.append((os.path.join(asset_dir, f), f))
|
| 663 |
+
tex_dir = os.path.join(asset_dir, "textures")
|
| 664 |
+
if os.path.isdir(tex_dir):
|
| 665 |
+
items.append((tex_dir, "textures"))
|
| 666 |
+
|
| 667 |
+
if not any(s.lower().endswith(".usd") for s, _ in items):
|
| 668 |
+
gr.Warning("No USD file available for this asset.")
|
| 669 |
+
return None
|
| 670 |
|
| 671 |
+
zip_path, uid = _fmt_zip_path(asset_dir, "usd", req)
|
| 672 |
+
_zip_items(zip_path, items)
|
| 673 |
+
gr.Info(f"✅ {uid}.zip (USD) is ready.")
|
| 674 |
+
return zip_path
|
| 675 |
+
|
| 676 |
+
|
| 677 |
+
def download_mjcf(asset_dir: str, req: gr.Request) -> str | None:
|
| 678 |
+
"""Package the ``mjcf/`` folder."""
|
| 679 |
+
if not asset_dir or not os.path.isdir(asset_dir):
|
| 680 |
+
gr.Warning("Please select an asset first.")
|
| 681 |
+
return None
|
| 682 |
+
|
| 683 |
+
ensure_hf_files([f"{_rel_dir(asset_dir)}/mjcf/**"])
|
| 684 |
+
mjcf_dir = os.path.join(asset_dir, "mjcf")
|
| 685 |
+
if not os.path.isdir(mjcf_dir):
|
| 686 |
+
gr.Warning("No MJCF folder available for this asset.")
|
| 687 |
+
return None
|
| 688 |
+
|
| 689 |
+
zip_path, uid = _fmt_zip_path(asset_dir, "mjcf", req)
|
| 690 |
+
_zip_items(zip_path, [(mjcf_dir, "mjcf")])
|
| 691 |
+
gr.Info(f"✅ {uid}.zip (MJCF) is ready.")
|
| 692 |
+
return zip_path
|
| 693 |
|
| 694 |
|
| 695 |
def start_session(req: gr.Request) -> None:
|
|
|
|
| 709 |
css=css,
|
| 710 |
title="3D Asset Library",
|
| 711 |
) as demo:
|
| 712 |
+
gr.HTML(lighting_css, visible=False)
|
| 713 |
gr.Markdown(
|
| 714 |
"""
|
| 715 |
## 🏛️ ***EmbodiedGen***: 3D Asset Gallery Explorer
|
|
|
|
| 810 |
with gr.TabItem("Visual Mesh") as t1:
|
| 811 |
viewer = gr.Model3D(
|
| 812 |
label="🧊 3D Model Viewer",
|
| 813 |
+
height=380,
|
| 814 |
clear_color=[0.95, 0.95, 0.95],
|
| 815 |
elem_id="visual_mesh",
|
| 816 |
)
|
| 817 |
with gr.TabItem("Collision Mesh") as t2:
|
| 818 |
collision_viewer_a = gr.Model3D(
|
| 819 |
label="🧊 Collision Mesh",
|
| 820 |
+
height=380,
|
| 821 |
clear_color=[0.95, 0.95, 0.95],
|
| 822 |
elem_id="collision_mesh_a",
|
| 823 |
visible=True,
|
| 824 |
)
|
| 825 |
collision_viewer_b = gr.Model3D(
|
| 826 |
label="🧊 Collision Mesh",
|
| 827 |
+
height=380,
|
| 828 |
clear_color=[0.95, 0.95, 0.95],
|
| 829 |
elem_id="collision_mesh_b",
|
| 830 |
visible=False,
|
|
|
|
| 861 |
label="URDF File Path", interactive=False, lines=2
|
| 862 |
)
|
| 863 |
with gr.Row():
|
| 864 |
+
urdf_dl_btn = gr.DownloadButton(
|
| 865 |
+
label="⬇️ Download URDF",
|
| 866 |
+
variant="primary",
|
| 867 |
+
interactive=False,
|
| 868 |
+
)
|
| 869 |
+
usd_dl_btn = gr.DownloadButton(
|
| 870 |
+
label="⬇️ Download USD",
|
| 871 |
variant="primary",
|
| 872 |
interactive=False,
|
| 873 |
)
|
| 874 |
+
mjcf_dl_btn = gr.DownloadButton(
|
| 875 |
+
label="⬇️ Download MJCF",
|
| 876 |
variant="primary",
|
| 877 |
interactive=False,
|
| 878 |
)
|
|
|
|
| 953 |
switch_viewer_state,
|
| 954 |
],
|
| 955 |
).success(
|
| 956 |
+
lambda: (
|
| 957 |
+
gr.update(interactive=True),
|
| 958 |
+
gr.update(interactive=True),
|
| 959 |
+
gr.update(interactive=True),
|
| 960 |
+
),
|
| 961 |
+
outputs=[urdf_dl_btn, usd_dl_btn, mjcf_dl_btn],
|
| 962 |
)
|
| 963 |
|
| 964 |
+
urdf_dl_btn.click(
|
| 965 |
+
fn=download_urdf, inputs=[asset_folder], outputs=[urdf_dl_btn]
|
| 966 |
+
)
|
| 967 |
+
usd_dl_btn.click(
|
| 968 |
+
fn=download_usd, inputs=[asset_folder], outputs=[usd_dl_btn]
|
| 969 |
+
)
|
| 970 |
+
mjcf_dl_btn.click(
|
| 971 |
+
fn=download_mjcf, inputs=[asset_folder], outputs=[mjcf_dl_btn]
|
| 972 |
+
)
|
| 973 |
|
| 974 |
demo.load(start_session)
|
| 975 |
demo.unload(end_session)
|