xinjjj commited on
Commit
c810416
·
1 Parent(s): bc86a46
Files changed (1) hide show
  1. app.py +282 -39
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/assets"
66
  elif RUNNING_MODE == "hf_remote":
67
- from huggingface_hub import snapshot_download
68
-
69
  snapshot_download(
70
- repo_id="HorizonRobotics/EmbodiedGenData",
71
  repo_type="dataset",
72
- allow_patterns=f"dataset/**",
73
- local_dir="EmbodiedGenData",
 
 
 
74
  local_dir_use_symlinks=False,
75
  )
76
- DATA_ROOT = "EmbodiedGenData/dataset"
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
- collision_unique = (
431
- _unique_path(collision_path, session_hash, "collision")
432
- if collision_path
433
- else None
434
- )
 
 
 
 
435
 
436
  if switch_viewer:
437
  yield (
438
- gr.update(value=visual_unique),
439
  gr.update(value=None, visible=False),
440
- gr.update(value=collision_unique, visible=True),
 
 
 
 
441
  False,
442
  )
443
  else:
444
  yield (
445
- gr.update(value=visual_unique),
446
- gr.update(value=collision_unique, visible=True),
 
 
 
 
447
  gr.update(value=None, visible=False),
448
  True,
449
  )
450
 
451
 
452
- def create_asset_zip(asset_dir: str, req: gr.Request):
453
- user_dir = os.path.join(TMP_DIR, str(req.session_hash))
454
- os.makedirs(user_dir, exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- archive_path = shutil.make_archive(
460
- base_name=zip_path_base, format='zip', root_dir=asset_dir
461
- )
462
- gr.Info(f"✅ {asset_folder_name}.zip is ready and can be downloaded.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
 
464
- return archive_path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # gr.HTML(lighting_css, visible=False)
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=500,
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=500,
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=500,
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
- extract_btn = gr.Button(
637
- "📥 Extract Asset",
 
 
 
 
 
638
  variant="primary",
639
  interactive=False,
640
  )
641
- download_btn = gr.DownloadButton(
642
- label="⬇️ Download Asset",
643
  variant="primary",
644
  interactive=False,
645
  )
@@ -720,13 +953,23 @@ with gr.Blocks(
720
  switch_viewer_state,
721
  ],
722
  ).success(
723
- lambda: (gr.Button(interactive=True), gr.Button(interactive=False)),
724
- outputs=[extract_btn, download_btn],
 
 
 
 
725
  )
726
 
727
- extract_btn.click(
728
- fn=create_asset_zip, inputs=[asset_folder], outputs=[download_btn]
729
- ).success(fn=lambda: gr.update(interactive=True), outputs=download_btn)
 
 
 
 
 
 
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)