splatweb / app.py
VISHAL18for4's picture
Upload app.py
23c5a20 verified
import gradio as gr
import subprocess
import sys
import os
import base64
import tempfile
from PIL import Image
# ── ZeroGPU optional ──
try:
import spaces
HAS_GPU = True
except Exception:
HAS_GPU = False
class spaces:
@staticmethod
def GPU(duration=60):
return lambda fn: fn
# ── Install SHARP ──
def install_sharp():
try:
r = subprocess.run(["sharp", "--help"], capture_output=True, timeout=10)
if r.returncode == 0:
return
except Exception:
pass
print("Installing Apple SHARP...")
subprocess.check_call([
sys.executable, "-m", "pip", "install",
"git+https://github.com/apple/ml-sharp.git",
"--quiet"
])
install_sharp()
# ── Resize before SHARP ──
def resize_image(path, max_size=512):
img = Image.open(path).convert("RGB")
w, h = img.size
if max(w, h) > max_size:
r = max_size / max(w, h)
img = img.resize((int(w*r), int(h*r)), Image.LANCZOS)
tmp = tempfile.NamedTemporaryFile(suffix=".jpg", delete=False, prefix="sharp_in_")
img.save(tmp.name, "JPEG", quality=90)
return tmp.name
# ── Run SHARP ──
@spaces.GPU(duration=300)
def run_sharp(image_path):
if image_path is None:
return None, "⚠ Please upload a photo first."
out_dir = tempfile.mkdtemp(prefix="splat_")
resized = None
try:
resized = resize_image(image_path, 512)
result = subprocess.run(
["sharp", "predict", "-i", resized, "-o", out_dir],
capture_output=True, text=True, timeout=270
)
ply_files = [f for f in os.listdir(out_dir) if f.endswith(".ply")]
if not ply_files:
err = (result.stderr or result.stdout or "No .ply produced.")[-800:]
return None, f"SHARP failed:\n{err}"
return os.path.join(out_dir, ply_files[0]), "βœ“ Done β€” 3D scene loading below ↓"
except subprocess.TimeoutExpired:
return None, "⚠ Timed out. Request ZeroGPU in your Space Community tab to make this instant."
except FileNotFoundError:
return None, "⚠ SHARP not found yet β€” wait 1 minute and try again."
except Exception as e:
return None, f"⚠ Error: {str(e)}"
finally:
if resized:
try: os.unlink(resized)
except: pass
# ── Animation HTML (two angles, camera flies between them) ──
def generate_animation_html(ply1, ply2):
def enc(p):
with open(p, "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
b1, b2 = enc(ply1), enc(ply2)
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<title>SplatWeb Animation</title>
<style>
*{{box-sizing:border-box;margin:0;padding:0}}
body{{background:#050810;color:#dce8ff;font-family:monospace;height:100vh;display:flex;flex-direction:column;overflow:hidden}}
#hdr{{display:flex;align-items:center;justify-content:space-between;padding:.8rem 1.4rem;
border-bottom:1px solid rgba(77,138,255,.15);background:rgba(5,8,16,.9);backdrop-filter:blur(10px);flex-shrink:0;gap:.8rem}}
.logo{{font-size:1.1rem;font-weight:bold;letter-spacing:.06em;background:linear-gradient(90deg,#4d8aff,#8b5cf6);-webkit-background-clip:text;-webkit-text-fill-color:transparent}}
.hr{{display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;justify-content:flex-end}}
.badge{{font-size:.58rem;color:#22d3a0;border:1px solid rgba(34,211,160,.3);padding:.22rem .65rem;border-radius:100px;white-space:nowrap}}
#btndl{{background:linear-gradient(135deg,#4d8aff,#8b5cf6);border:none;border-radius:8px;padding:.32rem .85rem;
color:#fff;font-family:monospace;font-size:.6rem;cursor:pointer;white-space:nowrap;transition:all .2s}}
#btndl:hover{{transform:translateY(-1px)}}
#wrap{{flex:1;position:relative;overflow:hidden}}
canvas{{width:100%!important;height:100%!important;display:block}}
#ov{{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1rem;background:rgba(5,8,16,.78);backdrop-filter:blur(4px)}}
#ov.hidden{{display:none}}
.sp{{width:42px;height:42px;border:2px solid rgba(77,138,255,.18);border-top-color:#4d8aff;border-radius:50%;animation:spin 1s linear infinite}}
@keyframes spin{{to{{transform:rotate(360deg)}}}}
#lm{{font-size:.7rem;color:rgba(180,200,255,.5);letter-spacing:.1em}}
#sl{{position:absolute;top:1rem;left:50%;transform:translateX(-50%);font-size:.6rem;color:rgba(180,200,255,.4);
background:rgba(0,0,0,.55);padding:.25rem .9rem;border-radius:100px;border:1px solid rgba(77,138,255,.12);display:none;white-space:nowrap}}
#ctrls{{position:absolute;bottom:1.2rem;left:50%;transform:translateX(-50%);display:flex;gap:.5rem;flex-wrap:wrap;justify-content:center}}
.pill{{background:rgba(0,0,0,.62);border:1px solid rgba(255,255,255,.07);border-radius:100px;padding:.28rem .72rem;font-size:.57rem;color:rgba(180,200,255,.42)}}
#pbw{{position:absolute;bottom:0;left:0;right:0;height:3px;background:rgba(77,138,255,.1)}}
#pb{{height:100%;background:linear-gradient(90deg,#4d8aff,#8b5cf6,#22d3a0);width:0%;transition:width .12s linear}}
</style>
</head>
<body>
<div id="hdr">
<div class="logo">SPLATWEB</div>
<div class="hr">
<div class="badge">✦ 3D KEYFRAME ANIMATION</div>
<button id="btndl" onclick="saveme()">⬇ SAVE HTML</button>
</div>
</div>
<div id="wrap">
<canvas id="c"></canvas>
<div id="ov"><div class="sp"></div><div id="lm">LOADING 3D SCENES…</div></div>
<div id="sl">ANGLE 1</div>
<div id="ctrls">
<div class="pill">Drag β†’ Orbit</div>
<div class="pill">Scroll β†’ Zoom</div>
<div class="pill">Camera auto-animates</div>
</div>
<div id="pbw"><div id="pb"></div></div>
</div>
<script type="importmap">{{"imports":{{"@mkkellogg/gaussian-splats-3d":"https://cdn.jsdelivr.net/npm/@mkkellogg/gaussian-splats-3d@0.4.2/build/gaussian-splats-3d.module.js"}}}}</script>
<script type="module">
import * as G from '@mkkellogg/gaussian-splats-3d';
function b2u(b){{const bin=atob(b),buf=new Uint8Array(bin.length);for(let i=0;i<bin.length;i++)buf[i]=bin.charCodeAt(i);return URL.createObjectURL(new Blob([buf],{{type:'application/octet-stream'}}));}}
const u1=b2u(`{b1}`),u2=b2u(`{b2}`);
const canvas=document.getElementById('c'),ov=document.getElementById('ov'),lm=document.getElementById('lm'),pb=document.getElementById('pb'),sl=document.getElementById('sl');
const A={{x:-2.5,y:-1.2,z:5}},B={{x:2.5,y:-.4,z:5}},TRAVEL=4000,HOLD=1500;
let viewer=null,phase='hold_a',ps=null;
const ease=t=>t<.5?2*t*t:-1+(4-2*t)*t,lerp=(a,b,t)=>a+(b-a)*t;
function tick(now){{
if(!viewer||!viewer.camera){{requestAnimationFrame(tick);return;}}
if(!ps)ps=now;const e=now-ps,cam=viewer.camera;
if(phase==='hold_a'){{cam.position.set(A.x,A.y,A.z);pb.style.width='0%';sl.textContent='ANGLE 1';if(e>=HOLD){{phase='a_to_b';ps=now;}}}}
else if(phase==='a_to_b'){{const t=ease(Math.min(e/TRAVEL,1));cam.position.set(lerp(A.x,B.x,t),lerp(A.y,B.y,t),lerp(A.z,B.z,t));pb.style.width=(t*100)+'%';sl.textContent='ANGLE 1 β†’ ANGLE 2';if(e>=TRAVEL){{phase='hold_b';ps=now;}}}}
else if(phase==='hold_b'){{cam.position.set(B.x,B.y,B.z);pb.style.width='100%';sl.textContent='ANGLE 2';if(e>=HOLD){{phase='b_to_a';ps=now;}}}}
else if(phase==='b_to_a'){{const t=ease(Math.min(e/TRAVEL,1));cam.position.set(lerp(B.x,A.x,t),lerp(B.y,A.y,t),lerp(B.z,A.z,t));pb.style.width=((1-t)*100)+'%';sl.textContent='ANGLE 2 β†’ ANGLE 1';if(e>=TRAVEL){{phase='hold_a';ps=now;}}}}
cam.lookAt(0,0,0);requestAnimationFrame(tick);
}}
async function init(){{
viewer=new G.Viewer({{canvas,cameraUp:[0,-1,0],initialCameraPosition:[A.x,A.y,A.z],initialCameraLookAt:[0,0,0],selfDrivenMode:true,dynamicScene:true}});
try{{
lm.textContent='LOADING ANGLE 1…';await viewer.addSplatScene(u1,{{progressiveLoad:false}});
lm.textContent='LOADING ANGLE 2…';await viewer.addSplatScene(u2,{{progressiveLoad:false}});
ov.classList.add('hidden');sl.style.display='block';viewer.start();requestAnimationFrame(tick);
}}catch(err){{lm.textContent='⚠ Load error.';console.error(err);}}
}}
init();
</script>
<script>function saveme(){{const blob=new Blob([document.documentElement.outerHTML],{{type:'text/html'}});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='splatweb-animation.html';document.body.appendChild(a);a.click();document.body.removeChild(a);}}</script>
</body></html>"""
# ────────────────────────────────────────────────────────────────
# CSS β€” back in gr.Blocks() where it belongs in Gradio 6
# (shows a harmless warning in logs but does NOT crash)
# ────────────────────────────────────────────────────────────────
CSS = """
@import url('https://fonts.googleapis.com/css2?family=Syne:wght@700;800&family=JetBrains+Mono:wght@300;400&display=swap');
body, .gradio-container {
background: #050810 !important;
font-family: 'JetBrains Mono', monospace !important;
}
.gradio-container { max-width: 820px !important; margin: 0 auto !important; }
h1 {
font-family: 'Syne', sans-serif !important;
font-weight: 800 !important;
font-size: 2.6rem !important;
background: linear-gradient(135deg, #4d8aff, #8b5cf6, #22d3a0) !important;
-webkit-background-clip: text !important;
-webkit-text-fill-color: transparent !important;
letter-spacing: -0.02em !important;
line-height: 1.1 !important;
margin-bottom: 0.5rem !important;
}
button.lg { font-family: 'Syne', sans-serif !important; font-weight: 700 !important; }
"""
# ────────────────────────────────────────────────────────────────
# The 3D viewer HTML β€” always visible, loads instantly
# After processing, JavaScript swaps in the PLY via base64
# ────────────────────────────────────────────────────────────────
VIEWER_SHELL = """
<div id="splatweb-viewer"
style="background:#050810;border:1px solid rgba(77,138,255,0.18);
border-radius:18px;overflow:hidden;">
<!-- Header bar -->
<div style="display:flex;align-items:center;justify-content:space-between;
padding:0.7rem 1.2rem;border-bottom:1px solid rgba(77,138,255,0.1);
background:rgba(5,8,16,0.95);gap:0.6rem;flex-wrap:wrap;">
<div>
<span style="font-family:monospace;font-size:0.68rem;font-weight:bold;
background:linear-gradient(90deg,#4d8aff,#8b5cf6);
-webkit-background-clip:text;-webkit-text-fill-color:transparent;
letter-spacing:0.06em;">✦ 3D SCENE VIEWER</span>
<span id="sw-size" style="font-family:monospace;font-size:0.55rem;
color:rgba(180,200,255,0.3);margin-left:0.5rem;"></span>
</div>
<button id="sw-dl-btn" onclick="swDownload()"
style="display:none;background:linear-gradient(135deg,#4d8aff,#8b5cf6);
border:none;border-radius:8px;padding:0.3rem 0.9rem;color:#fff;
font-family:monospace;font-size:0.6rem;letter-spacing:0.07em;
cursor:pointer;box-shadow:0 2px 10px rgba(77,138,255,0.3);">
⬇ DOWNLOAD .PLY
</button>
</div>
<!-- Canvas area -->
<div style="position:relative;width:100%;height:460px;" id="sw-wrap">
<canvas id="sw-canvas"
style="width:100%;height:100%;display:block;
background:radial-gradient(ellipse at center,#071020,#020408);"></canvas>
<!-- Idle state (shown before any file is processed) -->
<div id="sw-idle"
style="position:absolute;inset:0;display:flex;flex-direction:column;
align-items:center;justify-content:center;gap:0.8rem;
background:rgba(5,8,16,0.85);">
<div style="font-size:2.5rem;opacity:0.3;">✦</div>
<div style="font-family:monospace;font-size:0.7rem;color:rgba(180,200,255,0.35);
letter-spacing:0.12em;text-align:center;line-height:1.7;">
3D SCENE WILL APPEAR HERE<br/>
<span style="font-size:0.58rem;opacity:0.6;">Upload a photo and press Build β†’</span>
</div>
</div>
<!-- Loading spinner (shown while PLY loads into viewer) -->
<div id="sw-loading"
style="position:absolute;inset:0;display:none;flex-direction:column;
align-items:center;justify-content:center;gap:1rem;
background:rgba(5,8,16,0.82);backdrop-filter:blur(4px);">
<div style="width:40px;height:40px;
border:2px solid rgba(77,138,255,0.2);
border-top-color:#4d8aff;border-radius:50%;
animation:sw_spin 1s linear infinite;"></div>
<div id="sw-load-msg"
style="font-family:monospace;font-size:0.68rem;
color:rgba(180,200,255,0.55);letter-spacing:0.1em;">
BUILDING 3D SCENE…
</div>
</div>
<!-- Controls (shown after scene loads) -->
<div id="sw-controls"
style="position:absolute;bottom:0.9rem;left:50%;transform:translateX(-50%);
display:none;gap:0.4rem;flex-wrap:wrap;justify-content:center;pointer-events:none;">
<span style="background:rgba(0,0,0,0.65);backdrop-filter:blur(6px);
border:1px solid rgba(255,255,255,0.07);border-radius:100px;
padding:0.25rem 0.65rem;font-family:monospace;font-size:0.55rem;
color:rgba(180,200,255,0.4);">Drag β†’ Orbit</span>
<span style="background:rgba(0,0,0,0.65);backdrop-filter:blur(6px);
border:1px solid rgba(255,255,255,0.07);border-radius:100px;
padding:0.25rem 0.65rem;font-family:monospace;font-size:0.55rem;
color:rgba(180,200,255,0.4);">Scroll β†’ Zoom</span>
<span style="background:rgba(0,0,0,0.65);backdrop-filter:blur(6px);
border:1px solid rgba(255,255,255,0.07);border-radius:100px;
padding:0.25rem 0.65rem;font-family:monospace;font-size:0.55rem;
color:rgba(180,200,255,0.4);">Shift+Drag β†’ Pan</span>
</div>
</div>
</div>
<style>@keyframes sw_spin { to { transform: rotate(360deg); } }</style>
<script type="importmap">
{
"imports": {
"@mkkellogg/gaussian-splats-3d":
"https://cdn.jsdelivr.net/npm/@mkkellogg/gaussian-splats-3d@0.4.2/build/gaussian-splats-3d.module.js"
}
}
</script>
<script type="module">
import * as GaussianSplats3D from '@mkkellogg/gaussian-splats-3d';
// Expose loadSplat globally so Gradio JS can call it
window._swViewer = null;
window._swB64 = null;
window.swLoadB64 = async function(b64, sizeMb) {
const idle = document.getElementById('sw-idle');
const loading = document.getElementById('sw-loading');
const controls = document.getElementById('sw-controls');
const dlBtn = document.getElementById('sw-dl-btn');
const sizeEl = document.getElementById('sw-size');
const canvas = document.getElementById('sw-canvas');
const msg = document.getElementById('sw-load-msg');
// Store b64 for download
window._swB64 = b64;
// Hide idle, show loading
idle.style.display = 'none';
loading.style.display = 'flex';
controls.style.display = 'none';
if (sizeEl) sizeEl.textContent = sizeMb + ' MB Β· WebGL Β· your GPU';
// Decode base64 β†’ Blob URL
const bin = atob(b64);
const buf = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
const url = URL.createObjectURL(new Blob([buf], { type: 'application/octet-stream' }));
// Dispose old viewer if any
if (window._swViewer) {
try { window._swViewer.dispose(); } catch(e) {}
window._swViewer = null;
}
try {
const viewer = new GaussianSplats3D.Viewer({
canvas,
cameraUp: [0, -1, 0],
initialCameraPosition: [-1, -4, 6],
initialCameraLookAt: [0, 0, 0],
selfDrivenMode: true,
});
window._swViewer = viewer;
msg.textContent = 'RENDERING GAUSSIANS…';
await viewer.addSplatScene(url, { progressiveLoad: true });
loading.style.display = 'none';
controls.style.display = 'flex';
dlBtn.style.display = 'block';
viewer.start();
} catch(err) {
msg.textContent = '⚠ Viewer error β€” try Chrome or Firefox desktop.';
console.error(err);
}
};
</script>
<script>
// Download using stored b64
function swDownload() {
if (!window._swB64) return;
const bin = atob(window._swB64);
const buf = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
const blob = new Blob([buf], { type: 'application/octet-stream' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'scene.ply';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
</script>
"""
# ── Build the JS trigger to auto-load PLY into the viewer ──
def make_load_script(ply_path: str) -> str:
"""Returns a tiny HTML snippet that calls swLoadB64() with the PLY data."""
with open(ply_path, "rb") as f:
b64 = base64.b64encode(f.read()).decode("utf-8")
size_mb = round(os.path.getsize(ply_path) / 1024 / 1024, 2)
return f"""
<script>
(function tryLoad() {{
if (typeof window.swLoadB64 === 'function') {{
window.swLoadB64(`{b64}`, '{size_mb}');
}} else {{
setTimeout(tryLoad, 200);
}}
}})();
</script>
"""
# ────────────────────────────────────────────────────────────────
# UI β€” css in gr.Blocks() (correct for all Gradio versions)
# ────────────────────────────────────────────────────────────────
with gr.Blocks(css=CSS, title="SplatWeb") as demo:
gr.HTML("""
<div style="text-align:center;padding:2.5rem 1rem 1.5rem;
border-bottom:1px solid rgba(80,130,255,0.1);margin-bottom:1.5rem;">
<h1>SPLATWEB</h1>
<p style="color:rgba(180,200,255,0.4);font-size:0.72rem;
letter-spacing:0.1em;margin-top:0.3rem;">
PHOTO β†’ 3D GAUSSIAN SPLAT Β· APPLE SHARP Β· FREE
</p>
<div style="display:inline-flex;gap:0.6rem;margin-top:0.8rem;
flex-wrap:wrap;justify-content:center;">
<span style="background:rgba(34,211,160,0.08);border:1px solid rgba(34,211,160,0.2);
color:#22d3a0;font-size:0.6rem;padding:0.2rem 0.7rem;border-radius:100px;">
βœ“ 100% FREE</span>
<span style="background:rgba(77,138,255,0.08);border:1px solid rgba(77,138,255,0.2);
color:#4d8aff;font-size:0.6rem;padding:0.2rem 0.7rem;border-radius:100px;">
⚑ AUTO 3D PREVIEW</span>
<span style="background:rgba(139,92,246,0.08);border:1px solid rgba(139,92,246,0.2);
color:#8b5cf6;font-size:0.6rem;padding:0.2rem 0.7rem;border-radius:100px;">
✦ CAMERA ANIMATION</span>
</div>
</div>
""")
with gr.Tabs():
# ── Tab 1: Single photo ──────────────────────────────
with gr.TabItem("πŸ“· Single Photo β†’ 3D"):
img1 = gr.Image(
type="filepath", label="// upload photo",
sources=["upload", "webcam"], height=260
)
with gr.Accordion("πŸ“Έ Tips for best results", open=False):
gr.Markdown("""
- Any size photo β€” app auto-resizes to 512px before processing
- Clear, well-lit subject gives the best 3D quality
- Works on: objects, rooms, food, people, landscapes
- **Without ZeroGPU:** ~2–4 min on CPU
- **With ZeroGPU:** under 1 second β€” request it in your Space's Community tab
""")
btn1 = gr.Button("✦ Build My 3D Scene", variant="primary", size="lg")
st1 = gr.Textbox(
label="// status", interactive=False, lines=2,
placeholder="Upload a photo and press the button…"
)
# 3D viewer β€” always visible, waits for content
gr.HTML(VIEWER_SHELL)
# Hidden trigger β€” populated after processing, fires JS to load PLY
trigger1 = gr.HTML(value="", visible=False)
# ── Tab 2: Two angles + animation ───────────────────
with gr.TabItem("🎬 Two Angles β†’ Animation"):
gr.HTML("""
<div style="background:rgba(139,92,246,0.06);
border:1px solid rgba(139,92,246,0.18);
border-radius:12px;padding:1rem 1.2rem;margin-bottom:1rem;">
<div style="font-size:0.62rem;color:#8b5cf6;
letter-spacing:0.1em;margin-bottom:0.4rem;">✦ HOW THIS WORKS</div>
<div style="font-size:0.72rem;color:rgba(180,200,255,0.55);line-height:1.65;">
Upload 2 photos of the same object from different angles.
Both convert to 3D. Enable animation to get a downloadable HTML file β€”
open it on any phone, camera flies between both angles in a loop.
The HTML has a <strong style="color:#dce8ff;">⬇ SAVE HTML</strong> button inside.
</div>
</div>
""")
with gr.Row():
imgA = gr.Image(
type="filepath", label="// angle 1 β€” front / left",
sources=["upload"], height=220
)
imgB = gr.Image(
type="filepath", label="// angle 2 β€” back / right",
sources=["upload"], height=220
)
anim_toggle = gr.Checkbox(
label="✦ Generate camera animation HTML",
value=True,
info="Downloadable .html β€” camera flies between angles in a loop, has ⬇ SAVE HTML inside"
)
btn2 = gr.Button("✦ Build 3D + Animation", variant="primary", size="lg")
st2 = gr.Textbox(
label="// status", interactive=False, lines=3,
placeholder="Upload both photos and press the button…"
)
anim_file = gr.File(
label="// animation .html β€” download & open on any phone",
visible=False, file_types=[".html"]
)
gr.HTML("""
<div style="margin-top:0.8rem;padding:0.9rem 1rem;
background:rgba(34,211,160,0.04);
border:1px solid rgba(34,211,160,0.1);
border-radius:10px;font-size:0.65rem;
color:rgba(180,200,255,0.38);line-height:1.8;">
πŸ“± Download animation .html β†’ open in Chrome or Safari on any phone β†’ 3D plays.<br/>
πŸ“€ Share via WhatsApp / email β€” recipient just opens the file, no app needed.<br/>
πŸ’Ύ Tap <strong style="color:#dce8ff;">⬇ SAVE HTML</strong> inside to re-download anytime.
</div>
""")
# ── Handlers ──────────────────────────────────────────────
def handle_single(img):
if img is None:
return "⚠ Please upload a photo first.", gr.update(value="", visible=False)
ply, status = run_sharp(img)
if ply:
script = make_load_script(ply)
return status, gr.update(value=script, visible=True)
return status, gr.update(value="", visible=False)
def handle_dual(a, b, do_anim):
if a is None or b is None:
return "⚠ Please upload BOTH photos.", gr.update(visible=False)
ply1, s1 = run_sharp(a)
if not ply1:
return f"⚠ Angle 1 failed:\n{s1}", gr.update(visible=False)
ply2, s2 = run_sharp(b)
if not ply2:
return f"βœ“ Angle 1 done.\n⚠ Angle 2 failed:\n{s2}", gr.update(visible=False)
msg = "βœ“ Both angles done!"
html_out = gr.update(visible=False)
if do_anim:
try:
content = generate_animation_html(ply1, ply2)
tmp = tempfile.NamedTemporaryFile(
suffix=".html", prefix="splatweb_anim_",
delete=False, mode="w", encoding="utf-8"
)
tmp.write(content)
tmp.close()
html_out = gr.update(value=tmp.name, visible=True)
msg += "\n✦ Animation HTML ready β€” download below and open on your phone!"
except Exception as e:
msg += f"\n⚠ Animation failed: {str(e)}"
return msg, html_out
btn1.click(fn=handle_single, inputs=[img1], outputs=[st1, trigger1])
btn2.click(fn=handle_dual, inputs=[imgA, imgB, anim_toggle], outputs=[st2, anim_file])
gr.HTML("""
<div style="text-align:center;padding:1.5rem 1rem;
border-top:1px solid rgba(80,130,255,0.07);margin-top:1.5rem;">
<p style="font-size:0.6rem;color:rgba(180,200,255,0.22);line-height:1.8;">
SplatWeb Β· Apple SHARP Β· HuggingFace Β· 3D renders on your device GPU via WebGL
</p>
</div>
""")
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860)