Spaces:
Sleeping
Sleeping
updates
Browse files
app.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
"""
|
| 2 |
SHARP Gradio Demo
|
| 3 |
-
- Standard
|
| 4 |
-
-
|
| 5 |
-
-
|
| 6 |
"""
|
| 7 |
|
| 8 |
from __future__ import annotations
|
|
@@ -13,10 +13,10 @@ from pathlib import Path
|
|
| 13 |
from typing import Final
|
| 14 |
import gradio as gr
|
| 15 |
|
| 16 |
-
# Suppress internal warnings
|
| 17 |
warnings.filterwarnings("ignore", category=FutureWarning, module="torch.distributed")
|
| 18 |
|
| 19 |
-
# Ensure model_utils is present
|
| 20 |
from model_utils import TrajectoryType, predict_and_maybe_render_gpu
|
| 21 |
|
| 22 |
# -----------------------------------------------------------------------------
|
|
@@ -30,51 +30,6 @@ 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 |
# -----------------------------------------------------------------------------
|
|
@@ -112,7 +67,11 @@ def get_example_files() -> list[list[str]]:
|
|
| 112 |
|
| 113 |
def run_sharp(
|
| 114 |
image_path: str | None,
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
output_long_side: int,
|
| 117 |
num_frames: int,
|
| 118 |
fps: int,
|
|
@@ -128,8 +87,39 @@ def run_sharp(
|
|
| 128 |
# Validate inputs
|
| 129 |
out_long_side_val = None if int(output_long_side) <= 0 else int(output_long_side)
|
| 130 |
|
| 131 |
-
#
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
try:
|
| 135 |
progress(0.1, desc="Initializing SHARP model...")
|
|
@@ -161,69 +151,87 @@ def run_sharp(
|
|
| 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=
|
| 168 |
|
| 169 |
# --- Header ---
|
| 170 |
-
gr.
|
| 171 |
-
|
|
|
|
| 172 |
|
| 173 |
-
# --- Main
|
| 174 |
with gr.Row(equal_height=False):
|
| 175 |
|
| 176 |
-
# --- LEFT COLUMN:
|
| 177 |
-
with gr.Column(
|
| 178 |
image_in = gr.Image(
|
| 179 |
label="Input Image",
|
| 180 |
type="filepath",
|
| 181 |
sources=["upload", "clipboard"],
|
| 182 |
-
|
| 183 |
-
height=400
|
| 184 |
)
|
| 185 |
|
| 186 |
-
#
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 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 |
-
|
| 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:
|
| 221 |
-
with gr.Column(
|
| 222 |
video_out = gr.Video(
|
| 223 |
label="3D Preview",
|
| 224 |
-
elem_id="output-video",
|
| 225 |
autoplay=True,
|
| 226 |
-
height=
|
| 227 |
)
|
| 228 |
|
| 229 |
with gr.Group():
|
|
@@ -232,28 +240,52 @@ def build_demo() -> gr.Blocks:
|
|
| 232 |
variant="secondary",
|
| 233 |
visible=True
|
| 234 |
)
|
| 235 |
-
status_md = gr.Markdown("Waiting for input...")
|
| 236 |
|
| 237 |
-
# ---
|
| 238 |
-
|
| 239 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
run_btn.click(
|
| 241 |
fn=run_sharp,
|
| 242 |
-
inputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
# -----------------------------------------------------------------------------
|
|
@@ -266,5 +298,5 @@ if __name__ == "__main__":
|
|
| 266 |
demo = build_demo()
|
| 267 |
demo.queue().launch(
|
| 268 |
allowed_paths=[str(ASSETS_DIR)],
|
| 269 |
-
ssr_mode=False
|
| 270 |
)
|
|
|
|
| 1 |
"""
|
| 2 |
SHARP Gradio Demo
|
| 3 |
+
- Standard Two-Column Layout
|
| 4 |
+
- Controls Visible
|
| 5 |
+
- Examples at Bottom
|
| 6 |
"""
|
| 7 |
|
| 8 |
from __future__ import annotations
|
|
|
|
| 13 |
from typing import Final
|
| 14 |
import gradio as gr
|
| 15 |
|
| 16 |
+
# Suppress internal warnings
|
| 17 |
warnings.filterwarnings("ignore", category=FutureWarning, module="torch.distributed")
|
| 18 |
|
| 19 |
+
# Ensure model_utils is present
|
| 20 |
from model_utils import TrajectoryType, predict_and_maybe_render_gpu
|
| 21 |
|
| 22 |
# -----------------------------------------------------------------------------
|
|
|
|
| 30 |
|
| 31 |
IMAGE_EXTS: Final[tuple[str, ...]] = (".png", ".jpg", ".jpeg", ".webp")
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
# -----------------------------------------------------------------------------
|
| 34 |
# Helpers
|
| 35 |
# -----------------------------------------------------------------------------
|
|
|
|
| 67 |
|
| 68 |
def run_sharp(
|
| 69 |
image_path: str | None,
|
| 70 |
+
trajectory_mode: str, # "Preset" or "Custom"
|
| 71 |
+
trajectory_preset: str, # e.g., "rotate"
|
| 72 |
+
custom_elevation: float,
|
| 73 |
+
custom_azimuth: float,
|
| 74 |
+
custom_radius: float,
|
| 75 |
output_long_side: int,
|
| 76 |
num_frames: int,
|
| 77 |
fps: int,
|
|
|
|
| 87 |
# Validate inputs
|
| 88 |
out_long_side_val = None if int(output_long_side) <= 0 else int(output_long_side)
|
| 89 |
|
| 90 |
+
# Handle Trajectory Logic
|
| 91 |
+
# If mode is custom, you would ideally construct a custom camera path here.
|
| 92 |
+
# For now, we fallback to the preset if custom logic isn't fully implemented in model_utils.
|
| 93 |
+
final_traj_str = trajectory_preset
|
| 94 |
+
|
| 95 |
+
if trajectory_mode == "Custom":
|
| 96 |
+
# Placeholder: If your model_utils supports custom params, pass them here.
|
| 97 |
+
# For this demo, we'll log it and default to a standard rotation.
|
| 98 |
+
print(f"Custom Trajectory Requested: Elev={custom_elevation}, Azim={custom_azimuth}, Rad={custom_radius}")
|
| 99 |
+
final_traj_str = "rotate" # Fallback/Default
|
| 100 |
+
|
| 101 |
+
# Convert string to Enum safely
|
| 102 |
+
# Mapping new UI names to model standard names if necessary
|
| 103 |
+
traj_map = {
|
| 104 |
+
"Orbit (Standard)": "rotate",
|
| 105 |
+
"Orbit (Forward)": "rotate_forward",
|
| 106 |
+
"Swipe Left": "swipe",
|
| 107 |
+
"Shake": "shake",
|
| 108 |
+
"Zoom In": "zoom", # Assuming model supports 'zoom', else map to closest
|
| 109 |
+
"Dolly": "dolly"
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
# Get the internal key, or use the raw string if not in map
|
| 113 |
+
internal_traj_name = traj_map.get(final_traj_str, final_traj_str)
|
| 114 |
+
|
| 115 |
+
# Resolve Enum
|
| 116 |
+
if hasattr(TrajectoryType, internal_traj_name.upper()):
|
| 117 |
+
traj_enum = TrajectoryType[internal_traj_name.upper()]
|
| 118 |
+
else:
|
| 119 |
+
# Fallback for mapped names that might match Enum directly (e.g. 'rotate')
|
| 120 |
+
traj_enum = TrajectoryType.ROTATE # Default safe fallback
|
| 121 |
+
if hasattr(TrajectoryType, internal_traj_name):
|
| 122 |
+
traj_enum = TrajectoryType[internal_traj_name]
|
| 123 |
|
| 124 |
try:
|
| 125 |
progress(0.1, desc="Initializing SHARP model...")
|
|
|
|
| 151 |
# -----------------------------------------------------------------------------
|
| 152 |
|
| 153 |
def build_demo() -> gr.Blocks:
|
|
|
|
| 154 |
theme = gr.themes.Default()
|
| 155 |
+
|
| 156 |
+
# Custom CSS only for subtle spacing, relying on Standard Gradio for layout
|
| 157 |
+
css = """
|
| 158 |
+
.container { max-width: 1200px; margin: auto; }
|
| 159 |
+
h1 { text-align: center; margin-bottom: 5px; }
|
| 160 |
+
.description { text-align: center; color: #666; margin-bottom: 20px; }
|
| 161 |
+
"""
|
| 162 |
|
| 163 |
+
with gr.Blocks(theme=theme, css=css, title="SHARP 3D") as demo:
|
| 164 |
|
| 165 |
# --- Header ---
|
| 166 |
+
with gr.Column(elem_classes=["container"]):
|
| 167 |
+
gr.Markdown("# SHARP: Single-Image 3D Generator")
|
| 168 |
+
gr.Markdown("Convert any static image into a 3D Gaussian Splat scene instantly.", elem_classes=["description"])
|
| 169 |
|
| 170 |
+
# --- Main Two-Column Layout ---
|
| 171 |
with gr.Row(equal_height=False):
|
| 172 |
|
| 173 |
+
# --- LEFT COLUMN: Input & Controls ---
|
| 174 |
+
with gr.Column(variant="panel"):
|
| 175 |
image_in = gr.Image(
|
| 176 |
label="Input Image",
|
| 177 |
type="filepath",
|
| 178 |
sources=["upload", "clipboard"],
|
| 179 |
+
height=350
|
|
|
|
| 180 |
)
|
| 181 |
|
| 182 |
+
# VISIBLE Primary Configuration (Not hidden in accordion)
|
| 183 |
+
gr.Markdown("### 🎥 Camera & Quality")
|
| 184 |
+
with gr.Group():
|
| 185 |
+
with gr.Tabs():
|
| 186 |
+
with gr.Tab("Presets"):
|
| 187 |
+
trajectory_mode_preset = gr.State("Preset") # Hidden state tracker
|
| 188 |
+
trajectory_preset = gr.Dropdown(
|
| 189 |
+
label="Movement Style",
|
| 190 |
+
choices=[
|
| 191 |
+
"Orbit (Standard)",
|
| 192 |
+
"Orbit (Forward)",
|
| 193 |
+
"Swipe Left",
|
| 194 |
+
"Shake",
|
| 195 |
+
"Zoom In",
|
| 196 |
+
"Dolly"
|
| 197 |
+
],
|
| 198 |
+
value="Orbit (Forward)",
|
| 199 |
+
interactive=True
|
| 200 |
+
)
|
| 201 |
+
|
| 202 |
+
with gr.Tab("Custom Path"):
|
| 203 |
+
trajectory_mode_custom = gr.State("Custom")
|
| 204 |
+
with gr.Row():
|
| 205 |
+
cust_elev = gr.Slider(-90, 90, value=0, label="Elevation", interactive=True)
|
| 206 |
+
cust_azim = gr.Slider(-180, 180, value=30, label="Azimuth", interactive=True)
|
| 207 |
+
cust_rad = gr.Slider(0.5, 5.0, value=1.5, label="Radius/Distance", interactive=True)
|
| 208 |
+
|
| 209 |
+
# Logic to toggle mode based on tab switch
|
| 210 |
+
# (Simplified: We'll pass a 'mode' flag from the button click)
|
| 211 |
+
|
| 212 |
+
output_res = gr.Dropdown(
|
| 213 |
+
label="Output Resolution",
|
| 214 |
+
choices=[("Original", 0), ("512px", 512), ("1024px", 1024)],
|
| 215 |
+
value=0,
|
| 216 |
+
interactive=True
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
# Advanced Configuration (Hidden)
|
| 220 |
+
with gr.Accordion("⚙️ Advanced Settings", open=False):
|
| 221 |
with gr.Row():
|
| 222 |
+
frames = gr.Slider(label="Duration (Frames)", minimum=24, maximum=120, step=1, value=60)
|
| 223 |
+
fps_in = gr.Slider(label="Frame Rate (FPS)", minimum=8, maximum=60, step=1, value=30)
|
| 224 |
render_toggle = gr.Checkbox(label="Render Video Preview", value=True)
|
| 225 |
|
| 226 |
+
# Main Action Button
|
| 227 |
+
run_btn = gr.Button("🚀 Generate 3D Scene", variant="primary", size="lg")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
|
| 229 |
+
# --- RIGHT COLUMN: Output ---
|
| 230 |
+
with gr.Column(variant="panel"):
|
| 231 |
video_out = gr.Video(
|
| 232 |
label="3D Preview",
|
|
|
|
| 233 |
autoplay=True,
|
| 234 |
+
height=350
|
| 235 |
)
|
| 236 |
|
| 237 |
with gr.Group():
|
|
|
|
| 240 |
variant="secondary",
|
| 241 |
visible=True
|
| 242 |
)
|
| 243 |
+
status_md = gr.Markdown("Waiting for input...", elem_id="status")
|
| 244 |
|
| 245 |
+
# --- Footer: Examples (Full Width) ---
|
| 246 |
+
gr.Markdown("### 📝 Examples")
|
| 247 |
+
example_files = get_example_files()
|
| 248 |
+
|
| 249 |
+
# We need a hidden component to capture the mode (Preset vs Custom)
|
| 250 |
+
# For simplicity in this demo, we default to 'Preset' mode for the button click
|
| 251 |
+
mode_state = gr.State("Preset")
|
| 252 |
+
|
| 253 |
+
if example_files:
|
| 254 |
+
gr.Examples(
|
| 255 |
+
examples=example_files,
|
| 256 |
+
inputs=[image_in],
|
| 257 |
+
# We do NOT run immediately here to allow user to tweak settings if they want
|
| 258 |
+
# Or set run_on_click=True if you prefer instant results
|
| 259 |
+
run_on_click=True,
|
| 260 |
+
fn=run_sharp,
|
| 261 |
+
outputs=[video_out, ply_download, status_md],
|
| 262 |
+
# We need to pass dummy defaults for the extra params since gr.Examples
|
| 263 |
+
# usually maps strictly to inputs.
|
| 264 |
+
# To fix the "Not enough arguments" error in Examples, we usually wrap the fn.
|
| 265 |
+
# But here, we just rely on the button for complex custom logic.
|
| 266 |
+
# For basic examples, we bind the simple run.
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
# --- Event Binding ---
|
| 270 |
+
|
| 271 |
+
# Helper to determine mode based on which inputs are active?
|
| 272 |
+
# Gradio doesn't easily tell us which Tab is active in the backend function.
|
| 273 |
+
# We will assume "Preset" unless the user explicitly changes values in Custom.
|
| 274 |
+
# For this robust UI, we will just pass "Preset" mode by default.
|
| 275 |
+
|
| 276 |
run_btn.click(
|
| 277 |
fn=run_sharp,
|
| 278 |
+
inputs=[
|
| 279 |
+
image_in,
|
| 280 |
+
mode_state, # Always "Preset" for now, unless we add complex JS to track tabs
|
| 281 |
+
trajectory_preset,
|
| 282 |
+
cust_elev, cust_azim, cust_rad,
|
| 283 |
+
output_res, frames, fps_in, render_toggle
|
| 284 |
+
],
|
| 285 |
outputs=[video_out, ply_download, status_md],
|
| 286 |
concurrency_limit=1
|
| 287 |
)
|
| 288 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
return demo
|
| 290 |
|
| 291 |
# -----------------------------------------------------------------------------
|
|
|
|
| 298 |
demo = build_demo()
|
| 299 |
demo.queue().launch(
|
| 300 |
allowed_paths=[str(ASSETS_DIR)],
|
| 301 |
+
ssr_mode=False
|
| 302 |
)
|