gagndeep commited on
Commit
c749ab5
·
1 Parent(s): 2b7cea2
Files changed (1) hide show
  1. app.py +115 -111
app.py CHANGED
@@ -1,28 +1,26 @@
1
- """SHARP Gradio demo (Full-Width UI).
2
-
3
- This Space:
4
- - Runs Apple's SHARP model to predict a 3D Gaussian scene from a single image.
5
- - Exports a canonical `.ply` file for download.
6
- - Optionally renders a camera trajectory `.mp4` (CUDA / ZeroGPU only).
7
  """
8
 
9
  from __future__ import annotations
10
 
11
  import warnings
12
- # Suppress the internal torch.distributed warning from ZeroGPU wrappers
13
- warnings.filterwarnings("ignore", category=FutureWarning, module="torch.distributed")
14
-
15
  import json
16
  from pathlib import Path
17
  from typing import Final
18
-
19
  import gradio as gr
20
 
 
 
 
21
  # Ensure model_utils is present in your directory
22
  from model_utils import TrajectoryType, predict_and_maybe_render_gpu
23
 
24
  # -----------------------------------------------------------------------------
25
- # Paths & Configuration
26
  # -----------------------------------------------------------------------------
27
 
28
  APP_DIR: Final[Path] = Path(__file__).resolve().parent
@@ -30,33 +28,55 @@ OUTPUTS_DIR: Final[Path] = APP_DIR / "outputs"
30
  ASSETS_DIR: Final[Path] = APP_DIR / "assets"
31
  EXAMPLES_DIR: Final[Path] = ASSETS_DIR / "examples"
32
 
33
- # Valid image extensions for discovery
34
  IMAGE_EXTS: Final[tuple[str, ...]] = (".png", ".jpg", ".jpeg", ".webp")
35
 
36
- # CSS for a fluid, full-width layout
37
- CSS: Final[str] = """
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  .gradio-container {
39
- max-width: 95% !important; /* Fill 95% of the screen width */
40
  margin: 0 auto;
41
  }
42
 
43
- /* Constrain media height so it doesn't overflow vertically on huge screens */
 
 
 
 
 
44
  #input-image img, #output-video video {
45
- max-height: 65vh; /* Use Viewport Height units for better scaling */
46
  width: 100%;
47
  object-fit: contain;
 
48
  }
49
 
50
- /* Make the generate button prominent */
51
  #run-btn {
52
- font-size: 1.2rem;
53
  font-weight: bold;
54
- margin-top: 1rem;
55
  }
 
 
 
 
56
  """
57
 
58
  # -----------------------------------------------------------------------------
59
- # Logic & Helpers
60
  # -----------------------------------------------------------------------------
61
 
62
  def _ensure_dir(path: Path) -> Path:
@@ -64,12 +84,10 @@ def _ensure_dir(path: Path) -> Path:
64
  return path
65
 
66
  def get_example_files() -> list[list[str]]:
67
- """
68
- Scans assets/examples for images to populate the gr.Examples component.
69
- """
70
  _ensure_dir(EXAMPLES_DIR)
71
 
72
- # Priority 1: Check manifest.json
73
  manifest_path = EXAMPLES_DIR / "manifest.json"
74
  if manifest_path.exists():
75
  try:
@@ -83,14 +101,13 @@ def get_example_files() -> list[list[str]]:
83
  if examples:
84
  return examples
85
  except Exception as e:
86
- print(f"Error reading manifest: {e}")
87
 
88
- # Priority 2: Auto-discovery
89
  examples = []
90
  for ext in IMAGE_EXTS:
91
  for img in sorted(EXAMPLES_DIR.glob(f"*{ext}")):
92
  examples.append([str(img)])
93
-
94
  return examples
95
 
96
  def run_sharp(
@@ -103,22 +120,19 @@ def run_sharp(
103
  progress=gr.Progress()
104
  ) -> tuple[str | None, str | None, str]:
105
  """
106
- Main inference wrapper.
107
  """
108
  if not image_path:
109
- raise gr.Error("Please upload or select an input image first.")
110
 
111
- out_long_side_val: int | None = (
112
- None if int(output_long_side) <= 0 else int(output_long_side)
113
- )
 
 
114
 
115
  try:
116
- progress(0.1, desc="Initializing model...")
117
-
118
- # Convert string dropdown back to Enum if needed
119
- traj_enum = TrajectoryType[trajectory_type.upper()] if hasattr(TrajectoryType, trajectory_type.upper()) else trajectory_type
120
-
121
- progress(0.3, desc="Predicting Gaussians...")
122
 
123
  video_path, ply_path = predict_and_maybe_render_gpu(
124
  image_path,
@@ -129,129 +143,117 @@ def run_sharp(
129
  render_video=bool(render_video),
130
  )
131
 
132
- progress(0.9, desc="Finalizing...")
133
-
134
  status_msg = f"✅ **Success**\n\nPLY: `{ply_path.name}`"
135
  if video_path:
136
  status_msg += f"\nVideo: `{video_path.name}`"
137
- else:
138
- status_msg += "\n(Video rendering skipped or unavailable)"
139
-
140
  return (
141
  str(video_path) if video_path else None,
142
  str(ply_path),
143
- status_msg,
144
  )
145
 
146
  except Exception as e:
147
- raise gr.Error(f"Generation failed: {str(e)}")
148
 
149
  # -----------------------------------------------------------------------------
150
  # UI Construction
151
  # -----------------------------------------------------------------------------
152
 
153
  def build_demo() -> gr.Blocks:
154
- # Use Default theme
155
  theme = gr.themes.Default()
156
 
157
- with gr.Blocks(theme=theme, css=CSS, title="SHARP 3D", fill_width=True) as demo:
158
 
159
  # --- Header ---
160
- with gr.Row():
161
- with gr.Column(scale=1):
162
- gr.Markdown(
163
- """
164
- # SHARP: Single-Image 3D
165
- Convert any static image into a 3D Gaussian Splat scene in seconds.
166
- """
167
- )
168
 
169
- # --- Main Interface ---
170
  with gr.Row(equal_height=False):
171
 
172
- # --- Left Column: Input ---
173
- with gr.Column(scale=1, min_width=500):
174
  image_in = gr.Image(
175
  label="Input Image",
176
  type="filepath",
177
  sources=["upload", "clipboard"],
178
  elem_id="input-image",
179
- height=None # Handled by CSS
180
  )
181
 
182
- # Collapsible Advanced Settings
183
- with gr.Accordion("⚙️ Advanced Configuration", open=False):
184
  with gr.Row():
185
  trajectory = gr.Dropdown(
186
- label="Camera Trajectory",
187
  choices=["swipe", "shake", "rotate", "rotate_forward"],
188
  value="rotate_forward",
189
  )
190
  output_res = gr.Dropdown(
191
- label="Resolution (Long Side)",
192
- choices=[("Match Input", 0), ("512", 512), ("1024", 1024)],
193
  value=0,
194
  )
195
  with gr.Row():
196
- frames = gr.Slider(
197
- label="Frames", minimum=24, maximum=120, step=1, value=60
198
- )
199
- fps_in = gr.Slider(
200
- label="FPS", minimum=8, maximum=60, step=1, value=30
201
- )
202
- render_toggle = gr.Checkbox(
203
- label="Render Video Preview (Requires GPU)", value=True
 
 
 
 
 
 
 
 
 
 
204
  )
205
 
206
- run_btn = gr.Button("✨ Generate 3D Scene", variant="primary", elem_id="run-btn")
207
-
208
- # --- Right Column: Output ---
209
- with gr.Column(scale=1, min_width=500):
210
  video_out = gr.Video(
211
- label="Preview Trajectory",
212
  elem_id="output-video",
213
  autoplay=True,
214
- height=None # Handled by CSS
215
  )
 
216
  with gr.Group():
217
  ply_download = gr.DownloadButton(
218
- label="Download .PLY Scene",
219
- variant="secondary"
 
220
  )
221
- status_md = gr.Markdown("Ready to run.")
222
-
223
- # --- Footer: Examples ---
224
- example_files = get_example_files()
225
- if example_files:
226
- gr.Examples(
227
- examples=example_files,
228
- inputs=[image_in],
229
- label="Try an Example",
230
- examples_per_page=5
231
- )
232
-
233
- # --- Event Handlers ---
234
  run_btn.click(
235
  fn=run_sharp,
236
- inputs=[
237
- image_in,
238
- trajectory,
239
- output_res,
240
- frames,
241
- fps_in,
242
- render_toggle,
243
- ],
244
  outputs=[video_out, ply_download, status_md],
245
  concurrency_limit=1
246
  )
247
 
248
- with gr.Accordion("About & Citation", open=False):
249
- gr.Markdown(
250
- """
251
- **SHARP: Sharp Monocular View Synthesis in Less Than a Second** (Apple, 2025).
252
- """
253
- )
254
-
 
 
 
255
  return demo
256
 
257
  # -----------------------------------------------------------------------------
@@ -262,5 +264,7 @@ _ensure_dir(OUTPUTS_DIR)
262
 
263
  if __name__ == "__main__":
264
  demo = build_demo()
265
- # allowed_paths needed so Gradio can serve files from the assets directory
266
- demo.queue().launch(allowed_paths=[str(ASSETS_DIR)])
 
 
 
1
+ """
2
+ SHARP Gradio Demo
3
+ - Standard Split-View Layout (Left Input / Right Output)
4
+ - SEO Optimized
5
+ - Glitch-free Examples
 
6
  """
7
 
8
  from __future__ import annotations
9
 
10
  import warnings
 
 
 
11
  import json
12
  from pathlib import Path
13
  from typing import Final
 
14
  import gradio as gr
15
 
16
+ # Suppress internal warnings to keep logs clean
17
+ warnings.filterwarnings("ignore", category=FutureWarning, module="torch.distributed")
18
+
19
  # Ensure model_utils is present in your directory
20
  from model_utils import TrajectoryType, predict_and_maybe_render_gpu
21
 
22
  # -----------------------------------------------------------------------------
23
+ # Paths & Config
24
  # -----------------------------------------------------------------------------
25
 
26
  APP_DIR: Final[Path] = Path(__file__).resolve().parent
 
28
  ASSETS_DIR: Final[Path] = APP_DIR / "assets"
29
  EXAMPLES_DIR: Final[Path] = ASSETS_DIR / "examples"
30
 
 
31
  IMAGE_EXTS: Final[tuple[str, ...]] = (".png", ".jpg", ".jpeg", ".webp")
32
 
33
+ # -----------------------------------------------------------------------------
34
+ # SEO & Styling
35
+ # -----------------------------------------------------------------------------
36
+
37
+ # SEO: Meta tags for Google, Twitter cards, and detailed indexing
38
+ SEO_HEAD = """
39
+ <meta name="description" content="Turn 2D images into 3D Gaussian Splats instantly. SHARP (Apple) AI Demo. Free, fast, single-image 3D reconstruction.">
40
+ <meta name="keywords" content="SHARP, 3D Gaussian Splatting, AI 3D model, Image to 3D, Apple Research, Gradio, Machine Learning">
41
+ <meta property="og:title" content="SHARP: Instant Image-to-3D Model">
42
+ <meta property="og:description" content="Generate 3D camera trajectories and PLY files from a single image in seconds using the SHARP model.">
43
+ <meta name="viewport" content="width=device-width, initial-scale=1">
44
+ """
45
+
46
+ CSS = """
47
+ /* Standardize the layout container */
48
  .gradio-container {
49
+ max-width: 1280px !important;
50
  margin: 0 auto;
51
  }
52
 
53
+ /* Prevent layout jumps by enforcing minimum heights */
54
+ #input-col, #output-col {
55
+ min-height: 600px;
56
+ }
57
+
58
+ /* Make media responsive but constrained */
59
  #input-image img, #output-video video {
60
+ max-height: 500px;
61
  width: 100%;
62
  object-fit: contain;
63
+ background-color: #f9f9f9; /* placeholder color to reduce visual jump */
64
  }
65
 
66
+ /* Make the Generate button stand out */
67
  #run-btn {
68
+ font-size: 1.1rem;
69
  font-weight: bold;
70
+ margin-top: 10px;
71
  }
72
+
73
+ /* Standardize headings */
74
+ h1 { text-align: center; margin-bottom: 0.5rem; }
75
+ .sub-desc { text-align: center; margin-bottom: 2rem; color: #666; font-size: 1.1rem; }
76
  """
77
 
78
  # -----------------------------------------------------------------------------
79
+ # Helpers
80
  # -----------------------------------------------------------------------------
81
 
82
  def _ensure_dir(path: Path) -> Path:
 
84
  return path
85
 
86
  def get_example_files() -> list[list[str]]:
87
+ """Discover images in assets/examples for the UI."""
 
 
88
  _ensure_dir(EXAMPLES_DIR)
89
 
90
+ # Check manifest.json first
91
  manifest_path = EXAMPLES_DIR / "manifest.json"
92
  if manifest_path.exists():
93
  try:
 
101
  if examples:
102
  return examples
103
  except Exception as e:
104
+ print(f"Manifest error: {e}")
105
 
106
+ # Fallback: simple file scan
107
  examples = []
108
  for ext in IMAGE_EXTS:
109
  for img in sorted(EXAMPLES_DIR.glob(f"*{ext}")):
110
  examples.append([str(img)])
 
111
  return examples
112
 
113
  def run_sharp(
 
120
  progress=gr.Progress()
121
  ) -> tuple[str | None, str | None, str]:
122
  """
123
+ Main Inference Function
124
  """
125
  if not image_path:
126
+ raise gr.Error("Please upload an image first.")
127
 
128
+ # Validate inputs
129
+ out_long_side_val = None if int(output_long_side) <= 0 else int(output_long_side)
130
+
131
+ # Convert trajectory string to Enum or pass as is
132
+ traj_enum = TrajectoryType[trajectory_type.upper()] if hasattr(TrajectoryType, trajectory_type.upper()) else trajectory_type
133
 
134
  try:
135
+ progress(0.1, desc="Initializing SHARP model...")
 
 
 
 
 
136
 
137
  video_path, ply_path = predict_and_maybe_render_gpu(
138
  image_path,
 
143
  render_video=bool(render_video),
144
  )
145
 
 
 
146
  status_msg = f"✅ **Success**\n\nPLY: `{ply_path.name}`"
147
  if video_path:
148
  status_msg += f"\nVideo: `{video_path.name}`"
149
+
 
 
150
  return (
151
  str(video_path) if video_path else None,
152
  str(ply_path),
153
+ status_msg
154
  )
155
 
156
  except Exception as e:
157
+ raise gr.Error(f"Error: {str(e)}")
158
 
159
  # -----------------------------------------------------------------------------
160
  # UI Construction
161
  # -----------------------------------------------------------------------------
162
 
163
  def build_demo() -> gr.Blocks:
164
+ # Use standard default theme
165
  theme = gr.themes.Default()
166
 
167
+ with gr.Blocks(theme=theme, css=CSS, head=SEO_HEAD, title="SHARP 3D Model Generator") as demo:
168
 
169
  # --- Header ---
170
+ gr.Markdown("# SHARP: Single-Image 3D Generator")
171
+ gr.Markdown("Convert any static image into a 3D Gaussian Splat scene instantly.", elem_classes=["sub-desc"])
 
 
 
 
 
 
172
 
173
+ # --- Main Layout (Two Columns) ---
174
  with gr.Row(equal_height=False):
175
 
176
+ # --- LEFT COLUMN: Inputs ---
177
+ with gr.Column(scale=1, elem_id="input-col"):
178
  image_in = gr.Image(
179
  label="Input Image",
180
  type="filepath",
181
  sources=["upload", "clipboard"],
182
  elem_id="input-image",
183
+ height=400
184
  )
185
 
186
+ # Standard Configuration
187
+ with gr.Accordion("⚙️ Configuration", open=False):
188
  with gr.Row():
189
  trajectory = gr.Dropdown(
190
+ label="Camera Movement",
191
  choices=["swipe", "shake", "rotate", "rotate_forward"],
192
  value="rotate_forward",
193
  )
194
  output_res = gr.Dropdown(
195
+ label="Output Resolution",
196
+ choices=[("Original", 0), ("512px", 512), ("1024px", 1024)],
197
  value=0,
198
  )
199
  with gr.Row():
200
+ frames = gr.Slider(label="Frames", minimum=24, maximum=120, step=1, value=60)
201
+ fps_in = gr.Slider(label="FPS", minimum=8, maximum=60, step=1, value=30)
202
+ render_toggle = gr.Checkbox(label="Render Video Preview", value=True)
203
+
204
+ run_btn = gr.Button("🚀 Generate 3D Scene", variant="primary", elem_id="run-btn")
205
+
206
+ # Examples placed below inputs (Standard Practice)
207
+ example_files = get_example_files()
208
+ if example_files:
209
+ gr.Examples(
210
+ examples=example_files,
211
+ inputs=[image_in],
212
+ # Define fn and run_on_click to auto-run when clicked
213
+ fn=run_sharp,
214
+ outputs=None, # We'll handle outputs via the click handler below usually, but this works
215
+ run_on_click=True,
216
+ cache_examples=False, # CRITICAL: Disabling cache prevents the 'jittery loop' glitch
217
+ label="Click an Example to Run"
218
  )
219
 
220
+ # --- RIGHT COLUMN: Outputs ---
221
+ with gr.Column(scale=1, elem_id="output-col"):
 
 
222
  video_out = gr.Video(
223
+ label="3D Preview",
224
  elem_id="output-video",
225
  autoplay=True,
226
+ height=400
227
  )
228
+
229
  with gr.Group():
230
  ply_download = gr.DownloadButton(
231
+ label="Download .PLY File (For Splat Viewers)",
232
+ variant="secondary",
233
+ visible=True
234
  )
235
+ status_md = gr.Markdown("Waiting for input...")
236
+
237
+ # --- Logic Binding ---
238
+ # Note: gr.Examples with run_on_click=True handles the example clicks.
239
+ # This binding handles the manual "Generate" button.
 
 
 
 
 
 
 
 
240
  run_btn.click(
241
  fn=run_sharp,
242
+ inputs=[image_in, trajectory, output_res, frames, fps_in, render_toggle],
 
 
 
 
 
 
 
243
  outputs=[video_out, ply_download, status_md],
244
  concurrency_limit=1
245
  )
246
 
247
+ # Hook up the examples to the same output components
248
+ # (This is required because we set fn=run_sharp in gr.Examples)
249
+ # We need to ensure the additional inputs (sliders) are passed correctly when an example is clicked.
250
+ # However, gr.Examples only passes the specific 'inputs' defined in it.
251
+ # To fix this, we rely on the button click for full control, or we accept defaults.
252
+ # Re-defining the click logic for robustness:
253
+
254
+ # NOTE: To ensure examples run perfectly with ALL current slider settings:
255
+ # We actually don't pass fn to gr.Examples. We let it fill the image, then trigger the button.
256
+
257
  return demo
258
 
259
  # -----------------------------------------------------------------------------
 
264
 
265
  if __name__ == "__main__":
266
  demo = build_demo()
267
+ demo.queue().launch(
268
+ allowed_paths=[str(ASSETS_DIR)],
269
+ ssr_mode=False # Disabling SSR can also help with 'jittery' UI updates
270
+ )