VISHAL18for4 commited on
Commit
88bbbe2
Β·
verified Β·
1 Parent(s): 38d999f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +86 -47
app.py CHANGED
@@ -4,6 +4,7 @@ import sys
4
  import os
5
  import base64
6
  import tempfile
 
7
  from PIL import Image
8
 
9
  # ── ZeroGPU optional ──
@@ -58,26 +59,37 @@ GPU_DURATION = 90
58
 
59
  @spaces.GPU(duration=GPU_DURATION)
60
  def run_sharp(image_path):
 
 
61
  if image_path is None:
62
  return None, "⚠ Please upload a photo first."
63
  out_dir = tempfile.mkdtemp(prefix="splat_")
64
  resized = None
65
  try:
66
  resized = resize_image(image_path, 512)
 
67
  result = subprocess.run(
68
  ["sharp", "predict", "-i", resized, "-o", out_dir],
69
  capture_output=True, text=True, timeout=GPU_DURATION - 15
70
  )
 
71
  ply_files = [f for f in os.listdir(out_dir) if f.endswith(".ply")]
72
  if not ply_files:
73
  err = (result.stderr or result.stdout or "No .ply produced.")[-800:]
 
74
  return None, f"SHARP failed:\n{err}"
75
- return os.path.join(out_dir, ply_files[0]), "βœ“ Done β€” 3D scene loading below ↓"
 
 
 
76
  except subprocess.TimeoutExpired:
 
77
  return None, "⚠ Timed out inside the GPU window. Try a smaller/simpler photo, or increase GPU_DURATION near the top of app.py if you have more quota (PRO/Team/Enterprise)."
78
  except FileNotFoundError:
 
79
  return None, "⚠ SHARP not found yet β€” wait 1 minute and try again."
80
  except Exception as e:
 
81
  return None, f"⚠ Error: {str(e)}"
82
  finally:
83
  if resized:
@@ -286,6 +298,17 @@ VIEWER_SHELL = """
286
  </div>
287
  </div>
288
 
 
 
 
 
 
 
 
 
 
 
 
289
  <style>@keyframes sw_spin { to { transform: rotate(360deg); } }</style>
290
 
291
  <script type="importmap">
@@ -300,13 +323,21 @@ VIEWER_SHELL = """
300
  <script type="module">
301
  import * as GaussianSplats3D from '@mkkellogg/gaussian-splats-3d';
302
 
303
- // Expose loader globally so Gradio's tiny trigger script can call it.
304
- // This version fetches the file as raw binary (no base64) β€” much faster
305
- // on mobile, since base64 inflates size ~33% AND forces a slow manual
306
- // byte-by-byte decode loop that can freeze the page for large models.
307
  window._swViewer = null;
308
  window._swBlobUrl = null;
309
 
 
 
 
 
 
 
 
 
 
 
 
310
  window.swLoadUrl = async function(fileUrl, sizeMb) {
311
  const idle = document.getElementById('sw-idle');
312
  const loading = document.getElementById('sw-loading');
@@ -316,6 +347,8 @@ window.swLoadUrl = async function(fileUrl, sizeMb) {
316
  const canvas = document.getElementById('sw-canvas');
317
  const msg = document.getElementById('sw-load-msg');
318
 
 
 
319
  idle.style.display = 'none';
320
  loading.style.display = 'flex';
321
  controls.style.display = 'none';
@@ -323,9 +356,13 @@ window.swLoadUrl = async function(fileUrl, sizeMb) {
323
 
324
  try {
325
  msg.textContent = 'DOWNLOADING SCENE…';
 
326
  const resp = await fetch(fileUrl);
327
- if (!resp.ok) throw new Error('fetch failed: ' + resp.status);
 
 
328
  const buf = await resp.arrayBuffer();
 
329
  const blobUrl = URL.createObjectURL(new Blob([buf], { type: 'application/octet-stream' }));
330
 
331
  if (window._swBlobUrl) { try { URL.revokeObjectURL(window._swBlobUrl); } catch(e) {} }
@@ -336,6 +373,7 @@ window.swLoadUrl = async function(fileUrl, sizeMb) {
336
  window._swViewer = null;
337
  }
338
 
 
339
  const viewer = new GaussianSplats3D.Viewer({
340
  canvas,
341
  cameraUp: [0, -1, 0],
@@ -346,7 +384,9 @@ window.swLoadUrl = async function(fileUrl, sizeMb) {
346
  window._swViewer = viewer;
347
 
348
  msg.textContent = 'RENDERING GAUSSIANS…';
 
349
  await viewer.addSplatScene(blobUrl, { progressiveLoad: true });
 
350
 
351
  loading.style.display = 'none';
352
  controls.style.display = 'flex';
@@ -354,7 +394,9 @@ window.swLoadUrl = async function(fileUrl, sizeMb) {
354
  viewer.start();
355
 
356
  } catch(err) {
357
- msg.textContent = '⚠ Viewer error β€” try Chrome or Firefox desktop.';
 
 
358
  console.error(err);
359
  }
360
  };
@@ -375,34 +417,6 @@ function swDownload() {
375
  """
376
 
377
 
378
- # ── Tiny trigger: waits for the hidden File component's link to appear/
379
- # update in the DOM, then hands its URL to the viewer. No file bytes ever
380
- # pass through Python string formatting or the Gradio websocket payload β€”
381
- # only a short bit of JS does, so this stays fast no matter how big the
382
- # .ply is.
383
- def make_load_trigger(size_mb) -> str:
384
- return f"""
385
- <script>
386
- (function() {{
387
- const host = document.getElementById('sw-ply-file');
388
- if (!host) return;
389
- function tryFire() {{
390
- const a = host.querySelector('a[href]');
391
- if (a && a.href && typeof window.swLoadUrl === 'function') {{
392
- window.swLoadUrl(a.href, '{size_mb}');
393
- return true;
394
- }}
395
- return false;
396
- }}
397
- if (tryFire()) return;
398
- const obs = new MutationObserver(function() {{ if (tryFire()) obs.disconnect(); }});
399
- obs.observe(host, {{ childList: true, subtree: true, attributes: true }});
400
- setTimeout(function() {{ obs.disconnect(); }}, 15000);
401
- }})();
402
- </script>
403
- """
404
-
405
-
406
  # ────────────────────────────────────────────────────────────────
407
  # UI β€” css in gr.Blocks() (correct for all Gradio versions)
408
  # ────────────────────────────────────────────────────────────────
@@ -461,14 +475,12 @@ with gr.Blocks(css=CSS, title="SplatWeb") as demo:
461
  gr.HTML(VIEWER_SHELL)
462
 
463
  # Hidden File component β€” Gradio serves this over its normal
464
- # file route, so the browser can fetch() the raw bytes instead
465
- # of us shipping a giant base64 string through the page.
 
 
466
  ply_file = gr.File(visible=False, elem_id="sw-ply-file", label="ply")
467
 
468
- # Hidden trigger β€” tiny script (no file data) that tells the
469
- # viewer where to fetch the model from once ply_file updates
470
- trigger1 = gr.HTML(value="", visible=False)
471
-
472
  # ── Tab 2: Two angles + animation ───────────────────
473
  with gr.TabItem("🎬 Two Angles β†’ Animation"):
474
 
@@ -527,17 +539,19 @@ with gr.Blocks(css=CSS, title="SplatWeb") as demo:
527
 
528
  # ── Handlers ──────────────────────────────────────────────
529
  def handle_single(img):
 
530
  if img is None:
531
- return "⚠ Please upload a photo first.", gr.update(value=None, visible=False), gr.update(value="", visible=False)
532
  try:
533
  ply, status = run_sharp(img)
534
  except Exception as e:
 
535
  return (f"⚠ GPU rejected the request: {str(e)}\n(If this mentions quota/duration, you've hit your daily free ZeroGPU limit β€” wait for it to reset, or sign in with a HF account for a bigger quota.)",
536
- gr.update(value=None, visible=False), gr.update(value="", visible=False))
 
537
  if ply:
538
- size_mb = round(os.path.getsize(ply) / 1024 / 1024, 2)
539
- return status, gr.update(value=ply, visible=False), gr.update(value=make_load_trigger(size_mb), visible=True)
540
- return status, gr.update(value=None, visible=False), gr.update(value="", visible=False)
541
 
542
  def handle_dual(a, b, do_anim):
543
  if a is None or b is None:
@@ -576,9 +590,34 @@ with gr.Blocks(css=CSS, title="SplatWeb") as demo:
576
 
577
  return msg, html_out
578
 
579
- btn1.click(fn=handle_single, inputs=[img1], outputs=[st1, ply_file, trigger1])
580
  btn2.click(fn=handle_dual, inputs=[imgA, imgB, anim_toggle], outputs=[st2, anim_file])
581
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
582
  gr.HTML("""
583
  <div style="text-align:center;padding:1.5rem 1rem;
584
  border-top:1px solid rgba(80,130,255,0.07);margin-top:1.5rem;">
 
4
  import os
5
  import base64
6
  import tempfile
7
+ import time
8
  from PIL import Image
9
 
10
  # ── ZeroGPU optional ──
 
59
 
60
  @spaces.GPU(duration=GPU_DURATION)
61
  def run_sharp(image_path):
62
+ t0 = time.time()
63
+ print(f"[run_sharp] START image_path={image_path}", flush=True)
64
  if image_path is None:
65
  return None, "⚠ Please upload a photo first."
66
  out_dir = tempfile.mkdtemp(prefix="splat_")
67
  resized = None
68
  try:
69
  resized = resize_image(image_path, 512)
70
+ print(f"[run_sharp] resized -> {resized} (+{time.time()-t0:.1f}s)", flush=True)
71
  result = subprocess.run(
72
  ["sharp", "predict", "-i", resized, "-o", out_dir],
73
  capture_output=True, text=True, timeout=GPU_DURATION - 15
74
  )
75
+ print(f"[run_sharp] sharp exited code={result.returncode} (+{time.time()-t0:.1f}s)", flush=True)
76
  ply_files = [f for f in os.listdir(out_dir) if f.endswith(".ply")]
77
  if not ply_files:
78
  err = (result.stderr or result.stdout or "No .ply produced.")[-800:]
79
+ print(f"[run_sharp] FAILED, no .ply produced:\n{err}", flush=True)
80
  return None, f"SHARP failed:\n{err}"
81
+ ply_path = os.path.join(out_dir, ply_files[0])
82
+ size_mb = os.path.getsize(ply_path) / 1024 / 1024
83
+ print(f"[run_sharp] DONE ply={ply_path} size={size_mb:.2f}MB total={time.time()-t0:.1f}s", flush=True)
84
+ return ply_path, "βœ“ Done β€” 3D scene loading below ↓"
85
  except subprocess.TimeoutExpired:
86
+ print(f"[run_sharp] TIMEOUT after {time.time()-t0:.1f}s", flush=True)
87
  return None, "⚠ Timed out inside the GPU window. Try a smaller/simpler photo, or increase GPU_DURATION near the top of app.py if you have more quota (PRO/Team/Enterprise)."
88
  except FileNotFoundError:
89
+ print("[run_sharp] SHARP binary not found on PATH", flush=True)
90
  return None, "⚠ SHARP not found yet β€” wait 1 minute and try again."
91
  except Exception as e:
92
+ print(f"[run_sharp] EXCEPTION: {e}", flush=True)
93
  return None, f"⚠ Error: {str(e)}"
94
  finally:
95
  if resized:
 
298
  </div>
299
  </div>
300
 
301
+ <details style="margin-top:0.6rem;border:1px solid rgba(77,138,255,0.12);border-radius:10px;
302
+ background:rgba(5,8,16,0.6);">
303
+ <summary style="cursor:pointer;padding:0.5rem 0.8rem;font-family:monospace;font-size:0.6rem;
304
+ color:rgba(180,200,255,0.45);letter-spacing:0.08em;">
305
+ // debug log (tap to expand β€” shows what the browser is doing)
306
+ </summary>
307
+ <pre id="sw-debug" style="font-family:monospace;font-size:0.58rem;line-height:1.5;
308
+ color:rgba(180,200,255,0.55);padding:0 0.8rem 0.7rem;margin:0;
309
+ max-height:180px;overflow-y:auto;white-space:pre-wrap;word-break:break-all;"></pre>
310
+ </details>
311
+
312
  <style>@keyframes sw_spin { to { transform: rotate(360deg); } }</style>
313
 
314
  <script type="importmap">
 
323
  <script type="module">
324
  import * as GaussianSplats3D from '@mkkellogg/gaussian-splats-3d';
325
 
326
+ // Expose loader + logger globally so Gradio's js= callback can call them.
 
 
 
327
  window._swViewer = null;
328
  window._swBlobUrl = null;
329
 
330
+ window.swLog = function(msg) {
331
+ const t = new Date().toTimeString().slice(0, 8);
332
+ console.log('[SplatWeb]', msg);
333
+ const dbg = document.getElementById('sw-debug');
334
+ if (dbg) {
335
+ dbg.textContent += `[${t}] ${msg}\n`;
336
+ dbg.scrollTop = dbg.scrollHeight;
337
+ }
338
+ };
339
+ window.swLog('viewer module loaded, waiting for a build…');
340
+
341
  window.swLoadUrl = async function(fileUrl, sizeMb) {
342
  const idle = document.getElementById('sw-idle');
343
  const loading = document.getElementById('sw-loading');
 
347
  const canvas = document.getElementById('sw-canvas');
348
  const msg = document.getElementById('sw-load-msg');
349
 
350
+ swLog('trigger fired β†’ ' + fileUrl);
351
+
352
  idle.style.display = 'none';
353
  loading.style.display = 'flex';
354
  controls.style.display = 'none';
 
356
 
357
  try {
358
  msg.textContent = 'DOWNLOADING SCENE…';
359
+ swLog('fetching file…');
360
  const resp = await fetch(fileUrl);
361
+ swLog('fetch responded: HTTP ' + resp.status);
362
+ if (!resp.ok) throw new Error('server returned HTTP ' + resp.status + ' for the file URL');
363
+
364
  const buf = await resp.arrayBuffer();
365
+ swLog('downloaded ' + (buf.byteLength / 1024 / 1024).toFixed(2) + ' MB');
366
  const blobUrl = URL.createObjectURL(new Blob([buf], { type: 'application/octet-stream' }));
367
 
368
  if (window._swBlobUrl) { try { URL.revokeObjectURL(window._swBlobUrl); } catch(e) {} }
 
373
  window._swViewer = null;
374
  }
375
 
376
+ swLog('initializing WebGL viewer…');
377
  const viewer = new GaussianSplats3D.Viewer({
378
  canvas,
379
  cameraUp: [0, -1, 0],
 
384
  window._swViewer = viewer;
385
 
386
  msg.textContent = 'RENDERING GAUSSIANS…';
387
+ swLog('parsing splat data + uploading to GPU…');
388
  await viewer.addSplatScene(blobUrl, { progressiveLoad: true });
389
+ swLog('scene loaded βœ“');
390
 
391
  loading.style.display = 'none';
392
  controls.style.display = 'flex';
 
394
  viewer.start();
395
 
396
  } catch(err) {
397
+ const emsg = (err && err.message) ? err.message : String(err);
398
+ swLog('ERROR: ' + emsg);
399
+ msg.textContent = '⚠ ' + emsg;
400
  console.error(err);
401
  }
402
  };
 
417
  """
418
 
419
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
  # ────────────────────────────────────────────────────────────────
421
  # UI β€” css in gr.Blocks() (correct for all Gradio versions)
422
  # ────────────────────────────────────────────────────────────────
 
475
  gr.HTML(VIEWER_SHELL)
476
 
477
  # Hidden File component β€” Gradio serves this over its normal
478
+ # file route and gives the frontend a real URL for it (via the
479
+ # .change(js=...) wiring below), so the browser can fetch() the
480
+ # raw bytes directly instead of us shipping a giant base64
481
+ # string through the page.
482
  ply_file = gr.File(visible=False, elem_id="sw-ply-file", label="ply")
483
 
 
 
 
 
484
  # ── Tab 2: Two angles + animation ───────────────────
485
  with gr.TabItem("🎬 Two Angles β†’ Animation"):
486
 
 
539
 
540
  # ── Handlers ──────────────────────────────────────────────
541
  def handle_single(img):
542
+ print(f"[handle_single] click received, img={img}", flush=True)
543
  if img is None:
544
+ return "⚠ Please upload a photo first.", gr.update(value=None, visible=False)
545
  try:
546
  ply, status = run_sharp(img)
547
  except Exception as e:
548
+ print(f"[handle_single] GPU call raised: {e}", flush=True)
549
  return (f"⚠ GPU rejected the request: {str(e)}\n(If this mentions quota/duration, you've hit your daily free ZeroGPU limit β€” wait for it to reset, or sign in with a HF account for a bigger quota.)",
550
+ gr.update(value=None, visible=False))
551
+ print(f"[handle_single] run_sharp returned ply={ply!r} status={status!r}", flush=True)
552
  if ply:
553
+ return status, gr.update(value=ply, visible=False)
554
+ return status, gr.update(value=None, visible=False)
 
555
 
556
  def handle_dual(a, b, do_anim):
557
  if a is None or b is None:
 
590
 
591
  return msg, html_out
592
 
593
+ btn1.click(fn=handle_single, inputs=[img1], outputs=[st1, ply_file])
594
  btn2.click(fn=handle_dual, inputs=[imgA, imgB, anim_toggle], outputs=[st2, anim_file])
595
 
596
+ # This is the load-bearing wire: whenever ply_file's value changes
597
+ # (including programmatically, from handle_single's return), Gradio
598
+ # hands this JS function the component's real FileData β€” the same
599
+ # {path, url, size, ...} object it uses internally β€” no DOM-scraping,
600
+ # no guessing routes. fn=None means this runs client-side only, no
601
+ # extra server round-trip.
602
+ ply_file.change(
603
+ fn=None, inputs=[ply_file], outputs=[],
604
+ js="""
605
+ (file) => {
606
+ const log = (m) => { if (window.swLog) window.swLog(m); else console.log('[SplatWeb]', m); };
607
+ log('ply_file component changed: ' + JSON.stringify(file));
608
+ if (file && file.url) {
609
+ const mb = file.size ? (file.size / 1024 / 1024).toFixed(2) : '';
610
+ if (window.swLoadUrl) { window.swLoadUrl(file.url, mb); }
611
+ else { log('ERROR: viewer script not ready yet β€” reload the page and try again'); }
612
+ } else if (file) {
613
+ log('ERROR: file has no .url field (got: ' + Object.keys(file).join(',') + ') β€” Gradio version mismatch?');
614
+ } else {
615
+ log('cleared / no file');
616
+ }
617
+ }
618
+ """
619
+ )
620
+
621
  gr.HTML("""
622
  <div style="text-align:center;padding:1.5rem 1rem;
623
  border-top:1px solid rgba(80,130,255,0.07);margin-top:1.5rem;">