VISHAL18for4 commited on
Commit
0d6285f
·
verified ·
1 Parent(s): 544b2bd

Delete app.py

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