CoolFace
Apppublic

RobinsAIWorld/ml-sharp

sourceHugging Faceupdated 9mo agoView on Hugging Face
1likes
app.py643 linesDownload Raw Back to root
1"""SHARP Gradio demo (minimal, responsive UI).2 3This 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 8Precompiled examples9Place precompiled examples under `assets/examples/`.10 11Recommended structure (matching stem):12  assets/examples/<name>.jpg|png|webp13  assets/examples/<name>.mp414  assets/examples/<name>.ply15 16Optional manifest (assets/examples/manifest.json):17  [18    {"label": "Desk", "image": "desk.jpg", "video": "desk.mp4", "ply": "desk.ply"},19    ...20  ]21"""22 23from __future__ import annotations24 25import json26from dataclasses import dataclass27from pathlib import Path28from typing import Final29 30import gradio as gr31 32import os33 34from model_utils import (35    TrajectoryType,36    predict_and_maybe_render_gpu,37    configure_gpu_mode,38    get_gpu_status,39)40from hardware_config import (41    get_hardware_choices,42    parse_hardware_choice,43    get_config,44    update_config,45    SPACES_HARDWARE_SPECS,46    is_running_on_spaces,47)48 49# -----------------------------------------------------------------------------50# Paths & constants51# -----------------------------------------------------------------------------52 53APP_DIR: Final[Path] = Path(__file__).resolve().parent54OUTPUTS_DIR: Final[Path] = APP_DIR / "outputs"55ASSETS_DIR: Final[Path] = APP_DIR / "assets"56EXAMPLES_DIR: Final[Path] = ASSETS_DIR / "examples"57 58IMAGE_EXTS: Final[tuple[str, ...]] = (".png", ".jpg", ".jpeg", ".webp")59DEFAULT_QUEUE_MAX_SIZE: Final[int] = 3260DEFAULT_PORT: Final[int] = int(os.getenv("SHARP_PORT", "49200"))61 62THEME: Final = gr.themes.Soft(63    primary_hue="indigo",64    secondary_hue="blue",65    neutral_hue="slate",66)67 68CSS: Final[str] = """69/* Keep layout stable when scrollbars appear/disappear */70html { scrollbar-gutter: stable; }71 72/* Use normal document flow (no fixed-height viewport shell) */73html, body { height: auto; }74body { overflow: auto; }75 76/* Comfortable max width; still fills small screens */77.gradio-container {78  max-width: 1400px;79  margin: 0 auto;80  padding: 0.75rem 1rem 1rem;81  box-sizing: border-box;82}83 84/* Make media components responsive without stretching */85#run-image, #run-video,86#examples-image, #examples-video {87  width: 100%;88}89 90/* Keep aspect ratio and prevent runaway vertical growth on tall viewports */91#run-image img, #examples-image img {92  width: 100%;93  height: auto;94  max-height: 70vh;95  object-fit: contain;96}97#run-video video, #examples-video video {98  width: 100%;99  height: auto;100  max-height: 70vh;101  object-fit: contain;102}103 104/* On very small screens, reduce max media height a bit */105@media (max-width: 640px) {106  #run-image img, #examples-image img,107  #run-video video, #examples-video video {108    max-height: 55vh;109  }110}111 112/* Reduce extra whitespace in markdown blocks */113.gr-markdown > :first-child { margin-top: 0 !important; }114.gr-markdown > :last-child { margin-bottom: 0 !important; }115"""116 117# -----------------------------------------------------------------------------118# Helpers119# -----------------------------------------------------------------------------120 121 122def _ensure_dir(path: Path) -> Path:123    path.mkdir(parents=True, exist_ok=True)124    return path125 126 127@dataclass(frozen=True, slots=True)128class ExampleSpec:129    """A precompiled example bundle (image + optional mp4 + optional ply)."""130 131    label: str132    image: Path133    video: Path | None134    ply: Path | None135 136 137def _normalize_key(path: str) -> str:138    """Normalize a path-like string for stable dictionary keys."""139    try:140        return str(Path(path).resolve())141    except Exception:142        return path143 144 145def _load_manifest(manifest_path: Path) -> list[dict]:146    """Load manifest.json if present; return an empty list on errors."""147    try:148        data = json.loads(manifest_path.read_text(encoding="utf-8"))149        if not isinstance(data, list):150            raise ValueError("manifest.json must contain a JSON list.")151        return [x for x in data if isinstance(x, dict)]152    except FileNotFoundError:153        return []154    except Exception as e:155        # Manifest errors should not crash the app.156        print(f"[examples] Failed to parse manifest.json: {type(e).__name__}: {e}")157        return []158 159 160def discover_examples(examples_dir: Path) -> list[ExampleSpec]:161    """Discover example bundles under assets/examples/."""162    _ensure_dir(examples_dir)163 164    manifest_rows = _load_manifest(examples_dir / "manifest.json")165    if manifest_rows:166        specs: list[ExampleSpec] = []167        for row in manifest_rows:168            label = str(row.get("label") or "Example").strip() or "Example"169            image_rel = row.get("image")170            if not image_rel:171                continue172 173            image = (examples_dir / str(image_rel)).resolve()174            if not image.exists():175                continue176 177            video = None178            ply = None179            if row.get("video"):180                v = (examples_dir / str(row["video"])).resolve()181                if v.exists():182                    video = v183            if row.get("ply"):184                p = (examples_dir / str(row["ply"])).resolve()185                if p.exists():186                    ply = p187 188            specs.append(ExampleSpec(label=label, image=image, video=video, ply=ply))189        return specs190 191    # Fallback: infer bundles by filename stem192    images: list[Path] = []193    for ext in IMAGE_EXTS:194        images.extend(sorted(examples_dir.glob(f"*{ext}")))195 196    specs = []197    for img in images:198        stem = img.stem199        video = examples_dir / f"{stem}.mp4"200        ply = examples_dir / f"{stem}.ply"201        specs.append(202            ExampleSpec(203                label=stem.replace("_", " ").strip() or stem,204                image=img.resolve(),205                video=video.resolve() if video.exists() else None,206                ply=ply.resolve() if ply.exists() else None,207            )208        )209    return specs210 211 212_ensure_dir(OUTPUTS_DIR)213 214EXAMPLE_SPECS: Final[list[ExampleSpec]] = discover_examples(EXAMPLES_DIR)215EXAMPLE_INDEX_BY_PATH: Final[dict[str, ExampleSpec]] = {216    _normalize_key(str(s.image)): s for s in EXAMPLE_SPECS217}218EXAMPLE_INDEX_BY_NAME: Final[dict[str, ExampleSpec]] = {219    s.image.name: s for s in EXAMPLE_SPECS220}221 222 223def load_example_assets(224    image_path: str | None,225) -> tuple[str | None, str | None, str | None, str]:226    """Return (image, video, ply_path, status) for the selected example image."""227    if not image_path:228        return None, None, None, "No example selected."229 230    spec = EXAMPLE_INDEX_BY_PATH.get(_normalize_key(image_path))231    if spec is None:232        spec = EXAMPLE_INDEX_BY_NAME.get(Path(image_path).name)233 234    if spec is None:235        return image_path, None, None, "No matching example bundle found."236 237    video = str(spec.video) if spec.video is not None else None238    ply_path = str(spec.ply) if spec.ply is not None else None239 240    missing: list[str] = []241    if video is None:242        missing.append("MP4")243    if ply_path is None:244        missing.append("PLY")245 246    msg = f"Loaded example: **{spec.label}**."247    if missing:248        msg += f" Missing: {', '.join(missing)}."249 250    return str(spec.image), video, ply_path, msg251 252 253def _validate_image(image_path: str | None) -> None:254    if not image_path:255        raise gr.Error("Upload an image first.")256 257 258# -----------------------------------------------------------------------------259# Hardware Configuration260# -----------------------------------------------------------------------------261 262 263def _get_current_hardware_value() -> str:264    """Get current hardware choice value for dropdown."""265    config = get_config()266    if config.mode == "local":267        return "local"268    return f"spaces:{config.spaces_hardware}"269 270 271def _format_gpu_status() -> str:272    """Format GPU status as markdown."""273    status = get_gpu_status()274    config = get_config()275    276    lines = ["### Current Status"]277    lines.append(f"- **Mode:** {'Local CUDA' if config.mode == 'local' else 'HuggingFace Spaces'}")278    279    if config.mode == "spaces":280        hw_spec = SPACES_HARDWARE_SPECS.get(config.spaces_hardware, {})281        lines.append(f"- **Spaces Hardware:** {hw_spec.get('name', config.spaces_hardware)}")282        lines.append(f"- **VRAM:** {hw_spec.get('vram', 'N/A')}")283        lines.append(f"- **Price:** {hw_spec.get('price', 'N/A')}")284        lines.append(f"- **Duration:** {config.spaces_duration}s")285    else:286        lines.append(f"- **CUDA Available:** {'✅ Yes' if status['cuda_available'] else '❌ No'}")287        lines.append(f"- **Spaces Module:** {'✅ Installed' if status['spaces_available'] else '❌ Not installed'}")288        289        if status['devices']:290            lines.append("\n### Local GPUs")291            for dev in status['devices']:292                lines.append(f"- **GPU {dev['index']}:** {dev['name']} ({dev['total_memory_gb']}GB)")293    294    if is_running_on_spaces():295        lines.append("\n⚠️ *Running on HuggingFace Spaces*")296    297    return "\n".join(lines)298 299 300def _apply_hardware_config(choice: str, duration: int) -> str:301    """Apply hardware configuration and return status."""302    mode, spaces_hw = parse_hardware_choice(choice)303    304    # Update config305    update_config(306        mode=mode,307        spaces_hardware=spaces_hw if spaces_hw else "zero-gpu",308        spaces_duration=duration,309    )310    311    # Configure GPU mode in model_utils312    configure_gpu_mode(313        use_spaces=(mode == "spaces"),314        duration=duration,315    )316    317    return _format_gpu_status()318 319 320def run_sharp(321    image_path: str | None,322    trajectory_type: TrajectoryType,323    output_long_side: int,324    num_frames: int,325    fps: int,326    render_video: bool,327) -> tuple[str | None, str | None, str]:328    """Run SHARP inference and return (video_path, ply_path, status_markdown)."""329    _validate_image(image_path)330    out_long_side: int | None = (331        None if int(output_long_side) <= 0 else int(output_long_side)332    )333 334    try:335        video_path, ply_path = predict_and_maybe_render_gpu(336            image_path,337            trajectory_type=trajectory_type,338            num_frames=int(num_frames),339            fps=int(fps),340            output_long_side=out_long_side,341            render_video=bool(render_video),342        )343 344        lines: list[str] = [f"**PLY:** `{ply_path.name}` (ready to download)"]345        if render_video:346            if video_path is None:347                lines.append("**Video:** not rendered (CUDA unavailable).")348            else:349                lines.append(f"**Video:** `{video_path.name}`")350        else:351            lines.append("**Video:** disabled.")352 353        return (354            str(video_path) if video_path is not None else None,355            str(ply_path),356            "\n".join(lines),357        )358    except gr.Error:359        raise360    except Exception as e:361        raise gr.Error(f"SHARP failed: {type(e).__name__}: {e}") from e362 363 364# -----------------------------------------------------------------------------365# UI366# -----------------------------------------------------------------------------367 368 369def build_demo() -> gr.Blocks:370    with gr.Blocks(371        title="SHARP • Single-Image 3D Gaussian Prediction",372        elem_id="sharp-root",373        fill_height=True,374    ) as demo:375        gr.Markdown("## SHARP\nSingle-image **3D Gaussian scene** prediction.")376 377        # Run tab components are referenced by Examples tab, so keep them in outer scope.378        with gr.Column(elem_id="tabs-shell"):379            with gr.Tabs():380                with gr.Tab("Run", id="run"):381                    with gr.Column(elem_id="run-panel"):382                        with gr.Row(equal_height=True, elem_id="run-media-row"):383                            with gr.Column(384                                scale=5, min_width=360, elem_id="run-left-col"385                            ):386                                image_in = gr.Image(387                                    label="Input image",388                                    type="filepath",389                                    sources=["upload"],390                                    elem_id="run-image",391                                )392 393                                with gr.Row():394                                    trajectory = gr.Dropdown(395                                        label="Trajectory",396                                        choices=[397                                            "swipe",398                                            "shake",399                                            "rotate",400                                            "rotate_forward",401                                        ],402                                        value="rotate_forward",403                                    )404                                    output_res = gr.Dropdown(405                                        label="Output long side",406                                        info="0 = match input",407                                        choices=[408                                            ("Match input", 0),409                                            ("512", 512),410                                            ("768", 768),411                                            ("1024", 1024),412                                            ("1280", 1280),413                                            ("1536", 1536),414                                        ],415                                        value=0,416                                    )417 418                                with gr.Row():419                                    frames = gr.Slider(420                                        label="Frames",421                                        minimum=24,422                                        maximum=120,423                                        step=1,424                                        value=60,425                                    )426                                    fps_in = gr.Slider(427                                        label="FPS",428                                        minimum=8,429                                        maximum=60,430                                        step=1,431                                        value=30,432                                    )433 434                                render_toggle = gr.Checkbox(435                                    label="Render MP4 (requires CUDA)",436                                    value=True,437                                )438 439                            with gr.Column(440                                scale=5, min_width=360, elem_id="run-right-col"441                            ):442                                video_out = gr.Video(443                                    label="Trajectory video (MP4)",444                                    elem_id="run-video",445                                )446                                with gr.Row(elem_id="run-download-row"):447                                    ply_download = gr.DownloadButton(448                                        label="Download PLY (.ply)",449                                        value=None,450                                        visible=True,451                                        elem_id="run-ply-download",452                                    )453                                status_md = gr.Markdown("", elem_id="run-status")454 455                        with gr.Row(elem_id="run-actions-row"):456                            run_btn = gr.Button("Generate", variant="primary")457                            clear_btn = gr.ClearButton(458                                [image_in, video_out, ply_download, status_md],459                                value="Clear",460                            )461 462                        # Ensure clearing also clears any previous download target.463                        clear_btn.click(464                            fn=lambda: None,465                            outputs=[ply_download],466                            queue=False,467                        )468 469                    run_btn.click(470                        fn=run_sharp,471                        inputs=[472                            image_in,473                            trajectory,474                            output_res,475                            frames,476                            fps_in,477                            render_toggle,478                        ],479                        outputs=[video_out, ply_download, status_md],480                        api_visibility="public",481                    )482 483                with gr.Tab("Examples", id="examples"):484                    with gr.Column(elem_id="examples-panel"):485                        if EXAMPLE_SPECS:486                            gr.Markdown(487                                "Click an example to preview precompiled outputs. "488                                "The example image will also be loaded into the Run tab."489                            )490 491                            # Define preview outputs first (unrendered), so we can reference them from gr.Examples.492                            ex_img = gr.Image(493                                label="Example image",494                                type="filepath",495                                interactive=False,496                                render=False,497                                height=360,498                                elem_id="examples-image",499                            )500                            ex_vid = gr.Video(501                                label="Pre-rendered MP4",502                                render=False,503                                height=360,504                                elem_id="examples-video",505                            )506                            ex_ply = gr.DownloadButton(507                                label="Download PLY (.ply)",508                                value=None,509                                visible=True,510                                render=False,511                                elem_id="examples-ply-download",512                            )513                            ex_status = gr.Markdown(514                                render=False, elem_id="examples-status"515                            )516 517                            with gr.Row(equal_height=True):518                                with gr.Column(scale=4, min_width=320):519                                    gr.Examples(520                                        examples=[521                                            [str(s.image)] for s in EXAMPLE_SPECS522                                        ],523                                        example_labels=[s.label for s in EXAMPLE_SPECS],524                                        inputs=[image_in],525                                        outputs=[ex_img, ex_vid, ex_ply, ex_status],526                                        fn=load_example_assets,527                                        cache_examples=False,528                                        run_on_click=True,529                                        examples_per_page=10,530                                        label=None,531                                    )532 533                                with gr.Column(scale=6, min_width=360):534                                    ex_img.render()535                                    ex_vid.render()536                                    ex_ply.render()537                                    ex_status.render()538 539                                    gr.Markdown(540                                        "Add example bundles under `assets/examples/` "541                                        "(image + mp4 + ply) or provide a `manifest.json`."542                                    )543                        else:544                            gr.Markdown(545                                "No precompiled examples found.\n\n"546                                "Add files under `assets/examples/`:\n"547                                "- `example.jpg` (or png/webp)\n"548                                "- `example.mp4`\n"549                                "- `example.ply`\n\n"550                                "Optionally add `assets/examples/manifest.json` to define labels and filenames."551                            )552 553                with gr.Tab("About", id="about"):554                    with gr.Column(elem_id="about-panel"):555                        gr.Markdown(556                            """557*Sharp Monocular View Synthesis in Less Than a Second* (Apple, 2025)558 559```bibtex560@inproceedings{Sharp2025:arxiv,561  title      = {Sharp Monocular View Synthesis in Less Than a Second},562  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},563  journal    = {arXiv preprint arXiv:2512.10685},564  year       = {2025},565  url        = {https://arxiv.org/abs/2512.10685},566}567```568                            """.strip()569                        )570 571                with gr.Tab("⚙️ Settings", id="settings"):572                    with gr.Column(elem_id="settings-panel"):573                        gr.Markdown("### GPU Hardware Selection")574                        gr.Markdown(575                            "Select local CUDA or HuggingFace Spaces GPU for inference. "576                            "Spaces GPUs require deploying to HuggingFace Spaces."577                        )578                        579                        with gr.Row():580                            with gr.Column(scale=3):581                                hw_dropdown = gr.Dropdown(582                                    label="Hardware",583                                    choices=get_hardware_choices(),584                                    value=_get_current_hardware_value(),585                                    interactive=True,586                                )587                                588                                duration_slider = gr.Slider(589                                    label="Spaces GPU Duration (seconds)",590                                    info="Max time for @spaces.GPU decorator (ZeroGPU only)",591                                    minimum=60,592                                    maximum=300,593                                    step=30,594                                    value=get_config().spaces_duration,595                                    interactive=True,596                                )597                                598                                apply_btn = gr.Button("Apply & Save", variant="primary")599                            600                            with gr.Column(scale=2):601                                hw_status = gr.Markdown(602                                    value=_format_gpu_status(),603                                    elem_id="hw-status",604                                )605                        606                        apply_btn.click(607                            fn=_apply_hardware_config,608                            inputs=[hw_dropdown, duration_slider],609                            outputs=[hw_status],610                        )611                        612                        gr.Markdown(613                            """614---615### Spaces Hardware Reference616 617| Hardware | VRAM | Price | Best For |618|----------|------|-------|----------|619| ZeroGPU (H200) | 70GB | Free (PRO) | Demos, dynamic allocation |620| T4 small/medium | 16GB | $0.40-0.60/hr | Light workloads |621| L4x1 | 24GB | $0.80/hr | Standard inference |622| L40Sx1 | 48GB | $1.80/hr | Large models |623| A10G large | 24GB | $1.50/hr | Balanced cost/performance |624| A100 large | 80GB | $2.50/hr | Maximum VRAM |625 626*Prices as of Dec 2024. See [HuggingFace Spaces GPU docs](https://huggingface.co/docs/hub/spaces-gpus).*627                            """628                        )629 630        demo.queue(max_size=DEFAULT_QUEUE_MAX_SIZE, default_concurrency_limit=1)631        return demo632 633 634demo = build_demo()635 636if __name__ == "__main__":637    demo.launch(638        theme=THEME,639        css=CSS,640        server_port=DEFAULT_PORT,641        show_api=True642    )643