Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -300,11 +300,14 @@ VIEWER_SHELL = """
|
|
| 300 |
<script type="module">
|
| 301 |
import * as GaussianSplats3D from '@mkkellogg/gaussian-splats-3d';
|
| 302 |
|
| 303 |
-
// Expose
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
window.
|
|
|
|
|
|
|
|
|
|
| 308 |
const idle = document.getElementById('sw-idle');
|
| 309 |
const loading = document.getElementById('sw-loading');
|
| 310 |
const controls = document.getElementById('sw-controls');
|
|
@@ -313,28 +316,26 @@ window.swLoadB64 = async function(b64, sizeMb) {
|
|
| 313 |
const canvas = document.getElementById('sw-canvas');
|
| 314 |
const msg = document.getElementById('sw-load-msg');
|
| 315 |
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
// Hide idle, show loading
|
| 320 |
-
idle.style.display = 'none';
|
| 321 |
-
loading.style.display = 'flex';
|
| 322 |
controls.style.display = 'none';
|
| 323 |
-
if (sizeEl) sizeEl.textContent = sizeMb + ' MB Β· WebGL Β· your GPU';
|
| 324 |
-
|
| 325 |
-
// Decode base64 β Blob URL
|
| 326 |
-
const bin = atob(b64);
|
| 327 |
-
const buf = new Uint8Array(bin.length);
|
| 328 |
-
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
|
| 329 |
-
const url = URL.createObjectURL(new Blob([buf], { type: 'application/octet-stream' }));
|
| 330 |
-
|
| 331 |
-
// Dispose old viewer if any
|
| 332 |
-
if (window._swViewer) {
|
| 333 |
-
try { window._swViewer.dispose(); } catch(e) {}
|
| 334 |
-
window._swViewer = null;
|
| 335 |
-
}
|
| 336 |
|
| 337 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
const viewer = new GaussianSplats3D.Viewer({
|
| 339 |
canvas,
|
| 340 |
cameraUp: [0, -1, 0],
|
|
@@ -345,7 +346,7 @@ window.swLoadB64 = async function(b64, sizeMb) {
|
|
| 345 |
window._swViewer = viewer;
|
| 346 |
|
| 347 |
msg.textContent = 'RENDERING GAUSSIANSβ¦';
|
| 348 |
-
await viewer.addSplatScene(
|
| 349 |
|
| 350 |
loading.style.display = 'none';
|
| 351 |
controls.style.display = 'flex';
|
|
@@ -360,15 +361,11 @@ window.swLoadB64 = async function(b64, sizeMb) {
|
|
| 360 |
</script>
|
| 361 |
|
| 362 |
<script>
|
| 363 |
-
// Download using
|
| 364 |
function swDownload() {
|
| 365 |
-
if (!window.
|
| 366 |
-
const bin = atob(window._swB64);
|
| 367 |
-
const buf = new Uint8Array(bin.length);
|
| 368 |
-
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
|
| 369 |
-
const blob = new Blob([buf], { type: 'application/octet-stream' });
|
| 370 |
const a = document.createElement('a');
|
| 371 |
-
a.href =
|
| 372 |
a.download = 'scene.ply';
|
| 373 |
document.body.appendChild(a);
|
| 374 |
a.click();
|
|
@@ -378,20 +375,29 @@ function swDownload() {
|
|
| 378 |
"""
|
| 379 |
|
| 380 |
|
| 381 |
-
# ββ
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
return f"""
|
| 388 |
<script>
|
| 389 |
-
(function
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
}})();
|
| 396 |
</script>
|
| 397 |
"""
|
|
@@ -454,7 +460,13 @@ with gr.Blocks(css=CSS, title="SplatWeb") as demo:
|
|
| 454 |
# 3D viewer β always visible, waits for content
|
| 455 |
gr.HTML(VIEWER_SHELL)
|
| 456 |
|
| 457 |
-
# Hidden
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
trigger1 = gr.HTML(value="", visible=False)
|
| 459 |
|
| 460 |
# ββ Tab 2: Two angles + animation βββββββββββββββββββ
|
|
@@ -516,15 +528,16 @@ with gr.Blocks(css=CSS, title="SplatWeb") as demo:
|
|
| 516 |
# ββ Handlers ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 517 |
def handle_single(img):
|
| 518 |
if img is None:
|
| 519 |
-
return "β Please upload a photo first.", gr.update(value="", visible=False)
|
| 520 |
try:
|
| 521 |
ply, status = run_sharp(img)
|
| 522 |
except Exception as e:
|
| 523 |
-
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.)",
|
|
|
|
| 524 |
if ply:
|
| 525 |
-
|
| 526 |
-
return status, gr.update(value=
|
| 527 |
-
return status, gr.update(value="", visible=False)
|
| 528 |
|
| 529 |
def handle_dual(a, b, do_anim):
|
| 530 |
if a is None or b is None:
|
|
@@ -563,7 +576,7 @@ with gr.Blocks(css=CSS, title="SplatWeb") as demo:
|
|
| 563 |
|
| 564 |
return msg, html_out
|
| 565 |
|
| 566 |
-
btn1.click(fn=handle_single, inputs=[img1], outputs=[st1, trigger1])
|
| 567 |
btn2.click(fn=handle_dual, inputs=[imgA, imgB, anim_toggle], outputs=[st2, anim_file])
|
| 568 |
|
| 569 |
gr.HTML("""
|
|
|
|
| 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');
|
| 313 |
const controls = document.getElementById('sw-controls');
|
|
|
|
| 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';
|
| 322 |
+
if (sizeEl) sizeEl.textContent = (sizeMb ? sizeMb + ' MB Β· ' : '') + 'WebGL Β· your GPU';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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) {} }
|
| 332 |
+
window._swBlobUrl = blobUrl;
|
| 333 |
+
|
| 334 |
+
if (window._swViewer) {
|
| 335 |
+
try { window._swViewer.dispose(); } catch(e) {}
|
| 336 |
+
window._swViewer = null;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
const viewer = new GaussianSplats3D.Viewer({
|
| 340 |
canvas,
|
| 341 |
cameraUp: [0, -1, 0],
|
|
|
|
| 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';
|
|
|
|
| 361 |
</script>
|
| 362 |
|
| 363 |
<script>
|
| 364 |
+
// Download using the blob URL we already fetched β no re-decoding needed.
|
| 365 |
function swDownload() {
|
| 366 |
+
if (!window._swBlobUrl) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 367 |
const a = document.createElement('a');
|
| 368 |
+
a.href = window._swBlobUrl;
|
| 369 |
a.download = 'scene.ply';
|
| 370 |
document.body.appendChild(a);
|
| 371 |
a.click();
|
|
|
|
| 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 |
"""
|
|
|
|
| 460 |
# 3D viewer β always visible, waits for content
|
| 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 βββββββββββββββββββ
|
|
|
|
| 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 |
|
| 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("""
|