| | """SHARP Gradio demo (minimal, responsive UI). |
| | |
| | This Space: |
| | - Runs Apple's SHARP model to predict a 3D Gaussian scene from a single image. |
| | - Exports a canonical `.ply` file for download. |
| | - Optionally renders a camera trajectory `.mp4` (CUDA / ZeroGPU only). |
| | |
| | Precompiled examples |
| | Place precompiled examples under `assets/examples/`. |
| | |
| | Recommended structure (matching stem): |
| | assets/examples/<name>.jpg|png|webp |
| | assets/examples/<name>.mp4 |
| | assets/examples/<name>.ply |
| | |
| | Optional manifest (assets/examples/manifest.json): |
| | [ |
| | {"label": "Desk", "image": "desk.jpg", "video": "desk.mp4", "ply": "desk.ply"}, |
| | ... |
| | ] |
| | """ |
| |
|
| | from __future__ import annotations |
| |
|
| | import json |
| | from dataclasses import dataclass |
| | from pathlib import Path |
| | from typing import Final |
| |
|
| | import gradio as gr |
| |
|
| | from model_utils import TrajectoryType, predict_and_maybe_render_gpu |
| |
|
| | |
| | |
| | |
| |
|
| | APP_DIR: Final[Path] = Path(__file__).resolve().parent |
| | OUTPUTS_DIR: Final[Path] = APP_DIR / "outputs" |
| | ASSETS_DIR: Final[Path] = APP_DIR / "assets" |
| | EXAMPLES_DIR: Final[Path] = ASSETS_DIR / "examples" |
| |
|
| | IMAGE_EXTS: Final[tuple[str, ...]] = (".png", ".jpg", ".jpeg", ".webp") |
| | DEFAULT_QUEUE_MAX_SIZE: Final[int] = 32 |
| |
|
| | THEME: Final = gr.themes.Soft( |
| | primary_hue="indigo", |
| | secondary_hue="blue", |
| | neutral_hue="slate", |
| | ) |
| |
|
| | CSS: Final[str] = """ |
| | /* Keep layout stable when scrollbars appear/disappear */ |
| | html { scrollbar-gutter: stable; } |
| | |
| | /* Use normal document flow (no fixed-height viewport shell) */ |
| | html, body { height: auto; } |
| | body { overflow: auto; } |
| | |
| | /* Comfortable max width; still fills small screens */ |
| | .gradio-container { |
| | max-width: 1400px; |
| | margin: 0 auto; |
| | padding: 0.75rem 1rem 1rem; |
| | box-sizing: border-box; |
| | } |
| | |
| | /* Make media components responsive without stretching */ |
| | #run-image, #run-video, |
| | #examples-image, #examples-video { |
| | width: 100%; |
| | } |
| | |
| | /* Keep aspect ratio and prevent runaway vertical growth on tall viewports */ |
| | #run-image img, #examples-image img { |
| | width: 100%; |
| | height: auto; |
| | max-height: 70vh; |
| | object-fit: contain; |
| | } |
| | #run-video video, #examples-video video { |
| | width: 100%; |
| | height: auto; |
| | max-height: 70vh; |
| | object-fit: contain; |
| | } |
| | |
| | /* On very small screens, reduce max media height a bit */ |
| | @media (max-width: 640px) { |
| | #run-image img, #examples-image img, |
| | #run-video video, #examples-video video { |
| | max-height: 55vh; |
| | } |
| | } |
| | |
| | /* Reduce extra whitespace in markdown blocks */ |
| | .gr-markdown > :first-child { margin-top: 0 !important; } |
| | .gr-markdown > :last-child { margin-bottom: 0 !important; } |
| | """ |
| |
|
| | |
| | |
| | |
| |
|
| |
|
| | def _ensure_dir(path: Path) -> Path: |
| | path.mkdir(parents=True, exist_ok=True) |
| | return path |
| |
|
| |
|
| | @dataclass(frozen=True, slots=True) |
| | class ExampleSpec: |
| | """A precompiled example bundle (image + optional mp4 + optional ply).""" |
| |
|
| | label: str |
| | image: Path |
| | video: Path | None |
| | ply: Path | None |
| |
|
| |
|
| | def _normalize_key(path: str) -> str: |
| | """Normalize a path-like string for stable dictionary keys.""" |
| | try: |
| | return str(Path(path).resolve()) |
| | except Exception: |
| | return path |
| |
|
| |
|
| | def _load_manifest(manifest_path: Path) -> list[dict]: |
| | """Load manifest.json if present; return an empty list on errors.""" |
| | try: |
| | data = json.loads(manifest_path.read_text(encoding="utf-8")) |
| | if not isinstance(data, list): |
| | raise ValueError("manifest.json must contain a JSON list.") |
| | return [x for x in data if isinstance(x, dict)] |
| | except FileNotFoundError: |
| | return [] |
| | except Exception as e: |
| | |
| | print(f"[examples] Failed to parse manifest.json: {type(e).__name__}: {e}") |
| | return [] |
| |
|
| |
|
| | def discover_examples(examples_dir: Path) -> list[ExampleSpec]: |
| | """Discover example bundles under assets/examples/.""" |
| | _ensure_dir(examples_dir) |
| |
|
| | manifest_rows = _load_manifest(examples_dir / "manifest.json") |
| | if manifest_rows: |
| | specs: list[ExampleSpec] = [] |
| | for row in manifest_rows: |
| | label = str(row.get("label") or "Example").strip() or "Example" |
| | image_rel = row.get("image") |
| | if not image_rel: |
| | continue |
| |
|
| | image = (examples_dir / str(image_rel)).resolve() |
| | if not image.exists(): |
| | continue |
| |
|
| | video = None |
| | ply = None |
| | if row.get("video"): |
| | v = (examples_dir / str(row["video"])).resolve() |
| | if v.exists(): |
| | video = v |
| | if row.get("ply"): |
| | p = (examples_dir / str(row["ply"])).resolve() |
| | if p.exists(): |
| | ply = p |
| |
|
| | specs.append(ExampleSpec(label=label, image=image, video=video, ply=ply)) |
| | return specs |
| |
|
| | |
| | images: list[Path] = [] |
| | for ext in IMAGE_EXTS: |
| | images.extend(sorted(examples_dir.glob(f"*{ext}"))) |
| |
|
| | specs = [] |
| | for img in images: |
| | stem = img.stem |
| | video = examples_dir / f"{stem}.mp4" |
| | ply = examples_dir / f"{stem}.ply" |
| | specs.append( |
| | ExampleSpec( |
| | label=stem.replace("_", " ").strip() or stem, |
| | image=img.resolve(), |
| | video=video.resolve() if video.exists() else None, |
| | ply=ply.resolve() if ply.exists() else None, |
| | ) |
| | ) |
| | return specs |
| |
|
| |
|
| | _ensure_dir(OUTPUTS_DIR) |
| |
|
| | EXAMPLE_SPECS: Final[list[ExampleSpec]] = discover_examples(EXAMPLES_DIR) |
| | EXAMPLE_INDEX_BY_PATH: Final[dict[str, ExampleSpec]] = { |
| | _normalize_key(str(s.image)): s for s in EXAMPLE_SPECS |
| | } |
| | EXAMPLE_INDEX_BY_NAME: Final[dict[str, ExampleSpec]] = { |
| | s.image.name: s for s in EXAMPLE_SPECS |
| | } |
| |
|
| |
|
| | def load_example_assets( |
| | image_path: str | None, |
| | ) -> tuple[str | None, str | None, str | None, str]: |
| | """Return (image, video, ply_path, status) for the selected example image.""" |
| | if not image_path: |
| | return None, None, None, "No example selected." |
| |
|
| | spec = EXAMPLE_INDEX_BY_PATH.get(_normalize_key(image_path)) |
| | if spec is None: |
| | spec = EXAMPLE_INDEX_BY_NAME.get(Path(image_path).name) |
| |
|
| | if spec is None: |
| | return image_path, None, None, "No matching example bundle found." |
| |
|
| | video = str(spec.video) if spec.video is not None else None |
| | ply_path = str(spec.ply) if spec.ply is not None else None |
| |
|
| | missing: list[str] = [] |
| | if video is None: |
| | missing.append("MP4") |
| | if ply_path is None: |
| | missing.append("PLY") |
| |
|
| | msg = f"Loaded example: **{spec.label}**." |
| | if missing: |
| | msg += f" Missing: {', '.join(missing)}." |
| |
|
| | return str(spec.image), video, ply_path, msg |
| |
|
| |
|
| | def _validate_image(image_path: str | None) -> None: |
| | if not image_path: |
| | raise gr.Error("Upload an image first.") |
| |
|
| |
|
| | def run_sharp( |
| | image_path: str | None, |
| | trajectory_type: TrajectoryType, |
| | output_long_side: int, |
| | num_frames: int, |
| | fps: int, |
| | render_video: bool, |
| | ) -> tuple[str | None, str | None, str]: |
| | """Run SHARP inference and return (video_path, ply_path, status_markdown).""" |
| | _validate_image(image_path) |
| | out_long_side: int | None = ( |
| | None if int(output_long_side) <= 0 else int(output_long_side) |
| | ) |
| |
|
| | try: |
| | video_path, ply_path = predict_and_maybe_render_gpu( |
| | image_path, |
| | trajectory_type=trajectory_type, |
| | num_frames=int(num_frames), |
| | fps=int(fps), |
| | output_long_side=out_long_side, |
| | render_video=bool(render_video), |
| | ) |
| |
|
| | lines: list[str] = [f"**PLY:** `{ply_path.name}` (ready to download)"] |
| | if render_video: |
| | if video_path is None: |
| | lines.append("**Video:** not rendered (CUDA unavailable).") |
| | else: |
| | lines.append(f"**Video:** `{video_path.name}`") |
| | else: |
| | lines.append("**Video:** disabled.") |
| |
|
| | return ( |
| | str(video_path) if video_path is not None else None, |
| | str(ply_path), |
| | "\n".join(lines), |
| | ) |
| | except gr.Error: |
| | raise |
| | except Exception as e: |
| | raise gr.Error(f"SHARP failed: {type(e).__name__}: {e}") from e |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| |
|
| | def build_demo() -> gr.Blocks: |
| | with gr.Blocks( |
| | title="SHARP • Single-Image 3D Gaussian Prediction", |
| | elem_id="sharp-root", |
| | fill_height=True, |
| | ) as demo: |
| | gr.Markdown("## SHARP\nSingle-image **3D Gaussian scene** prediction.") |
| |
|
| | |
| | with gr.Column(elem_id="tabs-shell"): |
| | with gr.Tabs(): |
| | with gr.Tab("Run", id="run"): |
| | with gr.Column(elem_id="run-panel"): |
| | with gr.Row(equal_height=True, elem_id="run-media-row"): |
| | with gr.Column( |
| | scale=5, min_width=360, elem_id="run-left-col" |
| | ): |
| | image_in = gr.Image( |
| | label="Input image", |
| | type="filepath", |
| | sources=["upload"], |
| | elem_id="run-image", |
| | ) |
| |
|
| | with gr.Row(): |
| | trajectory = gr.Dropdown( |
| | label="Trajectory", |
| | choices=[ |
| | "swipe", |
| | "shake", |
| | "rotate", |
| | "rotate_forward", |
| | ], |
| | value="rotate_forward", |
| | ) |
| | output_res = gr.Dropdown( |
| | label="Output long side", |
| | info="0 = match input", |
| | choices=[ |
| | ("Match input", 0), |
| | ("512", 512), |
| | ("768", 768), |
| | ("1024", 1024), |
| | ("1280", 1280), |
| | ("1536", 1536), |
| | ], |
| | value=0, |
| | ) |
| |
|
| | with gr.Row(): |
| | frames = gr.Slider( |
| | label="Frames", |
| | minimum=24, |
| | maximum=120, |
| | step=1, |
| | value=60, |
| | ) |
| | fps_in = gr.Slider( |
| | label="FPS", |
| | minimum=8, |
| | maximum=60, |
| | step=1, |
| | value=30, |
| | ) |
| |
|
| | render_toggle = gr.Checkbox( |
| | label="Render MP4 (CUDA / ZeroGPU only)", |
| | value=True, |
| | ) |
| |
|
| | with gr.Column( |
| | scale=5, min_width=360, elem_id="run-right-col" |
| | ): |
| | video_out = gr.Video( |
| | label="Trajectory video (MP4)", |
| | elem_id="run-video", |
| | ) |
| | with gr.Row(elem_id="run-download-row"): |
| | ply_download = gr.DownloadButton( |
| | label="Download PLY (.ply)", |
| | value=None, |
| | visible=True, |
| | elem_id="run-ply-download", |
| | ) |
| | status_md = gr.Markdown("", elem_id="run-status") |
| |
|
| | with gr.Row(elem_id="run-actions-row"): |
| | run_btn = gr.Button("Generate", variant="primary") |
| | clear_btn = gr.ClearButton( |
| | [image_in, video_out, ply_download, status_md], |
| | value="Clear", |
| | ) |
| |
|
| | |
| | clear_btn.click( |
| | fn=lambda: None, |
| | outputs=[ply_download], |
| | queue=False, |
| | ) |
| |
|
| | run_btn.click( |
| | fn=run_sharp, |
| | inputs=[ |
| | image_in, |
| | trajectory, |
| | output_res, |
| | frames, |
| | fps_in, |
| | render_toggle, |
| | ], |
| | outputs=[video_out, ply_download, status_md], |
| | api_visibility="public", |
| | ) |
| |
|
| | with gr.Tab("Examples", id="examples"): |
| | with gr.Column(elem_id="examples-panel"): |
| | if EXAMPLE_SPECS: |
| | gr.Markdown( |
| | "Click an example to preview precompiled outputs. " |
| | "The example image will also be loaded into the Run tab." |
| | ) |
| |
|
| | |
| | ex_img = gr.Image( |
| | label="Example image", |
| | type="filepath", |
| | interactive=False, |
| | render=False, |
| | height=360, |
| | elem_id="examples-image", |
| | ) |
| | ex_vid = gr.Video( |
| | label="Pre-rendered MP4", |
| | render=False, |
| | height=360, |
| | elem_id="examples-video", |
| | ) |
| | ex_ply = gr.DownloadButton( |
| | label="Download PLY (.ply)", |
| | value=None, |
| | visible=True, |
| | render=False, |
| | elem_id="examples-ply-download", |
| | ) |
| | ex_status = gr.Markdown( |
| | render=False, elem_id="examples-status" |
| | ) |
| |
|
| | with gr.Row(equal_height=True): |
| | with gr.Column(scale=4, min_width=320): |
| | gr.Examples( |
| | examples=[ |
| | [str(s.image)] for s in EXAMPLE_SPECS |
| | ], |
| | example_labels=[s.label for s in EXAMPLE_SPECS], |
| | inputs=[image_in], |
| | outputs=[ex_img, ex_vid, ex_ply, ex_status], |
| | fn=load_example_assets, |
| | cache_examples=False, |
| | run_on_click=True, |
| | examples_per_page=10, |
| | label=None, |
| | ) |
| |
|
| | with gr.Column(scale=6, min_width=360): |
| | ex_img.render() |
| | ex_vid.render() |
| | ex_ply.render() |
| | ex_status.render() |
| |
|
| | gr.Markdown( |
| | "Add example bundles under `assets/examples/` " |
| | "(image + mp4 + ply) or provide a `manifest.json`." |
| | ) |
| | else: |
| | gr.Markdown( |
| | "No precompiled examples found.\n\n" |
| | "Add files under `assets/examples/`:\n" |
| | "- `example.jpg` (or png/webp)\n" |
| | "- `example.mp4`\n" |
| | "- `example.ply`\n\n" |
| | "Optionally add `assets/examples/manifest.json` to define labels and filenames." |
| | ) |
| |
|
| | with gr.Tab("About", id="about"): |
| | with gr.Column(elem_id="about-panel"): |
| | gr.Markdown( |
| | """ |
| | *Sharp Monocular View Synthesis in Less Than a Second* (Apple, 2025) |
| | |
| | ```bibtex |
| | @inproceedings{Sharp2025:arxiv, |
| | title = {Sharp Monocular View Synthesis in Less Than a Second}, |
| | author = {Lars Mescheder and Wei Dong and Shiwei Li and Xuyang Bai and Marcel Santos and Peiyun Hu and Bruno Lecouat and Mingmin Zhen and Ama\\"{e}l Delaunoyand Tian Fang and Yanghai Tsin and Stephan R. Richter and Vladlen Koltun}, |
| | journal = {arXiv preprint arXiv:2512.10685}, |
| | year = {2025}, |
| | url = {https://arxiv.org/abs/2512.10685}, |
| | } |
| | ``` |
| | """.strip() |
| | ) |
| |
|
| | demo.queue(max_size=DEFAULT_QUEUE_MAX_SIZE, default_concurrency_limit=1) |
| | return demo |
| |
|
| |
|
| | demo = build_demo() |
| |
|
| | if __name__ == "__main__": |
| | demo.launch(theme=THEME, css=CSS) |
| |
|