from __future__ import annotations import atexit import html import json import shutil import time import uuid from pathlib import Path from urllib.parse import quote import spaces import gradio as gr from gradio.utils import get_upload_folder from PIL import Image, ImageOps from src.demo.hf_runtime import ( InfiniSplatRuntime, ViewerTemplate, export_browser_viewer, export_filtered_gaussian_ply, export_standalone_viewer, prepare_viewer_template, ) OUTPUT_ROOT = Path(get_upload_folder()).resolve() / "infinisplat" OUTPUT_ROOT.mkdir(parents=True, exist_ok=True) _runtime: InfiniSplatRuntime | None = None _viewer_template: ViewerTemplate | None = None GPU_DURATION_SECONDS = 6 REQUEST_CACHE_SECONDS = 3600 CACHE_CLEANUP_INTERVAL_SECONDS = 3600 FULL_TITLE = "Implicit Gaussian Decoding for Large-Baseline Monocular View Synthesis" GITHUB_URL = "https://github.com/zju3dv/InfiniSplat" PROJECT_PAGE_URL = "https://zju3dv.github.io/InfiniSplat" INPUT_IMAGE_HINT = "Better for indoor scenes due to HyperSim-only training" REPO_ROOT = Path(__file__).resolve().parents[2] RGB_EXAMPLE_DIR = REPO_ROOT / "examples/data/rgb_demo" EXAMPLE_THUMBNAIL_DIR = OUTPUT_ROOT / "_example_thumbnails" EXAMPLE_THUMBNAIL_SIZE = (480, 320) SUPPORTED_EXAMPLE_SUFFIXES = {".jpg", ".jpeg", ".png", ".webp"} CURATED_RGB_EXAMPLE_ORDER = [ "examples/data/rgb_demo/painting_room.jpg", "examples/data/rgb_demo/summer_room.jpg", "examples/data/rgb_demo/animate_room.jpg", "examples/data/rgb_demo/meerkat.jpg", "examples/data/rgb_demo/bedroom.jpg", "examples/data/rgb_demo/my_bedroom.JPG", "examples/data/rgb_demo/beggar_home.jpg", "examples/data/rgb_demo/cave_ai.jpg", "examples/data/rgb_demo/eth3d_courtyard.png", "examples/data/rgb_demo/dragon_ball.jpg", "examples/data/rgb_demo/flower_room.jpg", "examples/data/rgb_demo/maksim-shutov-unsplash.jpg", "examples/data/rgb_demo/ghibli_realroom.jpg", "examples/data/rgb_demo/ghibli_room.jpg", "examples/data/rgb_demo/pexels-masi.jpg", "examples/data/rgb_demo/gym.png", "examples/data/rgb_demo/living_room.jpg", "examples/data/rgb_demo/scannetpp_fe94fc30cf.JPG", "examples/data/rgb_demo/loft_room.jpg", "examples/data/rgb_demo/old_livingroom.png", "examples/data/rgb_demo/sofa_ai.jpg", ] def discover_rgb_examples() -> list[str]: """Find demo images in a stable curated order.""" discovered = { path.relative_to(REPO_ROOT).as_posix() for path in RGB_EXAMPLE_DIR.iterdir() if path.is_file() and path.suffix.lower() in SUPPORTED_EXAMPLE_SUFFIXES } curated = [path for path in CURATED_RGB_EXAMPLE_ORDER if path in discovered] unlisted = sorted(discovered.difference(curated), key=str.casefold) return [*curated[:2], *unlisted, *curated[2:]] RGB_EXAMPLES = discover_rgb_examples() def prepare_example_thumbnails( example_paths: list[str] = RGB_EXAMPLES, output_dir: Path = EXAMPLE_THUMBNAIL_DIR, ) -> list[str]: """Create compact WebP previews without changing inference inputs.""" output_dir.mkdir(parents=True, exist_ok=True) thumbnails = [] for example_path in example_paths: source = Path(example_path) if not source.is_absolute(): source = REPO_ROOT / source thumbnail = output_dir / f"{source.stem}.webp" if not thumbnail.is_file() or thumbnail.stat().st_mtime < source.stat().st_mtime: with Image.open(source) as image: image = ImageOps.exif_transpose(image).convert("RGB") image.thumbnail(EXAMPLE_THUMBNAIL_SIZE, Image.Resampling.LANCZOS) image.save(thumbnail, format="WEBP", quality=82, method=6) thumbnails.append(str(thumbnail)) return thumbnails APP_THEME = gr.themes.Soft( primary_hue="emerald", secondary_hue="amber", neutral_hue="zinc", spacing_size="sm", radius_size="sm", font=("ui-sans-serif", "system-ui", "sans-serif"), ).set( body_background_fill="#f5f6f6", body_background_fill_dark="#f5f6f6", body_text_color="#17201e", body_text_color_dark="#17201e", body_text_color_subdued="#68716f", body_text_color_subdued_dark="#68716f", background_fill_primary="#ffffff", background_fill_primary_dark="#ffffff", background_fill_secondary="#f8f9f9", background_fill_secondary_dark="#f8f9f9", block_background_fill="#ffffff", block_background_fill_dark="#ffffff", block_label_text_color="#17201e", block_label_text_color_dark="#17201e", block_title_text_color="#17201e", block_title_text_color_dark="#17201e", input_background_fill="#ffffff", input_background_fill_dark="#ffffff", input_placeholder_color="#68716f", input_placeholder_color_dark="#68716f", button_primary_background_fill="#087f62", button_primary_background_fill_dark="#087f62", button_primary_text_color="#ffffff", button_primary_text_color_dark="#ffffff", button_secondary_background_fill="#f8f9f9", button_secondary_background_fill_dark="#f8f9f9", button_secondary_text_color="#17201e", button_secondary_text_color_dark="#17201e", ) APP_CSS = """ :root { color-scheme: light; --app-bg: #f5f6f6; --surface: #ffffff; --surface-muted: #f8f9f9; --border: #dfe3e1; --border-strong: #c6ceca; --text: #17201e; --muted: #68716f; --viewer: #101413; --primary: #087f62; --primary-hover: #066b53; --accent: #c27616; } html, body { background: var(--app-bg) !important; color: var(--text) !important; } .gradio-container { max-width: none !important; min-height: 100vh; padding: 0 !important; background: var(--app-bg) !important; color: var(--text); } .gradio-container *, .dark .gradio-container * { letter-spacing: 0 !important; } #app-title { padding: 28px 2px 18px; background: transparent !important; } #app-title .app-title-content { display: flex; align-items: center; justify-content: space-between; gap: 24px; } #app-title h1 { display: flex; align-items: baseline; flex: 1; flex-wrap: wrap; gap: 0.35em; margin: 0 !important; color: var(--text); line-height: 1.2; } #app-title .title-brand { font-size: clamp(1.75rem, 2.5vw, 2.2rem); font-weight: 720; } #app-title .title-description { color: var(--muted); font-size: clamp(0.92rem, 1.35vw, 1.08rem); font-weight: 550; } .title-actions { display: flex; flex-shrink: 0; gap: 9px; } .title-link { display: inline-flex; align-items: center; min-height: 38px; padding: 0 14px; border: 1px solid var(--border-strong); border-radius: 6px; background: var(--surface); color: var(--text) !important; font-size: 0.78rem; font-weight: 650; text-decoration: none !important; transition: border-color 150ms ease-out, background 150ms ease-out; } .title-link:hover { border-color: var(--primary); background: #edf8f4; } #app-main { width: calc(100% - clamp(28px, 6vw, 80px)); max-width: 1440px; margin: 0 auto !important; gap: 0 !important; } .section-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; } .section-heading h2 { margin: 0; color: var(--text); font-size: 0.9rem; font-weight: 700; } .section-heading span { color: var(--muted); font-size: 0.75rem; } #workspace { margin: 0 0 26px !important; gap: 18px; align-items: stretch; } .tool-panel { min-width: 0 !important; overflow: hidden; gap: 0 !important; border: 1px solid var(--border) !important; border-radius: 8px !important; background: var(--surface) !important; box-shadow: 0 8px 28px rgba(23, 32, 30, 0.055); } .panel-heading { display: flex; align-items: center; justify-content: space-between; box-sizing: border-box; width: 100%; min-width: 0; min-height: 56px; margin: 0 !important; padding: 17px 12px 15px; border-bottom: 1px solid var(--border); } .panel-heading h2 { display: flex; align-items: center; flex-shrink: 0; gap: 9px; margin: 0; color: var(--text); font-size: 0.9rem; font-weight: 700; line-height: 1.2; white-space: nowrap; } .panel-heading span { color: var(--muted); font-size: 0.75rem; line-height: 1.2; } .panel-heading .input-hint { flex: 1; min-width: 0; margin-left: 12px; font-size: clamp(0.58rem, 0.72vw, 0.66rem); line-height: 1.2; text-align: right; white-space: normal; } .panel-heading .section-index { color: var(--accent); font-size: 0.7rem; font-variant-numeric: tabular-nums; } .source-content, .viewer-content { margin: 0 !important; padding: 12px !important; gap: 0 !important; background: var(--surface) !important; } #source-image { min-height: 510px; overflow: hidden; border: 1px solid var(--border) !important; border-radius: 6px !important; background: var(--surface-muted) !important; } #source-image > div, #source-image .wrap { border-radius: 6px !important; background: var(--surface-muted) !important; } #source-image p, #source-image span, #source-image button:not(.primary) { color: var(--muted) !important; } .source-actions { margin: 0 !important; padding: 0 12px 12px; border: 0 !important; background: var(--surface) !important; } #reconstruct-button { min-height: 46px; border-color: var(--primary) !important; background: var(--primary) !important; color: #ffffff !important; font-weight: 700; box-shadow: 0 4px 12px rgba(8, 127, 98, 0.16); } #reconstruct-button:hover { border-color: var(--primary-hover) !important; background: var(--primary-hover) !important; } .splat-shell, .viewer-state, .splat-frame { width: 100%; height: min(62vh, 510px); min-height: 510px; background: var(--viewer); } .splat-shell { overflow: hidden; border-radius: 6px; } .viewer-host { position: relative; } .viewer-preloader { position: absolute; width: 1px; height: 1px; overflow: hidden; border: 0; opacity: 0; pointer-events: none; } .viewer-state { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%; height: 100%; gap: 8px; color: #f4f7f6; text-align: center; } .viewer-state strong { color: #f4f7f6 !important; font-size: 0.9rem; font-weight: 650; } .viewer-state span { color: #94a19d !important; font-size: 0.75rem; } .viewer-idle { width: 32px; height: 2px; margin-bottom: 7px; background: #46514e; } .viewer-loader { width: 34px; height: 34px; margin-bottom: 7px; border: 3px solid #35413d; border-top-color: #34d399; border-radius: 50%; animation: viewer-spin 0.9s linear infinite; } .viewer-error { width: 32px; height: 3px; margin-bottom: 7px; background: #f87171; } @keyframes viewer-spin { to { transform: rotate(360deg); } } @media (prefers-reduced-motion: reduce) { .viewer-loader { animation-duration: 1.8s; } } .splat-frame { display: block; border: 0; } .viewer-host .splat-frame { opacity: 0; transition: opacity 180ms ease-out; } .viewer-client-state { position: absolute; inset: 0; z-index: 2; transition: opacity 180ms ease-out, visibility 180ms ease-out; } .viewer-client-error, .viewer-client-timeout { display: none; } .viewer-host[data-viewer-state="ready"] .splat-frame { opacity: 1; } .viewer-host[data-viewer-state="ready"] .viewer-client-state { visibility: hidden; opacity: 0; } .viewer-host[data-viewer-state="error"] .viewer-client-loading, .viewer-host[data-viewer-state="timeout"] .viewer-client-loading { display: none; } .viewer-host[data-viewer-state="error"] .viewer-client-error, .viewer-host[data-viewer-state="timeout"] .viewer-client-timeout { display: flex; } #viewer-output { margin: 0 !important; padding: 0 !important; border: 0 !important; } .download-bar { margin: 0 !important; padding: 0 12px 12px; border: 0 !important; gap: 10px; background: var(--surface) !important; } .artifact-button { min-height: 42px; border-color: var(--border-strong) !important; background: var(--surface-muted) !important; color: var(--text) !important; font-weight: 650; } .artifact-button:hover { border-color: var(--accent) !important; background: #ffffff !important; } .artifact-ready { border-color: #8ac6b5 !important; background: #edf8f4 !important; color: #075e49 !important; } .artifact-ready:hover { border-color: var(--primary) !important; background: #e3f4ee !important; } #examples-section { gap: 11px !important; margin: 0 0 36px !important; padding: 4px 2px 0; } #example-gallery { overflow: visible; padding: 0 !important; border: 0 !important; border-radius: 8px !important; background: transparent !important; } #example-gallery .gallery-container, #example-gallery .grid-wrap { height: auto !important; min-height: 0 !important; } #example-gallery .grid-wrap { overflow: visible !important; padding: 0 !important; } #example-gallery .grid-container { display: grid !important; grid-template-columns: repeat(28, minmax(0, 1fr)) !important; grid-auto-flow: row dense !important; grid-auto-rows: 158px !important; height: auto !important; gap: 10px !important; } #example-gallery .gallery-item { grid-column: span 4 !important; height: 158px !important; min-width: 0 !important; overflow: hidden; border-radius: 7px !important; } #example-gallery .gallery-item:nth-child(4), #example-gallery .gallery-item:nth-child(9), #example-gallery .gallery-item:nth-child(10), #example-gallery .gallery-item:nth-child(11), #example-gallery .gallery-item:nth-child(15), #example-gallery .gallery-item:nth-child(19), #example-gallery .gallery-item:nth-child(20) { grid-column: span 3 !important; } #example-gallery .gallery-item:nth-child(6), #example-gallery .gallery-item:nth-child(8), #example-gallery .gallery-item:nth-child(12), #example-gallery .gallery-item:nth-child(14), #example-gallery .gallery-item:nth-child(16), #example-gallery .gallery-item:nth-child(18), #example-gallery .gallery-item:nth-child(21) { grid-column: span 5 !important; } #example-gallery .thumbnail-item, #example-gallery .thumbnail-item:hover, #example-gallery .thumbnail-item.selected { border: 0 !important; background: transparent !important; box-shadow: none !important; filter: none !important; } #example-gallery .caption-label { display: none !important; } #example-gallery img { object-fit: cover !important; } #example-gallery button { color: var(--text) !important; } @media (max-width: 980px) { #app-main { width: calc(100% - 28px); } #workspace { flex-direction: column; } #workspace > .tool-panel { width: 100% !important; } .splat-shell, .viewer-state, .splat-frame { height: 56vh; min-height: 420px; } #source-image { min-height: 420px; } #example-gallery .grid-container { grid-template-columns: repeat(6, minmax(0, 1fr)) !important; } #example-gallery .gallery-item, #example-gallery .gallery-item:nth-child(4), #example-gallery .gallery-item:nth-child(6), #example-gallery .gallery-item:nth-child(8), #example-gallery .gallery-item:nth-child(9), #example-gallery .gallery-item:nth-child(10), #example-gallery .gallery-item:nth-child(11), #example-gallery .gallery-item:nth-child(12), #example-gallery .gallery-item:nth-child(14), #example-gallery .gallery-item:nth-child(15), #example-gallery .gallery-item:nth-child(16), #example-gallery .gallery-item:nth-child(18), #example-gallery .gallery-item:nth-child(19), #example-gallery .gallery-item:nth-child(20), #example-gallery .gallery-item:nth-child(21) { grid-column: span 2 !important; } } @media (max-width: 560px) { #app-main { width: calc(100% - 20px); } #app-title { padding: 22px 2px 15px; } #app-title .app-title-content { align-items: flex-start; flex-direction: column; gap: 13px; } #app-title .title-brand { font-size: 1.7rem; } #app-title .title-description { font-size: 0.9rem; } #workspace { margin: 14px 0 22px !important; gap: 12px; } .panel-heading { min-height: 52px; padding: 14px; } .panel-heading .input-hint { margin-left: 8px; } .source-content, .viewer-content { padding: 9px !important; } .source-actions, .download-bar { padding: 0 9px 9px; } .splat-shell, .viewer-state, .splat-frame { min-height: 360px; } #source-image { min-height: 360px; } #examples-section { margin-bottom: 24px !important; padding: 0; } #example-gallery .grid-container { grid-template-columns: repeat(4, minmax(0, 1fr)) !important; grid-auto-rows: 132px !important; } #example-gallery .gallery-item { grid-column: span 2 !important; height: 132px !important; } #example-gallery .gallery-item:last-child:nth-child(odd) { grid-column: span 4 !important; } } @media (max-width: 440px) { .panel-heading .input-hint { max-width: 170px; white-space: normal; } } """ def _gradio_file_url(path: Path) -> str: encoded_path = quote(str(path.resolve()), safe="/") return html.escape(f"/gradio_api/file={encoded_path}", quote=True) def _log_timing(stage: str, request_dir: Path, started_at: float, **metrics) -> None: """Emit one structured timing record for demo performance checks.""" payload = { "stage": stage, "request": request_dir.name, "seconds": round(time.perf_counter() - started_at, 3), **metrics, } print(f"INFINISPLAT_TIMING {json.dumps(payload, sort_keys=True)}", flush=True) def cleanup_request_directories( output_root: Path = OUTPUT_ROOT, max_age_seconds: int | None = REQUEST_CACHE_SECONDS, now: float | None = None, ) -> list[Path]: """Remove expired per-request directories while preserving shared assets.""" if not output_root.is_dir(): return [] cutoff = None if max_age_seconds is not None: cutoff = (time.time() if now is None else now) - max_age_seconds removed = [] for request_dir in output_root.iterdir(): if request_dir.is_symlink() or not request_dir.is_dir(): continue try: if uuid.UUID(hex=request_dir.name).hex != request_dir.name: continue except ValueError: continue if cutoff is not None and request_dir.lstat().st_mtime > cutoff: continue shutil.rmtree(request_dir) removed.append(request_dir) return removed def cleanup_expired_request_directories() -> None: """Delete request directories after their download URLs have expired.""" cleanup_request_directories() def build_viewer_iframe(viewer_html: Path) -> str: """Build a same-origin iframe for one generated viewer.""" source = _gradio_file_url(viewer_html) return ( '