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""" SplatWeb Animation
✦ 3D KEYFRAME ANIMATION
LOADING 3D SCENES…
ANGLE 1
Drag → Orbit
Scroll → Zoom
Camera auto-animates
""" # ──────────────────────────────────────────────────────────────── # 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 = """
✦ 3D SCENE VIEWER
3D SCENE WILL APPEAR HERE
Upload a photo and press Build →
""" # ── 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""" """ # ──────────────────────────────────────────────────────────────── # UI — css in gr.Blocks() (correct for all Gradio versions) # ──────────────────────────────────────────────────────────────── with gr.Blocks(css=CSS, title="SplatWeb") as demo: gr.HTML("""

SPLATWEB

PHOTO → 3D GAUSSIAN SPLAT · APPLE SHARP · FREE

✓ 100% FREE ⚡ AUTO 3D PREVIEW ✦ CAMERA ANIMATION
""") 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("""
✦ HOW THIS WORKS
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 ⬇ SAVE HTML button inside.
""") 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("""
📱 Download animation .html → open in Chrome or Safari on any phone → 3D plays.
📤 Share via WhatsApp / email — recipient just opens the file, no app needed.
💾 Tap ⬇ SAVE HTML inside to re-download anytime.
""") # ── 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("""

SplatWeb · Apple SHARP · HuggingFace · 3D renders on your device GPU via WebGL

""") if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860)