VISHAL18for4 commited on
Commit
411fdcb
·
verified ·
1 Parent(s): c441c77

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -649
app.py DELETED
@@ -1,649 +0,0 @@
1
- import gradio as gr
2
- import subprocess
3
- import sys
4
- import os
5
- import base64
6
- import tempfile
7
- import time
8
- import json
9
- import urllib.parse
10
- from PIL import Image
11
-
12
- # ── ZeroGPU optional ──
13
- try:
14
- import spaces
15
- HAS_GPU = True
16
- except Exception:
17
- HAS_GPU = False
18
- class spaces:
19
- @staticmethod
20
- def GPU(duration=60):
21
- return lambda fn: fn
22
-
23
- # ── Install SHARP ──
24
- def install_sharp():
25
- try:
26
- r = subprocess.run(["sharp", "--help"], capture_output=True, timeout=10)
27
- if r.returncode == 0:
28
- return
29
- except Exception:
30
- pass
31
- print("Installing Apple SHARP...")
32
- subprocess.check_call([
33
- sys.executable, "-m", "pip", "install",
34
- "git+https://github.com/apple/ml-sharp.git",
35
- "--quiet"
36
- ])
37
-
38
- install_sharp()
39
-
40
- # ── Resize before SHARP ──
41
- def resize_image(path, max_size=512):
42
- img = Image.open(path).convert("RGB")
43
- w, h = img.size
44
- if max(w, h) > max_size:
45
- r = max_size / max(w, h)
46
- img = img.resize((int(w*r), int(h*r)), Image.LANCZOS)
47
- tmp = tempfile.NamedTemporaryFile(suffix=".jpg", delete=False, prefix="sharp_in_")
48
- img.save(tmp.name, "JPEG", quality=90)
49
- return tmp.name
50
-
51
- # ── Run SHARP ──
52
- # NOTE on duration: HuggingFace's free-tier ZeroGPU accounts get a 5-minute
53
- # (300s) TOTAL daily quota. Requesting a duration at or above that cap makes
54
- # the platform reject the call outright with "requested GPU duration is
55
- # larger than the maximum allowed" (an "illegal duration" error) — it fails
56
- # instantly, before your function even runs, and retrying won't help.
57
- # Keeping this comfortably under the cap lets a free/unauthenticated user
58
- # actually get a few conversions in per day instead of burning the whole
59
- # quota (or being rejected) on a single call.
60
- GPU_DURATION = 90
61
-
62
- @spaces.GPU(duration=GPU_DURATION)
63
- def run_sharp(image_path):
64
- t0 = time.time()
65
- print(f"[run_sharp] START image_path={image_path}", flush=True)
66
- if image_path is None:
67
- return None, "⚠ Please upload a photo first."
68
- out_dir = tempfile.mkdtemp(prefix="splat_")
69
- resized = None
70
- try:
71
- resized = resize_image(image_path, 512)
72
- print(f"[run_sharp] resized -> {resized} (+{time.time()-t0:.1f}s)", flush=True)
73
- result = subprocess.run(
74
- ["sharp", "predict", "-i", resized, "-o", out_dir],
75
- capture_output=True, text=True, timeout=GPU_DURATION - 15
76
- )
77
- print(f"[run_sharp] sharp exited code={result.returncode} (+{time.time()-t0:.1f}s)", flush=True)
78
- ply_files = [f for f in os.listdir(out_dir) if f.endswith(".ply")]
79
- if not ply_files:
80
- err = (result.stderr or result.stdout or "No .ply produced.")[-800:]
81
- print(f"[run_sharp] FAILED, no .ply produced:\n{err}", flush=True)
82
- return None, f"SHARP failed:\n{err}"
83
- ply_path = os.path.join(out_dir, ply_files[0])
84
- size_mb = os.path.getsize(ply_path) / 1024 / 1024
85
- print(f"[run_sharp] DONE ply={ply_path} size={size_mb:.2f}MB total={time.time()-t0:.1f}s", flush=True)
86
- return ply_path, "✓ Done — 3D scene loading below ↓"
87
- except subprocess.TimeoutExpired:
88
- print(f"[run_sharp] TIMEOUT after {time.time()-t0:.1f}s", flush=True)
89
- 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)."
90
- except FileNotFoundError:
91
- print("[run_sharp] SHARP binary not found on PATH", flush=True)
92
- return None, "⚠ SHARP not found yet — wait 1 minute and try again."
93
- except Exception as e:
94
- print(f"[run_sharp] EXCEPTION: {e}", flush=True)
95
- return None, f"⚠ Error: {str(e)}"
96
- finally:
97
- if resized:
98
- try: os.unlink(resized)
99
- except: pass
100
-
101
- # ── Animation HTML (two angles, camera flies between them) ──
102
- def generate_animation_html(ply1, ply2):
103
- def enc(p):
104
- with open(p, "rb") as f:
105
- return base64.b64encode(f.read()).decode("utf-8")
106
- b1, b2 = enc(ply1), enc(ply2)
107
- return f"""<!DOCTYPE html>
108
- <html lang="en">
109
- <head>
110
- <meta charset="UTF-8"/>
111
- <meta name="viewport" content="width=device-width,initial-scale=1.0"/>
112
- <title>SplatWeb Animation</title>
113
- <style>
114
- *{{box-sizing:border-box;margin:0;padding:0}}
115
- body{{background:#050810;color:#dce8ff;font-family:monospace;height:100vh;display:flex;flex-direction:column;overflow:hidden}}
116
- #hdr{{display:flex;align-items:center;justify-content:space-between;padding:.8rem 1.4rem;
117
- border-bottom:1px solid rgba(77,138,255,.15);background:rgba(5,8,16,.9);backdrop-filter:blur(10px);flex-shrink:0;gap:.8rem}}
118
- .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}}
119
- .hr{{display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;justify-content:flex-end}}
120
- .badge{{font-size:.58rem;color:#22d3a0;border:1px solid rgba(34,211,160,.3);padding:.22rem .65rem;border-radius:100px;white-space:nowrap}}
121
- #btndl{{background:linear-gradient(135deg,#4d8aff,#8b5cf6);border:none;border-radius:8px;padding:.32rem .85rem;
122
- color:#fff;font-family:monospace;font-size:.6rem;cursor:pointer;white-space:nowrap;transition:all .2s}}
123
- #btndl:hover{{transform:translateY(-1px)}}
124
- #wrap{{flex:1;position:relative;overflow:hidden}}
125
- canvas{{width:100%!important;height:100%!important;display:block}}
126
- #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)}}
127
- #ov.hidden{{display:none}}
128
- .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}}
129
- @keyframes spin{{to{{transform:rotate(360deg)}}}}
130
- #lm{{font-size:.7rem;color:rgba(180,200,255,.5);letter-spacing:.1em}}
131
- #sl{{position:absolute;top:1rem;left:50%;transform:translateX(-50%);font-size:.6rem;color:rgba(180,200,255,.4);
132
- 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}}
133
- #ctrls{{position:absolute;bottom:1.2rem;left:50%;transform:translateX(-50%);display:flex;gap:.5rem;flex-wrap:wrap;justify-content:center}}
134
- .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)}}
135
- #pbw{{position:absolute;bottom:0;left:0;right:0;height:3px;background:rgba(77,138,255,.1)}}
136
- #pb{{height:100%;background:linear-gradient(90deg,#4d8aff,#8b5cf6,#22d3a0);width:0%;transition:width .12s linear}}
137
- </style>
138
- </head>
139
- <body>
140
- <div id="hdr">
141
- <div class="logo">SPLATWEB</div>
142
- <div class="hr">
143
- <div class="badge">✦ 3D KEYFRAME ANIMATION</div>
144
- <button id="btndl" onclick="saveme()">⬇ SAVE HTML</button>
145
- </div>
146
- </div>
147
- <div id="wrap">
148
- <canvas id="c"></canvas>
149
- <div id="ov"><div class="sp"></div><div id="lm">LOADING 3D SCENES…</div></div>
150
- <div id="sl">ANGLE 1</div>
151
- <div id="ctrls">
152
- <div class="pill">Drag → Orbit</div>
153
- <div class="pill">Scroll → Zoom</div>
154
- <div class="pill">Camera auto-animates</div>
155
- </div>
156
- <div id="pbw"><div id="pb"></div></div>
157
- </div>
158
- <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>
159
- <script type="module">
160
- import * as G from '@mkkellogg/gaussian-splats-3d';
161
- 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'}}));}}
162
- const u1=b2u(`{b1}`),u2=b2u(`{b2}`);
163
- const canvas=document.getElementById('c'),ov=document.getElementById('ov'),lm=document.getElementById('lm'),pb=document.getElementById('pb'),sl=document.getElementById('sl');
164
- const A={{x:-2.5,y:-1.2,z:5}},B={{x:2.5,y:-.4,z:5}},TRAVEL=4000,HOLD=1500;
165
- let viewer=null,phase='hold_a',ps=null;
166
- const ease=t=>t<.5?2*t*t:-1+(4-2*t)*t,lerp=(a,b,t)=>a+(b-a)*t;
167
- function tick(now){{
168
- if(!viewer||!viewer.camera){{requestAnimationFrame(tick);return;}}
169
- if(!ps)ps=now;const e=now-ps,cam=viewer.camera;
170
- 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;}}}}
171
- 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;}}}}
172
- 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;}}}}
173
- 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;}}}}
174
- cam.lookAt(0,0,0);requestAnimationFrame(tick);
175
- }}
176
- async function init(){{
177
- viewer=new G.Viewer({{canvas,cameraUp:[0,-1,0],initialCameraPosition:[A.x,A.y,A.z],initialCameraLookAt:[0,0,0],selfDrivenMode:true,dynamicScene:true}});
178
- try{{
179
- lm.textContent='LOADING ANGLE 1…';await viewer.addSplatScene(u1,{{progressiveLoad:false}});
180
- lm.textContent='LOADING ANGLE 2…';await viewer.addSplatScene(u2,{{progressiveLoad:false}});
181
- ov.classList.add('hidden');sl.style.display='block';viewer.start();requestAnimationFrame(tick);
182
- }}catch(err){{lm.textContent='⚠ Load error.';console.error(err);}}
183
- }}
184
- init();
185
- </script>
186
- <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>
187
- </body></html>"""
188
-
189
-
190
- # ────────────────────────────────────────────────────────────────
191
- # CSS — back in gr.Blocks() where it belongs in Gradio 6
192
- # (shows a harmless warning in logs but does NOT crash)
193
- # ────────────────────────────────────────────────────────────────
194
- CSS = """
195
- @import url('https://fonts.googleapis.com/css2?family=Syne:wght@700;800&family=JetBrains+Mono:wght@300;400&display=swap');
196
-
197
- body, .gradio-container {
198
- background: #050810 !important;
199
- font-family: 'JetBrains Mono', monospace !important;
200
- }
201
- .gradio-container { max-width: 820px !important; margin: 0 auto !important; }
202
-
203
- h1 {
204
- font-family: 'Syne', sans-serif !important;
205
- font-weight: 800 !important;
206
- font-size: 2.6rem !important;
207
- background: linear-gradient(135deg, #4d8aff, #8b5cf6, #22d3a0) !important;
208
- -webkit-background-clip: text !important;
209
- -webkit-text-fill-color: transparent !important;
210
- letter-spacing: -0.02em !important;
211
- line-height: 1.1 !important;
212
- margin-bottom: 0.5rem !important;
213
- }
214
-
215
- button.lg { font-family: 'Syne', sans-serif !important; font-weight: 700 !important; }
216
- """
217
-
218
- # ────────────────────────────────────────────────────────────────
219
- # The 3D viewer HTML — always visible, loads instantly
220
- # After processing, JavaScript swaps in the PLY via base64
221
- # ────────────────────────────────────────────────────────────────
222
- VIEWER_SHELL = """
223
- <div id="splatweb-viewer"
224
- style="background:#050810;border:1px solid rgba(77,138,255,0.18);
225
- border-radius:18px;overflow:hidden;">
226
-
227
- <!-- Header bar -->
228
- <div style="display:flex;align-items:center;justify-content:space-between;
229
- padding:0.7rem 1.2rem;border-bottom:1px solid rgba(77,138,255,0.1);
230
- background:rgba(5,8,16,0.95);gap:0.6rem;flex-wrap:wrap;">
231
- <div>
232
- <span style="font-family:monospace;font-size:0.68rem;font-weight:bold;
233
- background:linear-gradient(90deg,#4d8aff,#8b5cf6);
234
- -webkit-background-clip:text;-webkit-text-fill-color:transparent;
235
- letter-spacing:0.06em;">✦ 3D SCENE VIEWER</span>
236
- <span id="sw-size" style="font-family:monospace;font-size:0.55rem;
237
- color:rgba(180,200,255,0.3);margin-left:0.5rem;"></span>
238
- </div>
239
- <button id="sw-dl-btn" onclick="swDownload()"
240
- style="display:none;background:linear-gradient(135deg,#4d8aff,#8b5cf6);
241
- border:none;border-radius:8px;padding:0.3rem 0.9rem;color:#fff;
242
- font-family:monospace;font-size:0.6rem;letter-spacing:0.07em;
243
- cursor:pointer;box-shadow:0 2px 10px rgba(77,138,255,0.3);">
244
- ⬇ DOWNLOAD .PLY
245
- </button>
246
- </div>
247
-
248
- <!-- Canvas area -->
249
- <div style="position:relative;width:100%;height:460px;" id="sw-wrap">
250
- <canvas id="sw-canvas"
251
- style="width:100%;height:100%;display:block;
252
- background:radial-gradient(ellipse at center,#071020,#020408);"></canvas>
253
-
254
- <!-- Idle state (shown before any file is processed) -->
255
- <div id="sw-idle"
256
- style="position:absolute;inset:0;display:flex;flex-direction:column;
257
- align-items:center;justify-content:center;gap:0.8rem;
258
- background:rgba(5,8,16,0.85);">
259
- <div style="font-size:2.5rem;opacity:0.3;">✦</div>
260
- <div style="font-family:monospace;font-size:0.7rem;color:rgba(180,200,255,0.35);
261
- letter-spacing:0.12em;text-align:center;line-height:1.7;">
262
- 3D SCENE WILL APPEAR HERE<br/>
263
- <span style="font-size:0.58rem;opacity:0.6;">Upload a photo and press Build →</span>
264
- </div>
265
- </div>
266
-
267
- <!-- Loading spinner (shown while PLY loads into viewer) -->
268
- <div id="sw-loading"
269
- style="position:absolute;inset:0;display:none;flex-direction:column;
270
- align-items:center;justify-content:center;gap:1rem;
271
- background:rgba(5,8,16,0.82);backdrop-filter:blur(4px);">
272
- <div style="width:40px;height:40px;
273
- border:2px solid rgba(77,138,255,0.2);
274
- border-top-color:#4d8aff;border-radius:50%;
275
- animation:sw_spin 1s linear infinite;"></div>
276
- <div id="sw-load-msg"
277
- style="font-family:monospace;font-size:0.68rem;
278
- color:rgba(180,200,255,0.55);letter-spacing:0.1em;">
279
- BUILDING 3D SCENE…
280
- </div>
281
- </div>
282
-
283
- <!-- Controls (shown after scene loads) -->
284
- <div id="sw-controls"
285
- style="position:absolute;bottom:0.9rem;left:50%;transform:translateX(-50%);
286
- display:none;gap:0.4rem;flex-wrap:wrap;justify-content:center;pointer-events:none;">
287
- <span style="background:rgba(0,0,0,0.65);backdrop-filter:blur(6px);
288
- border:1px solid rgba(255,255,255,0.07);border-radius:100px;
289
- padding:0.25rem 0.65rem;font-family:monospace;font-size:0.55rem;
290
- color:rgba(180,200,255,0.4);">Drag → Orbit</span>
291
- <span style="background:rgba(0,0,0,0.65);backdrop-filter:blur(6px);
292
- border:1px solid rgba(255,255,255,0.07);border-radius:100px;
293
- padding:0.25rem 0.65rem;font-family:monospace;font-size:0.55rem;
294
- color:rgba(180,200,255,0.4);">Scroll → Zoom</span>
295
- <span style="background:rgba(0,0,0,0.65);backdrop-filter:blur(6px);
296
- border:1px solid rgba(255,255,255,0.07);border-radius:100px;
297
- padding:0.25rem 0.65rem;font-family:monospace;font-size:0.55rem;
298
- color:rgba(180,200,255,0.4);">Shift+Drag → Pan</span>
299
- </div>
300
- </div>
301
- </div>
302
-
303
- <details style="margin-top:0.6rem;border:1px solid rgba(77,138,255,0.12);border-radius:10px;
304
- background:rgba(5,8,16,0.6);">
305
- <summary style="cursor:pointer;padding:0.5rem 0.8rem;font-family:monospace;font-size:0.6rem;
306
- color:rgba(180,200,255,0.45);letter-spacing:0.08em;">
307
- // debug log (tap to expand — shows what the browser is doing)
308
- </summary>
309
- <pre id="sw-debug" style="font-family:monospace;font-size:0.58rem;line-height:1.5;
310
- color:rgba(180,200,255,0.55);padding:0 0.8rem 0.7rem;margin:0;
311
- max-height:180px;overflow-y:auto;white-space:pre-wrap;word-break:break-all;"></pre>
312
- </details>
313
-
314
- <style>@keyframes sw_spin { to { transform: rotate(360deg); } }</style>
315
-
316
- <script type="importmap">
317
- {
318
- "imports": {
319
- "@mkkellogg/gaussian-splats-3d":
320
- "https://cdn.jsdelivr.net/npm/@mkkellogg/gaussian-splats-3d@0.4.2/build/gaussian-splats-3d.module.js"
321
- }
322
- }
323
- </script>
324
-
325
- <script type="module">
326
- import * as GaussianSplats3D from '@mkkellogg/gaussian-splats-3d';
327
-
328
- // Expose loader + logger globally so Gradio's js= callback can call them.
329
- window._swViewer = null;
330
- window._swBlobUrl = null;
331
-
332
- window.swLog = function(msg) {
333
- const t = new Date().toTimeString().slice(0, 8);
334
- console.log('[SplatWeb]', msg);
335
- const dbg = document.getElementById('sw-debug');
336
- if (dbg) {
337
- dbg.textContent += `[${t}] ${msg}\n`;
338
- dbg.scrollTop = dbg.scrollHeight;
339
- }
340
- };
341
- window.swLog('viewer module loaded, waiting for a build…');
342
-
343
- window.swLoadUrl = async function(fileUrl, sizeMb) {
344
- const idle = document.getElementById('sw-idle');
345
- const loading = document.getElementById('sw-loading');
346
- const controls = document.getElementById('sw-controls');
347
- const dlBtn = document.getElementById('sw-dl-btn');
348
- const sizeEl = document.getElementById('sw-size');
349
- const canvas = document.getElementById('sw-canvas');
350
- const msg = document.getElementById('sw-load-msg');
351
-
352
- swLog('trigger fired → ' + fileUrl);
353
-
354
- idle.style.display = 'none';
355
- loading.style.display = 'flex';
356
- controls.style.display = 'none';
357
- if (sizeEl) sizeEl.textContent = (sizeMb ? sizeMb + ' MB · ' : '') + 'WebGL · your GPU';
358
-
359
- try {
360
- msg.textContent = 'DOWNLOADING SCENE…';
361
- swLog('fetching file…');
362
- const resp = await fetch(fileUrl);
363
- swLog('fetch responded: HTTP ' + resp.status);
364
- if (!resp.ok) throw new Error('server returned HTTP ' + resp.status + ' for the file URL');
365
-
366
- const buf = await resp.arrayBuffer();
367
- swLog('downloaded ' + (buf.byteLength / 1024 / 1024).toFixed(2) + ' MB');
368
- const blobUrl = URL.createObjectURL(new Blob([buf], { type: 'application/octet-stream' }));
369
-
370
- if (window._swBlobUrl) { try { URL.revokeObjectURL(window._swBlobUrl); } catch(e) {} }
371
- window._swBlobUrl = blobUrl;
372
-
373
- if (window._swViewer) {
374
- try { window._swViewer.dispose(); } catch(e) {}
375
- window._swViewer = null;
376
- }
377
-
378
- swLog('initializing WebGL viewer…');
379
- const viewer = new GaussianSplats3D.Viewer({
380
- canvas,
381
- cameraUp: [0, -1, 0],
382
- initialCameraPosition: [-1, -4, 6],
383
- initialCameraLookAt: [0, 0, 0],
384
- selfDrivenMode: true,
385
- });
386
- window._swViewer = viewer;
387
-
388
- msg.textContent = 'RENDERING GAUSSIANS…';
389
- swLog('parsing splat data + uploading to GPU…');
390
- await viewer.addSplatScene(blobUrl, { progressiveLoad: true });
391
- swLog('scene loaded ✓');
392
-
393
- loading.style.display = 'none';
394
- controls.style.display = 'flex';
395
- dlBtn.style.display = 'block';
396
- viewer.start();
397
-
398
- } catch(err) {
399
- const emsg = (err && err.message) ? err.message : String(err);
400
- swLog('ERROR: ' + emsg);
401
- msg.textContent = '⚠ ' + emsg;
402
- console.error(err);
403
- }
404
- };
405
- </script>
406
-
407
- <script>
408
- // Download using the blob URL we already fetched — no re-decoding needed.
409
- function swDownload() {
410
- if (!window._swBlobUrl) return;
411
- const a = document.createElement('a');
412
- a.href = window._swBlobUrl;
413
- a.download = 'scene.ply';
414
- document.body.appendChild(a);
415
- a.click();
416
- document.body.removeChild(a);
417
- }
418
- </script>
419
- """
420
-
421
-
422
- # Gradio serves any file under its allowed paths (which includes the
423
- # system temp dir — where our .ply files live) at this documented route:
424
- # /gradio_api/file=<path>
425
- def make_ply_url(ply_path: str) -> str:
426
- return "/gradio_api/file=" + urllib.parse.quote(ply_path, safe="/")
427
-
428
-
429
- # ────────────────────────────────────────────────────────────────
430
- # UI — css in gr.Blocks() (correct for all Gradio versions)
431
- # ────────────────────────────────────────────────────────────────
432
- with gr.Blocks(css=CSS, title="SplatWeb") as demo:
433
-
434
- gr.HTML("""
435
- <div style="text-align:center;padding:2.5rem 1rem 1.5rem;
436
- border-bottom:1px solid rgba(80,130,255,0.1);margin-bottom:1.5rem;">
437
- <h1>SPLATWEB</h1>
438
- <p style="color:rgba(180,200,255,0.4);font-size:0.72rem;
439
- letter-spacing:0.1em;margin-top:0.3rem;">
440
- PHOTO → 3D GAUSSIAN SPLAT · APPLE SHARP · FREE
441
- </p>
442
- <div style="display:inline-flex;gap:0.6rem;margin-top:0.8rem;
443
- flex-wrap:wrap;justify-content:center;">
444
- <span style="background:rgba(34,211,160,0.08);border:1px solid rgba(34,211,160,0.2);
445
- color:#22d3a0;font-size:0.6rem;padding:0.2rem 0.7rem;border-radius:100px;">
446
- ✓ 100% FREE</span>
447
- <span style="background:rgba(77,138,255,0.08);border:1px solid rgba(77,138,255,0.2);
448
- color:#4d8aff;font-size:0.6rem;padding:0.2rem 0.7rem;border-radius:100px;">
449
- ⚡ AUTO 3D PREVIEW</span>
450
- <span style="background:rgba(139,92,246,0.08);border:1px solid rgba(139,92,246,0.2);
451
- color:#8b5cf6;font-size:0.6rem;padding:0.2rem 0.7rem;border-radius:100px;">
452
- ✦ CAMERA ANIMATION</span>
453
- </div>
454
- </div>
455
- """)
456
-
457
- with gr.Tabs():
458
-
459
- # ── Tab 1: Single photo ──────────────────────────────
460
- with gr.TabItem("📷 Single Photo → 3D"):
461
-
462
- img1 = gr.Image(
463
- type="filepath", label="// upload photo",
464
- sources=["upload", "webcam"], height=260
465
- )
466
- with gr.Accordion("📸 Tips for best results", open=False):
467
- gr.Markdown("""
468
- - Any size photo — app auto-resizes to 512px before processing
469
- - Clear, well-lit subject gives the best 3D quality
470
- - Works on: objects, rooms, food, people, landscapes
471
- - **ZeroGPU hardware must be selected in your Space's Settings → Hardware** (it isn't automatic just because it's free now)
472
- - **Free HF account:** 5 min of GPU time per day total (~3 conversions at current settings) — resets 24h after your first run
473
- - **Not signed in:** only 2 min/day — sign in to a free HF account for more
474
- - **PRO/Team/Enterprise:** much larger daily quota, plus pay-as-you-go
475
- """)
476
-
477
- btn1 = gr.Button("✦ Build My 3D Scene", variant="primary", size="lg")
478
- st1 = gr.Textbox(
479
- label="// status", interactive=False, lines=2,
480
- placeholder="Upload a photo and press the button…"
481
- )
482
-
483
- # 3D viewer — always visible, waits for content
484
- gr.HTML(VIEWER_SHELL)
485
-
486
- # Real, native Gradio download — works regardless of whether
487
- # the custom WebGL preview above loads, so you always have a
488
- # reliable way to get the file and open it in another app.
489
- dl1 = gr.DownloadButton("⬇ Download .ply", visible=False)
490
-
491
- # Hidden trigger — a plain Textbox (not gr.HTML). We used to
492
- # push a <script> tag through a gr.HTML update, but browsers
493
- # (and Gradio's Svelte renderer) never execute <script> tags
494
- # inserted via innerHTML on a dynamic update — only on the
495
- # page's *initial* load. That's almost certainly why nothing
496
- # ever fired. A Textbox's .change(js=...) event is Gradio's
497
- # actual supported mechanism for running JS off a Python
498
- # return value, and reliably passes the exact string we set —
499
- # no ambiguity, no timing races.
500
- url_holder = gr.Textbox(value="", visible=False, elem_id="sw-url-holder")
501
-
502
- # ── Tab 2: Two angles + animation ───────────────────
503
- with gr.TabItem("🎬 Two Angles → Animation"):
504
-
505
- gr.HTML("""
506
- <div style="background:rgba(139,92,246,0.06);
507
- border:1px solid rgba(139,92,246,0.18);
508
- border-radius:12px;padding:1rem 1.2rem;margin-bottom:1rem;">
509
- <div style="font-size:0.62rem;color:#8b5cf6;
510
- letter-spacing:0.1em;margin-bottom:0.4rem;">✦ HOW THIS WORKS</div>
511
- <div style="font-size:0.72rem;color:rgba(180,200,255,0.55);line-height:1.65;">
512
- Upload 2 photos of the same object from different angles.
513
- Both convert to 3D. Enable animation to get a downloadable HTML file —
514
- open it on any phone, camera flies between both angles in a loop.
515
- The HTML has a <strong style="color:#dce8ff;">⬇ SAVE HTML</strong> button inside.
516
- </div>
517
- </div>
518
- """)
519
-
520
- with gr.Row():
521
- imgA = gr.Image(
522
- type="filepath", label="// angle 1 — front / left",
523
- sources=["upload"], height=220
524
- )
525
- imgB = gr.Image(
526
- type="filepath", label="// angle 2 — back / right",
527
- sources=["upload"], height=220
528
- )
529
-
530
- anim_toggle = gr.Checkbox(
531
- label="✦ Generate camera animation HTML",
532
- value=True,
533
- info="Downloadable .html — camera flies between angles in a loop, has ⬇ SAVE HTML inside"
534
- )
535
- btn2 = gr.Button("✦ Build 3D + Animation", variant="primary", size="lg")
536
- st2 = gr.Textbox(
537
- label="// status", interactive=False, lines=3,
538
- placeholder="Upload both photos and press the button…"
539
- )
540
-
541
- anim_file = gr.File(
542
- label="// animation .html — download & open on any phone",
543
- visible=False, file_types=[".html"]
544
- )
545
-
546
- gr.HTML("""
547
- <div style="margin-top:0.8rem;padding:0.9rem 1rem;
548
- background:rgba(34,211,160,0.04);
549
- border:1px solid rgba(34,211,160,0.1);
550
- border-radius:10px;font-size:0.65rem;
551
- color:rgba(180,200,255,0.38);line-height:1.8;">
552
- 📱 Download animation .html → open in Chrome or Safari on any phone → 3D plays.<br/>
553
- 📤 Share via WhatsApp / email — recipient just opens the file, no app needed.<br/>
554
- 💾 Tap <strong style="color:#dce8ff;">⬇ SAVE HTML</strong> inside to re-download anytime.
555
- </div>
556
- """)
557
-
558
- # ── Handlers ──────────────────────────────────────────────
559
- def handle_single(img):
560
- print(f"[handle_single] click received, img={img}", flush=True)
561
- if img is None:
562
- return "⚠ Please upload a photo first.", gr.update(visible=False), ""
563
- try:
564
- ply, status = run_sharp(img)
565
- except Exception as e:
566
- print(f"[handle_single] GPU call raised: {e}", flush=True)
567
- 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.)",
568
- gr.update(visible=False), "")
569
- print(f"[handle_single] run_sharp returned ply={ply!r} status={status!r}", flush=True)
570
- if ply:
571
- size_mb = round(os.path.getsize(ply) / 1024 / 1024, 2)
572
- dl_update = gr.update(value=ply, visible=True, label=f"⬇ Download .ply ({size_mb} MB)")
573
- payload = json.dumps({"url": make_ply_url(ply), "size_mb": size_mb, "ts": time.time()})
574
- return status, dl_update, payload
575
- return status, gr.update(visible=False), ""
576
-
577
- def handle_dual(a, b, do_anim):
578
- if a is None or b is None:
579
- return "⚠ Please upload BOTH photos.", gr.update(visible=False)
580
-
581
- try:
582
- ply1, s1 = run_sharp(a)
583
- except Exception as e:
584
- return f"⚠ GPU rejected angle 1: {str(e)}", gr.update(visible=False)
585
- if not ply1:
586
- return f"⚠ Angle 1 failed:\n{s1}", gr.update(visible=False)
587
-
588
- try:
589
- ply2, s2 = run_sharp(b)
590
- except Exception as e:
591
- return f"✓ Angle 1 done.\n⚠ GPU rejected angle 2: {str(e)}", gr.update(visible=False)
592
- if not ply2:
593
- return f"✓ Angle 1 done.\n⚠ Angle 2 failed:\n{s2}", gr.update(visible=False)
594
-
595
- msg = "✓ Both angles done!"
596
- html_out = gr.update(visible=False)
597
-
598
- if do_anim:
599
- try:
600
- content = generate_animation_html(ply1, ply2)
601
- tmp = tempfile.NamedTemporaryFile(
602
- suffix=".html", prefix="splatweb_anim_",
603
- delete=False, mode="w", encoding="utf-8"
604
- )
605
- tmp.write(content)
606
- tmp.close()
607
- html_out = gr.update(value=tmp.name, visible=True)
608
- msg += "\n✦ Animation HTML ready — download below and open on your phone!"
609
- except Exception as e:
610
- msg += f"\n⚠ Animation failed: {str(e)}"
611
-
612
- return msg, html_out
613
-
614
- btn1.click(fn=handle_single, inputs=[img1], outputs=[st1, dl1, url_holder])
615
- btn2.click(fn=handle_dual, inputs=[imgA, imgB, anim_toggle], outputs=[st2, anim_file])
616
-
617
- # This is the load-bearing wire for the preview: whenever url_holder's
618
- # value changes (including programmatically, from handle_single's
619
- # return), Gradio calls this JS function with that exact string — the
620
- # documented, reliable way to run custom JS off a Python result.
621
- url_holder.change(
622
- fn=None, inputs=[url_holder], outputs=[],
623
- js="""
624
- (payload) => {
625
- const log = (m) => { if (window.swLog) window.swLog(m); else console.log('[SplatWeb]', m); };
626
- if (!payload) { log('trigger cleared, nothing to load'); return; }
627
- try {
628
- const data = JSON.parse(payload);
629
- log('trigger fired -> ' + data.url);
630
- if (window.swLoadUrl) { window.swLoadUrl(data.url, data.size_mb); }
631
- else { log('ERROR: viewer script not ready yet — reload the page and try again'); }
632
- } catch (e) {
633
- log('ERROR parsing trigger payload: ' + e.message);
634
- }
635
- }
636
- """
637
- )
638
-
639
- gr.HTML("""
640
- <div style="text-align:center;padding:1.5rem 1rem;
641
- border-top:1px solid rgba(80,130,255,0.07);margin-top:1.5rem;">
642
- <p style="font-size:0.6rem;color:rgba(180,200,255,0.22);line-height:1.8;">
643
- SplatWeb · Apple SHARP · HuggingFace · 3D renders on your device GPU via WebGL
644
- </p>
645
- </div>
646
- """)
647
-
648
- if __name__ == "__main__":
649
- demo.launch(server_name="0.0.0.0", server_port=7860)