linjohnss commited on
Commit
0f856c7
·
1 Parent(s): 77b1eb5

Add example

Browse files
app.py CHANGED
@@ -71,6 +71,30 @@ BENCH_WIDTHS = [int(x) for x in os.environ.get("BENCH_WIDTHS", "512 768").split(
71
  ARTIFACTS_DIR = Path(tempfile.gettempdir()) / "dvsm_artifacts"
72
  ARTIFACTS_DIR.mkdir(parents=True, exist_ok=True)
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  # ---------------------------------------------------------------------------
75
  # Module-level model load: download + keep on CPU (ZeroGPU convention).
76
  # Real `.cuda()` happens inside @spaces.GPU workers.
@@ -150,9 +174,14 @@ class _SessionArgs:
150
  @spaces.GPU(duration=SESSION_SECONDS)
151
  def _live_session_worker(artifact_path: str, port: int):
152
  """Generator running in the GPU worker. Builds the renderer, starts a viser
153
- server on `port`, then holds the GPU (yielding heartbeats) until the session
154
- budget expires. Each yield is the integer seconds remaining; the first yield
155
- signals the viser server is up and serving."""
 
 
 
 
 
156
  import torch
157
  import viser
158
  from dvsm.models.pose_preprocessor import PosePreprocessor
@@ -160,6 +189,7 @@ def _live_session_worker(artifact_path: str, port: int):
160
  from viser_app.viewer import install_viser_handlers
161
  from viser_app.viewer_optim import install_optimizations
162
 
 
163
  model = DVSM_MODEL_CPU.cuda().eval()
164
  artifact = torch.load(artifact_path, weights_only=False, map_location="cpu")
165
  renderer = build_renderer(model, artifact)
@@ -172,24 +202,25 @@ def _live_session_worker(artifact_path: str, port: int):
172
  renderer.pose_preprocessor = None
173
  pp_scale = float(pp_cpu.s.reshape(-1)[0])
174
 
 
175
  renderer = install_optimizations(
176
  renderer, use_fast_plucker=True, use_cuda_graph=True,
177
  n_warmup=5, max_graphs=len(BENCH_WIDTHS) + 2,
178
  )
179
 
 
180
  server = viser.ViserServer(host="0.0.0.0", port=port)
181
  server.scene.set_up_direction("+y")
182
  install_viser_handlers(server, renderer, scene_meta, pp_cpu, pp_scale,
183
  model, _SessionArgs())
184
 
185
  deadline = time.monotonic() + SESSION_SECONDS - 5 # margin before hard kill
186
- first = True
187
  while True:
188
  remaining = int(deadline - time.monotonic())
189
  if remaining <= 0:
190
  break
191
- yield remaining if not first else 0 # first yield = "ready"
192
- first = False
193
  time.sleep(1.0)
194
  try:
195
  server.stop()
@@ -232,6 +263,21 @@ def _viewer_message(msg: str, color: str = "var(--body-text-color-subdued)") ->
232
  )
233
 
234
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  # ---------------------------------------------------------------------------
236
  # Gradio callbacks (main process)
237
  # ---------------------------------------------------------------------------
@@ -339,13 +385,15 @@ def live_session(artifact_path):
339
  _state["viser_port"] = port
340
  _state["session_alive"] = True
341
  try:
342
- first = True
343
- for remaining in _live_session_worker(artifact_path, port):
344
- if first:
345
- first = False
 
 
346
  yield (_iframe_html(port), _session_bar(SESSION_SECONDS, SESSION_SECONDS))
347
- else:
348
- yield (gr.update(), _session_bar(int(remaining), SESSION_SECONDS))
349
  finally:
350
  _state["session_alive"] = False
351
  _state["viser_port"] = None
@@ -489,6 +537,11 @@ input[type='range']::-moz-range-thumb { background: #76B900 !important; }
489
  sit flush at the column top (kept aligned) and the viewer fills its frame
490
  edge-to-edge (no padding gap around the iframe). */
491
  .flush-block { padding: 0 !important; }
 
 
 
 
 
492
  """
493
 
494
  with gr.Blocks(title="TTB-DVSM (ZeroGPU)") as demo:
@@ -533,7 +586,7 @@ with gr.Blocks(title="TTB-DVSM (ZeroGPU)") as demo:
533
  )
534
  # Below the text: 1 (inputs + buttons) : 3 (viser viewer).
535
  with gr.Row():
536
- with gr.Column(scale=1, min_width=320):
537
  inputs = gr.File(
538
  file_count="multiple",
539
  file_types=["image", "video", ".mp4", ".mov", ".mkv",
@@ -548,7 +601,19 @@ with gr.Blocks(title="TTB-DVSM (ZeroGPU)") as demo:
548
  recon_btn = gr.Button("Reconstruct", variant="primary")
549
  session_btn = gr.Button("Start / Continue Live Session",
550
  variant="primary", interactive=False)
551
- with gr.Column(scale=3):
 
 
 
 
 
 
 
 
 
 
 
 
552
  # Viewer first so its top edge aligns with the upload box on the
553
  # left (both flush via .flush-block). Recon progress/status overlay
554
  # the viewer box itself (see reconstruct_cb); only the live-session
@@ -568,6 +633,22 @@ with gr.Blocks(title="TTB-DVSM (ZeroGPU)") as demo:
568
  [viewer_html, session_status],
569
  show_progress="hidden")
570
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
 
572
  # ---------------------------------------------------------------------------
573
  # Launch Gradio (which starts uvicorn) and attach the viser reverse-proxy routes
 
71
  ARTIFACTS_DIR = Path(tempfile.gettempdir()) / "dvsm_artifacts"
72
  ARTIFACTS_DIR.mkdir(parents=True, exist_ok=True)
73
 
74
+ # Pre-built example scenes are auto-discovered from examples/: every
75
+ # "<stem>.recon.pt" (baked with THIS model) that has a matching "<stem>.jpg|png"
76
+ # thumbnail becomes a gallery entry. Drop files in / remove them to change the
77
+ # gallery — no code edit needed. Clicking a thumbnail skips reconstruction and
78
+ # opens the live session directly on the artifact.
79
+ EXAMPLES_DIR = PROJECT_ROOT / "examples"
80
+
81
+
82
+ def _discover_examples():
83
+ """Every "<stem>.recon.pt" with a matching "<stem>.jpg|jpeg|png|webp"
84
+ thumbnail. The gallery shows thumbnails only (no labels)."""
85
+ scenes = []
86
+ for recon in sorted(EXAMPLES_DIR.glob("*.recon.pt")):
87
+ stem = recon.name[: -len(".recon.pt")]
88
+ thumb = next((EXAMPLES_DIR / f"{stem}{ext}"
89
+ for ext in (".jpg", ".jpeg", ".png", ".webp")
90
+ if (EXAMPLES_DIR / f"{stem}{ext}").exists()), None)
91
+ if thumb is not None:
92
+ scenes.append({"thumb": thumb, "recon": recon})
93
+ return scenes
94
+
95
+
96
+ EXAMPLE_SCENES = _discover_examples()
97
+
98
  # ---------------------------------------------------------------------------
99
  # Module-level model load: download + keep on CPU (ZeroGPU convention).
100
  # Real `.cuda()` happens inside @spaces.GPU workers.
 
174
  @spaces.GPU(duration=SESSION_SECONDS)
175
  def _live_session_worker(artifact_path: str, port: int):
176
  """Generator running in the GPU worker. Builds the renderer, starts a viser
177
+ server on `port`, then holds the GPU until the session budget expires.
178
+
179
+ Yields tagged tuples so the UI can show a prep progress bar then a live
180
+ countdown:
181
+ ("prep", frac, desc) during setup
182
+ ("ready", port, None) viser is up and serving
183
+ ("alive", remaining, None) per-second heartbeat while holding the GPU
184
+ """
185
  import torch
186
  import viser
187
  from dvsm.models.pose_preprocessor import PosePreprocessor
 
189
  from viser_app.viewer import install_viser_handlers
190
  from viser_app.viewer_optim import install_optimizations
191
 
192
+ yield ("prep", 0.15, "Loading scene…")
193
  model = DVSM_MODEL_CPU.cuda().eval()
194
  artifact = torch.load(artifact_path, weights_only=False, map_location="cpu")
195
  renderer = build_renderer(model, artifact)
 
202
  renderer.pose_preprocessor = None
203
  pp_scale = float(pp_cpu.s.reshape(-1)[0])
204
 
205
+ yield ("prep", 0.4, "Loading scene…")
206
  renderer = install_optimizations(
207
  renderer, use_fast_plucker=True, use_cuda_graph=True,
208
  n_warmup=5, max_graphs=len(BENCH_WIDTHS) + 2,
209
  )
210
 
211
+ yield ("prep", 0.6, "Loading scene…")
212
  server = viser.ViserServer(host="0.0.0.0", port=port)
213
  server.scene.set_up_direction("+y")
214
  install_viser_handlers(server, renderer, scene_meta, pp_cpu, pp_scale,
215
  model, _SessionArgs())
216
 
217
  deadline = time.monotonic() + SESSION_SECONDS - 5 # margin before hard kill
218
+ yield ("ready", port, None)
219
  while True:
220
  remaining = int(deadline - time.monotonic())
221
  if remaining <= 0:
222
  break
223
+ yield ("alive", remaining, None)
 
224
  time.sleep(1.0)
225
  try:
226
  server.stop()
 
263
  )
264
 
265
 
266
+ def _prep_overlay(frac: float, desc: str) -> str:
267
+ """Determinate green progress bar centered in the viewer box, shown while
268
+ the viser session is being prepared (renderer build + warmup + bench)."""
269
+ pct = max(0, min(100, round(100 * frac)))
270
+ return (
271
+ "<div style='width:100%;aspect-ratio:16/9;overflow:hidden;display:flex;"
272
+ "flex-direction:column;align-items:center;justify-content:center;gap:10px;"
273
+ "padding:0 12%;color:var(--body-text-color-subdued)'>"
274
+ f"<div style='font-weight:600;font-size:1.05em'>{desc}</div>"
275
+ "<div style='width:100%;height:8px;background:#333;border-radius:4px;overflow:hidden'>"
276
+ f"<div style='height:100%;width:{pct}%;background:#76B900;"
277
+ "transition:width .3s linear'></div></div></div>"
278
+ )
279
+
280
+
281
  # ---------------------------------------------------------------------------
282
  # Gradio callbacks (main process)
283
  # ---------------------------------------------------------------------------
 
385
  _state["viser_port"] = port
386
  _state["session_alive"] = True
387
  try:
388
+ for tag, a, _b in _live_session_worker(artifact_path, port):
389
+ if tag == "prep":
390
+ # a = fraction, _b = description -> prep bar overlaid in the viewer
391
+ yield (_prep_overlay(a, _b), gr.update())
392
+ elif tag == "ready":
393
+ # viser is up -> swap in the iframe + start the countdown bar
394
  yield (_iframe_html(port), _session_bar(SESSION_SECONDS, SESSION_SECONDS))
395
+ else: # "alive": a = seconds remaining
396
+ yield (gr.update(), _session_bar(int(a), SESSION_SECONDS))
397
  finally:
398
  _state["session_alive"] = False
399
  _state["viser_port"] = None
 
537
  sit flush at the column top (kept aligned) and the viewer fills its frame
538
  edge-to-edge (no padding gap around the iframe). */
539
  .flush-block { padding: 0 !important; }
540
+ /* Proportional columns: flex-basis 0 so the row width is always split 1:3 and
541
+ BOTH columns scale with the window (default Gradio scale only distributes the
542
+ leftover space, leaving the left column looking fixed). */
543
+ .col-left { flex: 1 1 0 !important; min-width: 260px; }
544
+ .col-right { flex: 3 1 0 !important; min-width: 360px; }
545
  """
546
 
547
  with gr.Blocks(title="TTB-DVSM (ZeroGPU)") as demo:
 
586
  )
587
  # Below the text: 1 (inputs + buttons) : 3 (viser viewer).
588
  with gr.Row():
589
+ with gr.Column(scale=1, min_width=260, elem_classes=["col-left"]):
590
  inputs = gr.File(
591
  file_count="multiple",
592
  file_types=["image", "video", ".mp4", ".mov", ".mkv",
 
601
  recon_btn = gr.Button("Reconstruct", variant="primary")
602
  session_btn = gr.Button("Start / Continue Live Session",
603
  variant="primary", interactive=False)
604
+ if EXAMPLE_SCENES:
605
+ gr.Markdown("### Example")
606
+ example_gallery = gr.Gallery(
607
+ # No captions. Fixed box height -> vertical scrollbar when
608
+ # there are more thumbnails than fit; width still scales with
609
+ # the (resizable) column since it's 3 columns.
610
+ value=[str(s["thumb"]) for s in EXAMPLE_SCENES],
611
+ columns=3, height=200, object_fit="cover",
612
+ show_label=False, allow_preview=False,
613
+ )
614
+ else:
615
+ example_gallery = None
616
+ with gr.Column(scale=3, elem_classes=["col-right"]):
617
  # Viewer first so its top edge aligns with the upload box on the
618
  # left (both flush via .flush-block). Recon progress/status overlay
619
  # the viewer box itself (see reconstruct_cb); only the live-session
 
633
  [viewer_html, session_status],
634
  show_progress="hidden")
635
 
636
+ # Clicking an example thumbnail loads its prebuilt artifact and opens the
637
+ # live session directly (no reconstruction).
638
+ if example_gallery is not None:
639
+ def _on_example_select(evt: gr.SelectData):
640
+ idx = int(evt.index)
641
+ if 0 <= idx < len(EXAMPLE_SCENES):
642
+ return str(EXAMPLE_SCENES[idx]["recon"]), gr.update(interactive=True)
643
+ return "", gr.update(interactive=False)
644
+
645
+ example_gallery.select(
646
+ _on_example_select, None, [artifact_state, session_btn],
647
+ ).then(
648
+ live_session, [artifact_state], [viewer_html, session_status],
649
+ show_progress="hidden",
650
+ )
651
+
652
 
653
  # ---------------------------------------------------------------------------
654
  # Launch Gradio (which starts uvicorn) and attach the viser reverse-proxy routes
examples/dl3dv_scene139.jpg ADDED
examples/dl3dv_scene9.jpg ADDED
examples/mipnerf360_garden.jpg ADDED
examples/mipnerf360_kitchen.jpg ADDED
examples/tandt_truck.jpg ADDED
viser_app/recon_scene.py CHANGED
@@ -210,14 +210,17 @@ def reconstruct(ckpt_root: Path | None, ckpt_name: str, scene_data: dict,
210
 
211
 
212
  def _move_kv_to_cpu(kv):
 
 
 
213
  if kv is None:
214
  return None
 
 
215
  if isinstance(kv, dict):
216
- return {k: (v.cpu() if isinstance(v, torch.Tensor) else v) for k, v in kv.items()}
217
  if isinstance(kv, (tuple, list)):
218
  return type(kv)(_move_kv_to_cpu(v) for v in kv)
219
- if isinstance(kv, torch.Tensor):
220
- return kv.cpu()
221
  return kv
222
 
223
 
 
210
 
211
 
212
  def _move_kv_to_cpu(kv):
213
+ """Move baked fast-weights to CPU, storing floating tensors as fp16 to halve
214
+ the artifact size on disk (upcast back to fp32 at load time, see
215
+ render._kv_to_cuda)."""
216
  if kv is None:
217
  return None
218
+ if isinstance(kv, torch.Tensor):
219
+ return kv.half().cpu() if kv.is_floating_point() else kv.cpu()
220
  if isinstance(kv, dict):
221
+ return {k: _move_kv_to_cpu(v) for k, v in kv.items()}
222
  if isinstance(kv, (tuple, list)):
223
  return type(kv)(_move_kv_to_cpu(v) for v in kv)
 
 
224
  return kv
225
 
226
 
viser_app/render.py CHANGED
@@ -68,14 +68,16 @@ def load_model(ckpt_root: Path | None = None, ckpt_name: str = "latest.pt",
68
 
69
 
70
  def _kv_to_cuda(kv):
 
 
71
  if kv is None:
72
  return None
 
 
73
  if isinstance(kv, dict):
74
- return {k: (v.cuda() if isinstance(v, torch.Tensor) else v) for k, v in kv.items()}
75
  if isinstance(kv, (tuple, list)):
76
  return type(kv)(_kv_to_cuda(v) for v in kv)
77
- if isinstance(kv, torch.Tensor):
78
- return kv.cuda()
79
  return kv
80
 
81
 
 
68
 
69
 
70
  def _kv_to_cuda(kv):
71
+ """Move baked fast-weights to CUDA, upcasting fp16 (how they're stored on
72
+ disk, see recon_scene._move_kv_to_cpu) back to fp32 for rendering."""
73
  if kv is None:
74
  return None
75
+ if isinstance(kv, torch.Tensor):
76
+ return (kv.float() if kv.dtype == torch.float16 else kv).cuda()
77
  if isinstance(kv, dict):
78
+ return {k: _kv_to_cuda(v) for k, v in kv.items()}
79
  if isinstance(kv, (tuple, list)):
80
  return type(kv)(_kv_to_cuda(v) for v in kv)
 
 
81
  return kv
82
 
83