luh1124 commited on
Commit
a24870e
·
1 Parent(s): c81cbbd

feat(Space): ZeroGPU — lazy model load, @spaces.GPU on CUDA callbacks

Browse files
Files changed (1) hide show
  1. app.py +1049 -4
app.py CHANGED
@@ -1,7 +1,1052 @@
 
 
 
 
 
 
 
1
  import gradio as gr
2
 
3
- def greet(name):
4
- return "Hello " + name + "!!"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- demo = gr.Interface(fn=greet, inputs="text", outputs="text")
7
- demo.launch()
 
 
 
 
1
+ import os
2
+ import sys
3
+ import shutil
4
+ import threading
5
+ from pathlib import Path
6
+ from typing import Any, Dict, Optional
7
+
8
  import gradio as gr
9
 
10
+ try:
11
+ import spaces
12
+ except ImportError:
13
+ spaces = None
14
+ import imageio
15
+ import numpy as np
16
+ import torch
17
+ import trimesh
18
+ from PIL import Image
19
+
20
+ try:
21
+ import gradio_client.utils as client_utils
22
+
23
+ _get_type_orig = client_utils.get_type
24
+
25
+ def _get_type_patched(schema):
26
+ if isinstance(schema, bool):
27
+ return "boolean"
28
+ return _get_type_orig(schema)
29
+
30
+ client_utils.get_type = _get_type_patched
31
+ except Exception:
32
+ pass
33
+
34
+ sys.path.insert(0, "./hy3dshape")
35
+ os.environ.setdefault("ATTN_BACKEND", "xformers")
36
+ os.environ.setdefault("SPCONV_ALGO", "native")
37
+ # Cloud GPUs (T4, A10G, L4, …) vs H100: override with TORCH_CUDA_ARCH_LIST if cutlass/spconv complains.
38
+ os.environ.setdefault("TORCH_CUDA_ARCH_LIST", "7.5;8.0;8.6;8.9;9.0")
39
+
40
+ from trellis.pipelines import NeARImageToRelightable3DPipeline
41
+ from hy3dshape.pipelines import Hunyuan3DDiTFlowMatchingPipeline # pyright: ignore[reportMissingImports]
42
+
43
+ # Hugging Face ZeroGPU: wrap GPU work in @spaces.GPU (no-op locally if `spaces` is missing).
44
+ _ZGPU_MAX_S = int(os.environ.get("NEAR_ZEROGPU_MAX_SECONDS", "1800"))
45
+
46
+
47
+ def _zero_gpu(**kwargs):
48
+ """Decorator: request a GPU for this Gradio callback on HF ZeroGPU Spaces."""
49
+
50
+ def decorator(fn):
51
+ if spaces is None:
52
+ return fn
53
+ kwargs.setdefault("duration", _ZGPU_MAX_S)
54
+ return spaces.GPU(**kwargs)(fn)
55
+
56
+ return decorator
57
+
58
+
59
+ APP_DIR = Path(__file__).resolve().parent
60
+ CACHE_DIR = APP_DIR / "tmp_gradio"
61
+ CACHE_DIR.mkdir(exist_ok=True)
62
+
63
+ DEFAULT_IMAGE = APP_DIR / "assets/example_image/T.png"
64
+ DEFAULT_SLAT = APP_DIR / "assets/example_slats/2a0d671ce308adb93323eae7141953fc1a5ba68f38cc69f476d5e904c634864d.npz"
65
+ DEFAULT_HDRI = APP_DIR / "assets/hdris/studio_small_03_1k.exr"
66
+ DEFAULT_PORT = 7860
67
+ MAX_SEED = np.iinfo(np.int32).max
68
+
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # Session helpers
72
+ # ---------------------------------------------------------------------------
73
+
74
+ def ensure_session_dir(req: Optional[gr.Request]) -> Path:
75
+ session_id = getattr(req, "session_hash", None) or "shared"
76
+ d = CACHE_DIR / str(session_id)
77
+ d.mkdir(parents=True, exist_ok=True)
78
+ return d
79
+
80
+
81
+ @_zero_gpu(duration=120)
82
+ def clear_session_dir(req: Optional[gr.Request]) -> str:
83
+ d = ensure_session_dir(req)
84
+ shutil.rmtree(d, ignore_errors=True)
85
+ d.mkdir(parents=True, exist_ok=True)
86
+ if torch.cuda.is_available():
87
+ torch.cuda.empty_cache()
88
+ return "Session cache cleared."
89
+
90
+
91
+ def end_session(req: gr.Request):
92
+ d = ensure_session_dir(req)
93
+ shutil.rmtree(d, ignore_errors=True)
94
+
95
+
96
+ def get_file_path(file_obj: Any) -> Optional[str]:
97
+ if file_obj is None:
98
+ return None
99
+ if isinstance(file_obj, str):
100
+ return file_obj
101
+ for attr in ("name", "path", "value"):
102
+ v = getattr(file_obj, attr, None)
103
+ if isinstance(v, str) and v:
104
+ return v
105
+ return None
106
+
107
+
108
+ # ---------------------------------------------------------------------------
109
+ # Model loading (lazy — ZeroGPU may have no CUDA until @spaces.GPU runs)
110
+ # ---------------------------------------------------------------------------
111
+
112
+ _model_lock = threading.Lock()
113
+ PIPELINE: Optional[NeARImageToRelightable3DPipeline] = None
114
+ GEOMETRY_PIPELINE: Optional[Hunyuan3DDiTFlowMatchingPipeline] = None
115
+
116
+ # Dropdown defaults before lazy load; use allow_custom_value for full OCIO view names.
117
+ TONE_MAPPER_CHOICES = ["AgX", "False", "Khronos neutrals", "Filmic", "Khronos glTF PBR"]
118
+
119
+
120
+ def _ensure_models() -> None:
121
+ global PIPELINE, GEOMETRY_PIPELINE, TONE_MAPPER_CHOICES
122
+ with _model_lock:
123
+ if PIPELINE is not None:
124
+ return
125
+ device = "cuda" if torch.cuda.is_available() else "cpu"
126
+ near_id = os.environ.get("NEAR_PRETRAINED", "luh0502/NeAR")
127
+ gp = Hunyuan3DDiTFlowMatchingPipeline.from_pretrained("tencent/Hunyuan3D-2.1")
128
+ gp.to(device)
129
+ pl = NeARImageToRelightable3DPipeline.from_pretrained(near_id)
130
+ pl.to(device)
131
+ GEOMETRY_PIPELINE = gp
132
+ PIPELINE = pl
133
+ views = getattr(pl.tone_mapper, "available_views", None)
134
+ if isinstance(views, (list, tuple)) and views:
135
+ TONE_MAPPER_CHOICES = [str(v) for v in views]
136
+
137
+
138
+ def set_tone_mapper(view_name: str):
139
+ _ensure_models()
140
+ assert PIPELINE is not None
141
+ if view_name:
142
+ PIPELINE.setup_tone_mapper(view_name)
143
+
144
+
145
+ @_zero_gpu()
146
+ def preview_hdri(hdri_file_obj: Any, tone_mapper_name: str):
147
+ _ensure_models()
148
+ assert PIPELINE is not None
149
+ hdri_path = get_file_path(hdri_file_obj)
150
+ if not hdri_path:
151
+ return None, "Upload an HDRI `.exr` (left column)."
152
+ set_tone_mapper(tone_mapper_name)
153
+ hdri_np = PIPELINE.load_hdri(hdri_path)
154
+ preview = PIPELINE.tone_mapper.hdr_to_ldr(hdri_np)
155
+ preview = (np.clip(preview, 0, 1) * 255).astype(np.uint8)
156
+ name = Path(hdri_path).name
157
+ return preview, f"HDRI **{name}** — preview updated."
158
+
159
+
160
+ def switch_asset_source(mode: str):
161
+ return gr.Tabs(selected=1 if mode == "From Existing SLaT" else 0)
162
+
163
+
164
+ def _ensure_rgba(img: Image.Image) -> Image.Image:
165
+ """Normalize to RGBA so alpha is preserved for mesh (white matte) vs SLaT (black matte)."""
166
+ if img.mode == "RGBA":
167
+ return img
168
+ if img.mode == "RGB":
169
+ r, g, b = img.split()
170
+ a = Image.new("L", img.size, 255)
171
+ return Image.merge("RGBA", (r, g, b, a))
172
+ return img.convert("RGBA")
173
+
174
+
175
+ @_zero_gpu()
176
+ @torch.inference_mode()
177
+ def preprocess_image_only(image_input: Optional[Image.Image]):
178
+ _ensure_models()
179
+ assert PIPELINE is not None
180
+ if image_input is None:
181
+ return None
182
+ return PIPELINE.preprocess_image_rgba(_ensure_rgba(image_input))
183
+
184
+
185
+ def preprocess_default_image() -> Optional[Image.Image]:
186
+ """Run once on page load so the default example is background-removed (no .change loop)."""
187
+ img = Image.open(DEFAULT_IMAGE).convert("RGBA")
188
+ return preprocess_image_only(img)
189
+
190
+
191
+ def save_slat_npz(slat, save_path: Path):
192
+ np.savez(
193
+ save_path,
194
+ feats=slat.feats.detach().cpu().numpy(),
195
+ coords=slat.coords.detach().cpu().numpy(),
196
+ )
197
+
198
+
199
+ # ---------------------------------------------------------------------------
200
+ # Core pipeline functions
201
+ # ---------------------------------------------------------------------------
202
+
203
+ @_zero_gpu()
204
+ @torch.inference_mode()
205
+ def generate_mesh(
206
+ image_input: Optional[Image.Image],
207
+ req: gr.Request,
208
+ progress=gr.Progress(track_tqdm=True),
209
+ ):
210
+ """Step ①: generate Hunyuan3D geometry from an already preprocessed image.
211
+ Returns: (state, mesh_glb_path, status)
212
+ """
213
+ _ensure_models()
214
+ assert PIPELINE is not None and GEOMETRY_PIPELINE is not None
215
+ session_dir = ensure_session_dir(req)
216
+
217
+ if image_input is None:
218
+ raise gr.Error("Please upload an input image.")
219
+
220
+ rgba = _ensure_rgba(image_input)
221
+ if rgba.size != (518, 518):
222
+ rgba = PIPELINE.preprocess_image_rgba(rgba)
223
+ # Hunyuan3D mesh: composite onto white. SLaT step uses black matte separately.
224
+ mesh_rgb = PIPELINE.flatten_rgba_on_matte(rgba, (1.0, 1.0, 1.0))
225
+ rgba.save(session_dir / "input_preprocessed_rgba.png")
226
+ mesh_rgb.save(session_dir / "input_processed.png")
227
+
228
+ progress(0.6, desc="Generating geometry")
229
+ mesh = GEOMETRY_PIPELINE(image=mesh_rgb)[0]
230
+ mesh_path = session_dir / "initial_3d_shape.glb"
231
+ mesh.export(mesh_path)
232
+
233
+ state = {
234
+ "mode": "image",
235
+ "mesh_path": str(mesh_path),
236
+ "processed_image_path": str(session_dir / "input_processed.png"),
237
+ "slat_path": None,
238
+ }
239
+ return (
240
+ state,
241
+ str(mesh_path),
242
+ "**Mesh ready** — Click **② Generate / Load SLaT** to continue.",
243
+ )
244
+
245
+
246
+ @_zero_gpu()
247
+ @torch.inference_mode()
248
+ def generate_slat(
249
+ asset_state: Dict[str, Any],
250
+ image_input: Optional[Image.Image],
251
+ seed: int,
252
+ req: gr.Request,
253
+ progress=gr.Progress(track_tqdm=True),
254
+ ):
255
+ _ensure_models()
256
+ assert PIPELINE is not None
257
+ session_dir = ensure_session_dir(req)
258
+
259
+ if not asset_state or not asset_state.get("mesh_path"):
260
+ raise gr.Error("Please run ① Generate Mesh first.")
261
+ mesh_path = asset_state["mesh_path"]
262
+ if not os.path.exists(mesh_path):
263
+ raise gr.Error("Mesh file not found — please regenerate the mesh.")
264
+
265
+ if image_input is None:
266
+ raise gr.Error("Preprocessed image not found — please upload the image again.")
267
+
268
+ progress(0.1, desc="Loading mesh")
269
+ mesh = trimesh.load(mesh_path, force="mesh")
270
+ rgba = _ensure_rgba(image_input)
271
+ if rgba.size != (518, 518):
272
+ rgba = PIPELINE.preprocess_image_rgba(rgba)
273
+ slat_rgb = PIPELINE.flatten_rgba_on_matte(rgba, (0.0, 0.0, 0.0))
274
+
275
+ progress(0.3, desc="Computing SLaT coordinates")
276
+ coords = PIPELINE.shape_to_coords(mesh)
277
+
278
+ progress(0.6, desc="Generating SLaT")
279
+ slat = PIPELINE.run_with_coords([slat_rgb], coords, seed=int(seed), preprocess_image=False)
280
+
281
+ slat_path = session_dir / "generated_slat.npz"
282
+ save_slat_npz(slat, slat_path)
283
+
284
+ new_state = {**asset_state, "slat_path": str(slat_path)}
285
+ return new_state, f"**Asset ready** — SLaT generated (seed `{seed}`)."
286
+
287
+
288
+ def load_slat_file(slat_upload: Any, slat_path_text: str, req: gr.Request):
289
+ resolved = get_file_path(slat_upload) or (slat_path_text.strip() if slat_path_text else "")
290
+ if not resolved:
291
+ raise gr.Error("Please provide a SLaT `.npz` path or upload one.")
292
+ if not os.path.exists(resolved):
293
+ raise gr.Error(f"SLaT file not found: `{resolved}`")
294
+ state = {"mode": "slat", "slat_path": resolved, "mesh_path": None, "processed_image_path": None}
295
+ return state, f"SLaT **{Path(resolved).name}** loaded."
296
+
297
+
298
+ def prepare_slat(
299
+ source_mode: str,
300
+ asset_state: Dict[str, Any],
301
+ image_input: Optional[Image.Image],
302
+ seed: int,
303
+ slat_upload: Any,
304
+ slat_path_text: str,
305
+ req: gr.Request,
306
+ progress=gr.Progress(track_tqdm=True),
307
+ ):
308
+ if source_mode == "From Image":
309
+ return generate_slat(asset_state, image_input, seed, req, progress)
310
+ return load_slat_file(slat_upload, slat_path_text, req)
311
+
312
+
313
+ def require_asset_state(asset_state: Optional[Dict[str, Any]]) -> Dict[str, Any]:
314
+ if not asset_state or not asset_state.get("slat_path"):
315
+ raise gr.Error("Please generate or load a SLaT first.")
316
+ return asset_state
317
+
318
+
319
+ def load_asset_and_hdri(asset_state: Dict[str, Any], hdri_file_obj: Any, tone_mapper_name: str):
320
+ _ensure_models()
321
+ assert PIPELINE is not None
322
+ asset_state = require_asset_state(asset_state)
323
+ hdri_path = get_file_path(hdri_file_obj)
324
+ if not hdri_path:
325
+ raise gr.Error("Please upload an HDRI `.exr` file.")
326
+ set_tone_mapper(tone_mapper_name)
327
+ slat = PIPELINE.load_slat(asset_state["slat_path"])
328
+ hdri_np = PIPELINE.load_hdri(hdri_path)
329
+ return slat, hdri_np
330
+
331
+
332
+ @_zero_gpu()
333
+ @torch.inference_mode()
334
+ def render_preview(
335
+ asset_state: Dict[str, Any],
336
+ hdri_file_obj: Any,
337
+ tone_mapper_name: str,
338
+ hdri_rot: float,
339
+ yaw: float,
340
+ pitch: float,
341
+ fov: float,
342
+ radius: float,
343
+ resolution: int,
344
+ req: gr.Request,
345
+ progress=gr.Progress(track_tqdm=True),
346
+ ):
347
+ session_dir = ensure_session_dir(req)
348
+ progress(0.1, desc="Loading SLaT and HDRI")
349
+ slat, hdri_np = load_asset_and_hdri(asset_state, hdri_file_obj, tone_mapper_name)
350
+
351
+ progress(0.5, desc="Rendering")
352
+ views = PIPELINE.render_view(
353
+ slat, hdri_np,
354
+ yaw_deg=yaw, pitch_deg=pitch, fov=fov, radius=radius,
355
+ hdri_rot_deg=hdri_rot, resolution=int(resolution),
356
+ )
357
+ for key, image in views.items():
358
+ image.save(session_dir / f"preview_{key}.png")
359
+
360
+ msg = (
361
+ f"**Preview done** — "
362
+ f"yaw `{yaw:.0f}°` pitch `{pitch:.0f}°` · "
363
+ f"fov `{fov:.0f}` radius `{radius:.1f}` · HDRI rot `{hdri_rot:.0f}°`"
364
+ )
365
+ return (
366
+ views["color"],
367
+ views["base_color"],
368
+ views["metallic"],
369
+ views["roughness"],
370
+ views["shadow"],
371
+ msg,
372
+ )
373
+
374
+
375
+ @_zero_gpu()
376
+ @torch.inference_mode()
377
+ def render_camera_video(
378
+ asset_state: Dict[str, Any],
379
+ hdri_file_obj: Any,
380
+ tone_mapper_name: str,
381
+ hdri_rot: float,
382
+ fps: int,
383
+ num_views: int,
384
+ fov: float,
385
+ radius: float,
386
+ full_video: bool,
387
+ shadow_video: bool,
388
+ req: gr.Request,
389
+ progress=gr.Progress(track_tqdm=True),
390
+ ):
391
+ session_dir = ensure_session_dir(req)
392
+ progress(0.1, desc="Loading SLaT and HDRI")
393
+ slat, hdri_np = load_asset_and_hdri(asset_state, hdri_file_obj, tone_mapper_name)
394
+
395
+ progress(0.4, desc="Rendering camera path")
396
+ frames = PIPELINE.render_camera_path_video(
397
+ slat, hdri_np,
398
+ num_views=int(num_views), fov=fov, radius=radius,
399
+ hdri_rot_deg=hdri_rot, full_video=full_video, shadow_video=shadow_video,
400
+ bg_color=(1, 1, 1), verbose=True,
401
+ )
402
+ video_path = session_dir / ("camera_path_full.mp4" if full_video else "camera_path.mp4")
403
+ imageio.mimsave(video_path, frames, fps=int(fps))
404
+ return str(video_path), f"**Camera path video saved**"
405
+
406
+
407
+ @_zero_gpu()
408
+ @torch.inference_mode()
409
+ def render_hdri_video(
410
+ asset_state: Dict[str, Any],
411
+ hdri_file_obj: Any,
412
+ tone_mapper_name: str,
413
+ fps: int,
414
+ num_frames: int,
415
+ yaw: float,
416
+ pitch: float,
417
+ fov: float,
418
+ radius: float,
419
+ full_video: bool,
420
+ shadow_video: bool,
421
+ req: gr.Request,
422
+ progress=gr.Progress(track_tqdm=True),
423
+ ):
424
+ session_dir = ensure_session_dir(req)
425
+ progress(0.1, desc="Loading SLaT and HDRI")
426
+ slat, hdri_np = load_asset_and_hdri(asset_state, hdri_file_obj, tone_mapper_name)
427
+
428
+ progress(0.4, desc="Rendering HDRI rotation")
429
+ hdri_roll_frames, render_frames = PIPELINE.render_hdri_rotation_video(
430
+ slat, hdri_np,
431
+ num_frames=int(num_frames), yaw_deg=yaw, pitch_deg=pitch,
432
+ fov=fov, radius=radius, full_video=full_video, shadow_video=shadow_video,
433
+ bg_color=(1, 1, 1), verbose=True,
434
+ )
435
+ hdri_roll_path = session_dir / "hdri_roll.mp4"
436
+ render_path = session_dir / ("hdri_rotation_full.mp4" if full_video else "hdri_rotation.mp4")
437
+ imageio.mimsave(hdri_roll_path, hdri_roll_frames, fps=int(fps))
438
+ imageio.mimsave(render_path, render_frames, fps=int(fps))
439
+ return str(hdri_roll_path), str(render_path), "**HDRI rotation video saved**"
440
+
441
+
442
+ @_zero_gpu()
443
+ def export_glb(
444
+ asset_state: Dict[str, Any],
445
+ hdri_file_obj: Any,
446
+ tone_mapper_name: str,
447
+ hdri_rot: float,
448
+ simplify: float,
449
+ texture_size: int,
450
+ req: gr.Request,
451
+ progress=gr.Progress(track_tqdm=True),
452
+ ):
453
+ """Returns: (glb_path, status)"""
454
+ _ensure_models()
455
+ assert PIPELINE is not None
456
+ session_dir = ensure_session_dir(req)
457
+ progress(0.1, desc="Loading SLaT and HDRI")
458
+ slat, hdri_np = load_asset_and_hdri(asset_state, hdri_file_obj, tone_mapper_name)
459
+
460
+ progress(0.6, desc="Baking PBR textures")
461
+ glb = PIPELINE.export_glb_from_slat(
462
+ slat, hdri_np,
463
+ hdri_rot_deg=hdri_rot, base_mesh=None,
464
+ simplify=simplify, texture_size=int(texture_size), fill_holes=True,
465
+ )
466
+ glb_path = session_dir / "near_pbr.glb"
467
+ glb.export(glb_path)
468
+ return str(glb_path), f"PBR GLB exported: **{glb_path.name}**"
469
+
470
+
471
+ # ---------------------------------------------------------------------------
472
+ # CSS
473
+ # ---------------------------------------------------------------------------
474
+ CUSTOM_CSS = """
475
+ /* Use full browser width (was max-width:1600px leaving empty margin on the right) */
476
+ .gradio-container { max-width: 100% !important; width: 100% !important; }
477
+ main.gradio-container { max-width: 100% !important; }
478
+ .gradio-wrap { max-width: 100% !important; }
479
+
480
+ /* Top header: TRELLIS-style left-aligned title + bullets */
481
+ .near-app-header {
482
+ text-align: left !important;
483
+ padding: 0.35rem 0 1.1rem 0 !important;
484
+ margin: 0 !important;
485
+ }
486
+ .near-app-header .prose,
487
+ .near-app-header p { margin: 0 !important; }
488
+ .near-app-header h2 {
489
+ font-size: clamp(1.35rem, 2.4vw, 1.85rem) !important;
490
+ font-weight: 700 !important;
491
+ letter-spacing: -0.02em !important;
492
+ margin: 0 0 0.45rem 0 !important;
493
+ line-height: 1.25 !important;
494
+ }
495
+ .near-app-header h2 a {
496
+ color: var(--link-text-color, var(--color-accent)) !important;
497
+ text-decoration: none !important;
498
+ }
499
+ .near-app-header h2 a:hover { text-decoration: underline !important; }
500
+ .near-app-header ul {
501
+ margin: 0 !important;
502
+ padding-left: 1.2rem !important;
503
+ font-size: 0.88rem !important;
504
+ color: #4b5563 !important;
505
+ line-height: 1.45 !important;
506
+ }
507
+ .near-app-header li { margin: 0.15rem 0 !important; }
508
+
509
+ /* Left column: compact section labels (no numbered circles) */
510
+ .section-kicker {
511
+ font-size: 0.7rem !important;
512
+ font-weight: 700 !important;
513
+ color: #9ca3af !important;
514
+ text-transform: uppercase !important;
515
+ letter-spacing: 0.08em !important;
516
+ margin: 0 0 0.45rem 0 !important;
517
+ padding: 0 !important;
518
+ }
519
+
520
+ /* HDRI file picker: light card instead of default dark block */
521
+ .hdri-upload-zone,
522
+ .hdri-file-input,
523
+ .hdri-upload-zone .upload-container,
524
+ .hdri-upload-zone [data-testid="file-upload"],
525
+ .hdri-file-input [data-testid="file-upload"],
526
+ .hdri-upload-zone .file-preview,
527
+ .hdri-file-input .file-preview,
528
+ .hdri-upload-zone .wrap,
529
+ .hdri-file-input .wrap,
530
+ .hdri-upload-zone .panel,
531
+ .hdri-file-input .panel {
532
+ background: #f9fafb !important;
533
+ border-color: #e5e7eb !important;
534
+ color: #374151 !important;
535
+ }
536
+ .hdri-upload-zone .file-preview,
537
+ .hdri-file-input .file-preview { border-radius: 8px !important; }
538
+ .hdri-upload-zone .label-wrap,
539
+ .hdri-file-input .label-wrap { color: #4b5563 !important; }
540
+
541
+ /* HDRI preview image: remove thick / black frame (Gradio panel border) */
542
+ .hdri-preview-image,
543
+ .hdri-preview-image.panel,
544
+ .hdri-preview-image .wrap,
545
+ .hdri-preview-image .image-container,
546
+ .hdri-preview-image .image-frame,
547
+ .hdri-preview-image .image-wrapper,
548
+ .hdri-preview-image [data-testid="image"],
549
+ .hdri-preview-image .icon-buttons,
550
+ .hdri-preview-image img {
551
+ border: none !important;
552
+ outline: none !important;
553
+ box-shadow: none !important;
554
+ }
555
+ .hdri-preview-image img {
556
+ border-radius: 8px !important;
557
+ }
558
+
559
+ /* Export accordion: remove heavy black box; keep a light separator on the header only */
560
+ .export-accordion,
561
+ .export-accordion.panel,
562
+ .export-accordion > div,
563
+ .export-accordion details,
564
+ .export-accordion .label-wrap,
565
+ .export-accordion .accordion-header {
566
+ border: none !important;
567
+ outline: none !important;
568
+ box-shadow: none !important;
569
+ }
570
+ .export-accordion summary,
571
+ .export-accordion .label-wrap {
572
+ border-bottom: 1px solid #e5e7eb !important;
573
+ background: transparent !important;
574
+ }
575
+
576
+ /* Gradio 4+ block chrome sometimes forces --block-border-color */
577
+ .gradio-container .hdri-preview-image,
578
+ .gradio-container .export-accordion {
579
+ --block-border-width: 0px !important;
580
+ --panel-border-width: 0 !important;
581
+ }
582
+
583
+ /* Shadow map preview: same flat frame as HDRI preview */
584
+ .shadow-preview-image,
585
+ .shadow-preview-image.panel,
586
+ .shadow-preview-image .wrap,
587
+ .shadow-preview-image .image-container,
588
+ .shadow-preview-image .image-frame,
589
+ .shadow-preview-image .image-wrapper,
590
+ .shadow-preview-image [data-testid="image"],
591
+ .shadow-preview-image img {
592
+ border: none !important;
593
+ outline: none !important;
594
+ box-shadow: none !important;
595
+ }
596
+ .shadow-preview-image img { border-radius: 8px !important; }
597
+ .gradio-container .shadow-preview-image {
598
+ --block-border-width: 0px !important;
599
+ --panel-border-width: 0 !important;
600
+ }
601
+
602
+ /* Main output tabs: larger, easier to spot */
603
+ .main-output-tabs > .tab-nav,
604
+ .main-output-tabs .tab-nav button {
605
+ font-size: 0.95rem !important;
606
+ font-weight: 600 !important;
607
+ }
608
+ .main-output-tabs .tab-nav button { padding: 0.45rem 0.9rem !important; }
609
+
610
+ /* Status strip: one left accent only (Gradio panel also draws accent — disable it here) */
611
+ .gradio-container .status-footer,
612
+ .status-footer.panel,
613
+ .status-footer.block {
614
+ --block-border-width: 0px !important;
615
+ --panel-border-width: 0px !important;
616
+ }
617
+ .status-footer {
618
+ font-size: 0.8125rem !important;
619
+ line-height: 1.45 !important;
620
+ color: var(--body-text-color-subdued, #6b7280) !important;
621
+ margin: 0 0 0.65rem 0 !important;
622
+ padding: 0.5rem 0.65rem 0.5rem 0.7rem !important;
623
+ background: var(--block-background-fill, #f9fafb) !important;
624
+ /* Single box: one thick left edge (avoid stacking with Gradio .block border) */
625
+ border-width: 1px 1px 1px 3px !important;
626
+ border-style: solid !important;
627
+ border-color: var(--border-color-primary, #e5e7eb) var(--border-color-primary, #e5e7eb)
628
+ var(--border-color-primary, #e5e7eb) var(--color-accent, #2563eb) !important;
629
+ border-radius: 8px !important;
630
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05) !important;
631
+ }
632
+ .status-footer .form,
633
+ .status-footer .wrap,
634
+ .status-footer .prose,
635
+ .status-footer .prose > *:first-child {
636
+ border: none !important;
637
+ box-shadow: none !important;
638
+ }
639
+ .status-footer .prose blockquote {
640
+ border-left: none !important;
641
+ padding-left: 0 !important;
642
+ margin-left: 0 !important;
643
+ }
644
+ .status-footer p,
645
+ .status-footer .prose p {
646
+ margin: 0 !important;
647
+ line-height: 1.05 !important;
648
+ }
649
+ .status-footer strong {
650
+ color: var(--body-text-color, #374151) !important;
651
+ font-weight: 600 !important;
652
+ }
653
+ .status-footer a {
654
+ color: var(--link-text-color, var(--color-accent, #2563eb)) !important;
655
+ text-decoration: none !important;
656
+ }
657
+ .status-footer a:hover { text-decoration: underline !important; }
658
+
659
+ .ctrl-strip {
660
+ border:1px solid #e5e7eb; border-radius:8px;
661
+ padding:0.55rem 0.8rem 0.4rem; margin-bottom:0.6rem; background:#fff;
662
+ }
663
+ .ctrl-strip-title {
664
+ font-size:0.72rem; font-weight:600; color:#9ca3af;
665
+ text-transform:uppercase; letter-spacing:0.06em; margin-bottom:0.4rem;
666
+ }
667
+
668
+ .mat-label {
669
+ font-size:0.72rem; font-weight:700; color:#9ca3af;
670
+ text-transform:uppercase; letter-spacing:0.07em; margin:0.7rem 0 0.2rem;
671
+ }
672
+
673
+ .divider { border:none; border-top:1px solid #e5e7eb; margin:0.5rem 0; }
674
+
675
+ .img-gallery table { display:grid !important; grid-template-columns:repeat(3,1fr) !important; gap:3px !important; }
676
+ .img-gallery table thead { display:none !important; }
677
+ .img-gallery table tr { display:contents !important; }
678
+ .img-gallery table td { padding:0 !important; }
679
+ .img-gallery table td img { width:100% !important; height:68px !important; object-fit:cover !important; border-radius:5px !important; }
680
+
681
+ .hdri-gallery table { display:grid !important; grid-template-columns:repeat(2,1fr) !important; gap:3px !important; }
682
+ .hdri-gallery table thead { display:none !important; }
683
+ .hdri-gallery table tr { display:contents !important; }
684
+ .hdri-gallery table td { padding:0 !important; font-size:0.76rem; text-align:center; word-break:break-all; }
685
+
686
+ /* Right sidebar: align with TRELLIS-style narrow examples column */
687
+ .sidebar-examples { min-width: 0 !important; }
688
+ .sidebar-examples .label-wrap { font-size: 0.85rem !important; }
689
+ .gradio-container .sidebar-examples table { width: 100% !important; }
690
+
691
+ footer { display:none !important; }
692
+ """
693
+
694
+
695
+ # ---------------------------------------------------------------------------
696
+ # UI
697
+ # ---------------------------------------------------------------------------
698
+ def build_app() -> gr.Blocks:
699
+ with gr.Blocks(
700
+ theme=gr.themes.Base(
701
+ primary_hue=gr.themes.colors.blue,
702
+ secondary_hue=gr.themes.colors.blue,
703
+ ),
704
+ title="NeAR",
705
+ css=CUSTOM_CSS,
706
+ delete_cache=(600, 600),
707
+ fill_width=True,
708
+ ) as demo:
709
+ asset_state = gr.State({})
710
+
711
+ gr.Markdown(
712
+ """
713
+ ## Single Image to Relightable 3DGS with [NeAR](https://near-project.github.io/)
714
+ * Upload an RGBA image (or load an existing SLaT), run **Generate Mesh** then **Generate / Load SLaT**, pick an HDRI, and use **Camera & HDRI** to relight.
715
+ * Use **Geometry** for mesh / PBR preview, **Preview** for still renders, **Videos** for camera or HDRI paths; **Export PBR GLB** when you are happy with the result.
716
+ * Texture style transfer is possible when the reference images used for **mesh** and **SLaT** are different.
717
+ """,
718
+ elem_classes=["near-app-header"],
719
+ )
720
+
721
+ _img_ex = [[str(p)] for p in sorted((APP_DIR / "assets/example_image").glob("*.png"))]
722
+ _slat_ex = [[str(p)] for p in sorted((APP_DIR / "assets/example_slats").glob("*.npz"))]
723
+ _hdri_ex = [[str(p)] for p in sorted((APP_DIR / "assets/hdris").glob("*.exr"))]
724
+
725
+ with gr.Row(equal_height=False):
726
+
727
+ # ════════════════════════════════════════════════════════════════
728
+ # LEFT — controls only (TRELLIS-style narrow column)
729
+ # ═════════════════════════════════════════════════��══════════════
730
+ with gr.Column(scale=1, min_width=360):
731
+
732
+ with gr.Group():
733
+ gr.HTML('<p class="section-kicker">Asset</p>')
734
+ source_mode = gr.Radio(
735
+ ["From Image", "From Existing SLaT"],
736
+ value="From Image",
737
+ label="",
738
+ show_label=False,
739
+ )
740
+ with gr.Tabs(selected=0) as source_tabs:
741
+
742
+ with gr.Tab("Image", id=0):
743
+ image_input = gr.Image(
744
+ label="Input Image", type="pil", image_mode="RGBA",
745
+ value=str(DEFAULT_IMAGE) if DEFAULT_IMAGE.exists() else None,
746
+ height=400,
747
+ )
748
+ seed = gr.Slider(0, MAX_SEED, value=42, step=1, label="Seed (SLaT)")
749
+ mesh_button = gr.Button("① Generate Mesh", variant="primary", min_width=100)
750
+
751
+ with gr.Tab("SLaT", id=1):
752
+ slat_upload = gr.File(label="Upload SLaT (.npz)", file_types=[".npz"])
753
+ slat_path_text = gr.Textbox(
754
+ label="Or enter local path",
755
+ placeholder="/path/to/sample_slat.npz",
756
+ )
757
+
758
+ slat_button = gr.Button(
759
+ "② Generate / Load SLaT", variant="primary", min_width=100,
760
+ )
761
+ # gr.HTML(
762
+ # "<div style='font-size:0.78rem;color:#9ca3af;margin-top:0.2rem;'>"
763
+ # "Image mode: run ① then ②. SLaT mode: ② loads file directly.</div>"
764
+ # )
765
+
766
+ with gr.Group():
767
+ gr.HTML('<p class="section-kicker">HDRI</p>')
768
+ with gr.Column(elem_classes=["hdri-upload-zone"]):
769
+ hdri_file = gr.File(
770
+ label="Environment (.exr)", file_types=[".exr"],
771
+ value=str(DEFAULT_HDRI) if DEFAULT_HDRI.exists() else None,
772
+ elem_classes=["hdri-file-input"],
773
+ )
774
+ hdri_preview = gr.Image(
775
+ label="Preview",
776
+ interactive=False,
777
+ height=130,
778
+ container=False,
779
+ elem_classes=["hdri-preview-image"],
780
+ )
781
+
782
+ with gr.Group():
783
+ gr.HTML('<p class="section-kicker">Export</p>')
784
+ with gr.Accordion(
785
+ "Export Settings",
786
+ open=False,
787
+ elem_classes=["export-accordion"],
788
+ ):
789
+ with gr.Row():
790
+ simplify = gr.Slider(0.8, 0.99, value=0.95, step=0.01, label="Mesh Simplify")
791
+ texture_size = gr.Slider(512, 4096, value=2048, step=512, label="Texture Size")
792
+
793
+ with gr.Row():
794
+ clear_button = gr.Button("Clear Cache", variant="secondary", min_width=100)
795
+
796
+ # ════════════════════════════════════════════════════════════════
797
+ # CENTER — status at top, then Camera & HDRI, then tabs
798
+ # ════════════════════════════════════════════════════════════════
799
+ with gr.Column(scale=10, min_width=560):
800
+
801
+ status_md = gr.Markdown(
802
+ "Ready — use **Asset** (left) and **HDRI** to begin.",
803
+ elem_classes=["status-footer"],
804
+ )
805
+
806
+
807
+ with gr.Group(elem_classes=["ctrl-strip"]):
808
+ gr.HTML("<div class='ctrl-strip-title'>Camera &amp; HDRI</div>")
809
+ with gr.Row():
810
+ tone_mapper_name = gr.Dropdown(
811
+ choices=TONE_MAPPER_CHOICES,
812
+ value=TONE_MAPPER_CHOICES[0] if TONE_MAPPER_CHOICES else None,
813
+ label="Tone Mapper",
814
+ min_width=120,
815
+ allow_custom_value=True,
816
+ )
817
+ hdri_rot = gr.Slider(0, 360, value=0, step=1, label="HDRI Rotation °")
818
+ resolution = gr.Slider(256, 1024, value=512, step=256, label="Preview Res")
819
+ with gr.Row():
820
+ yaw = gr.Slider(0, 360, value=0, step=0.5, label="Yaw °")
821
+ pitch = gr.Slider(-90, 90, value=0, step=0.5, label="Pitch °")
822
+ fov = gr.Slider(10, 70, value=40, step=1, label="FoV")
823
+ radius = gr.Slider(1.0, 4.0, value=2.0, step=0.05, label="Radius")
824
+
825
+ with gr.Tabs(elem_classes=["main-output-tabs"]):
826
+
827
+ with gr.Tab("Geometry", id=0):
828
+ with gr.Row():
829
+ mesh_viewer = gr.Model3D(
830
+ label="3D Mesh", interactive=False, height=520,
831
+ )
832
+ pbr_viewer = gr.Model3D(
833
+ label="PBR GLB", interactive=False, height=520,
834
+ )
835
+ gr.HTML("<hr class='divider'>")
836
+ with gr.Row():
837
+ export_glb_button = gr.Button("Export PBR GLB", variant="primary", min_width=140)
838
+
839
+ with gr.Tab("Preview", id=1):
840
+ gr.HTML(
841
+ "<p style='font-size:0.78rem;color:#9ca3af;margin:0 0 0.35rem 0;'>"
842
+ "Use <b>Camera &amp; HDRI</b> under the tabs, then render.</p>"
843
+ )
844
+ preview_button = gr.Button("Render Preview", variant="primary", min_width=100)
845
+ gr.HTML("<hr class='divider'>")
846
+ with gr.Row():
847
+ color_output = gr.Image(label="Relit Result", interactive=False, height=400)
848
+ with gr.Column():
849
+ with gr.Row():
850
+ base_color_output = gr.Image(label="Base Color", interactive=False, height=200)
851
+ metallic_output = gr.Image(label="Metallic", interactive=False, height=200)
852
+ with gr.Row():
853
+ roughness_output = gr.Image(label="Roughness", interactive=False, height=200)
854
+ shadow_output = gr.Image(label="Shadow", interactive=False, height=200)
855
+
856
+ with gr.Tab("Videos", id=2):
857
+ with gr.Accordion("Video Settings", open=False):
858
+ with gr.Row():
859
+ fps = gr.Slider(1, 60, value=24, step=1, label="FPS")
860
+ num_views = gr.Slider(8, 120, value=40, step=1, label="Camera Frames")
861
+ num_frames = gr.Slider(8, 120, value=40, step=1, label="HDRI Frames")
862
+ with gr.Row():
863
+ full_video = gr.Checkbox(label="Full composite video", value=True)
864
+ shadow_video = gr.Checkbox(
865
+ label="Include shadow in video",
866
+ value=True,
867
+ )
868
+ with gr.Row():
869
+ camera_video_button = gr.Button("Camera Path Video", variant="primary", min_width=100)
870
+ hdri_video_button = gr.Button("HDRI Rotation Video", variant="primary", min_width=100)
871
+ camera_video_output = gr.Video(
872
+ label="Camera Path", autoplay=True, loop=True, height=340,
873
+ )
874
+ hdri_render_video_output = gr.Video(
875
+ label="HDRI Rotation Render", autoplay=True, loop=True, height=300,
876
+ )
877
+ with gr.Accordion("HDRI Roll (environment panorama)", open=False):
878
+ hdri_roll_video_output = gr.Video(
879
+ label="HDRI Roll", autoplay=True, loop=True, height=180,
880
+ )
881
+
882
+
883
+ # ════════════════════════════════════════════════════════════════
884
+ # RIGHT — examples sidebar (TRELLIS-style narrow column)
885
+ # ════════════════════════════════════════════════════════════════
886
+ with gr.Column(scale=1, min_width=172):
887
+ with gr.Column(visible=True, elem_classes=["sidebar-examples", "img-gallery"]) as col_img_examples:
888
+ if _img_ex:
889
+ gr.Examples(
890
+ examples=_img_ex,
891
+ inputs=[image_input],
892
+ fn=preprocess_image_only,
893
+ outputs=[image_input],
894
+ run_on_click=True,
895
+ examples_per_page=18,
896
+ label="Examples",
897
+ )
898
+ else:
899
+ gr.Markdown("*No PNG examples in `assets/example_image`*")
900
+
901
+ with gr.Column(visible=False, elem_classes=["sidebar-examples"]) as col_slat_examples:
902
+ if _slat_ex:
903
+ gr.Examples(
904
+ examples=_slat_ex,
905
+ inputs=[slat_path_text],
906
+ label="Example SLaTs",
907
+ )
908
+ else:
909
+ gr.Markdown("*No `.npz` examples in `assets/example_slats`*")
910
+
911
+ with gr.Column(visible=True, elem_classes=["sidebar-examples", "hdri-gallery"]) as col_hdri_examples:
912
+ if _hdri_ex:
913
+ gr.Examples(
914
+ examples=_hdri_ex,
915
+ inputs=[hdri_file],
916
+ label="Example HDRIs",
917
+ examples_per_page=8,
918
+ )
919
+ else:
920
+ gr.Markdown("*No `.exr` examples in `assets/hdris`*")
921
+
922
+ # ── Event wiring ─────────────────────────────────────────────────────
923
+ demo.unload(end_session)
924
+
925
+ # Default image: preprocess once on load. Do NOT use image_input.change → outputs=[image_input]:
926
+ # that retriggers change forever (spinner) because updating the same component fires change again.
927
+ if DEFAULT_IMAGE.exists():
928
+ demo.load(preprocess_default_image, outputs=[image_input])
929
+
930
+ source_mode.change(switch_asset_source, inputs=[source_mode], outputs=[source_tabs])
931
+ source_mode.change(
932
+ lambda m: (
933
+ gr.update(visible=m == "From Image"),
934
+ gr.update(visible=m == "From Existing SLaT"),
935
+ ),
936
+ inputs=[source_mode],
937
+ outputs=[col_img_examples, col_slat_examples],
938
+ )
939
+
940
+ for _trigger in (hdri_file.upload, hdri_file.change, tone_mapper_name.change):
941
+ _trigger(
942
+ preview_hdri,
943
+ inputs=[hdri_file, tone_mapper_name],
944
+ outputs=[hdri_preview, status_md],
945
+ )
946
+
947
+ # Same as TRELLIS.2 app.py: only on upload — avoids infinite preprocess loop.
948
+ image_input.upload(
949
+ preprocess_image_only,
950
+ inputs=[image_input],
951
+ outputs=[image_input],
952
+ )
953
+
954
+ mesh_button.click(
955
+ generate_mesh,
956
+ inputs=[image_input],
957
+ outputs=[asset_state, mesh_viewer, status_md],
958
+ )
959
+
960
+ slat_button.click(
961
+ prepare_slat,
962
+ inputs=[source_mode, asset_state, image_input, seed, slat_upload, slat_path_text],
963
+ outputs=[asset_state, status_md],
964
+ )
965
+
966
+ preview_button.click(
967
+ render_preview,
968
+ inputs=[asset_state, hdri_file, tone_mapper_name, hdri_rot,
969
+ yaw, pitch, fov, radius, resolution],
970
+ outputs=[
971
+ color_output,
972
+ base_color_output,
973
+ metallic_output,
974
+ roughness_output,
975
+ shadow_output,
976
+ status_md,
977
+ ],
978
+ )
979
+
980
+ camera_video_button.click(
981
+ render_camera_video,
982
+ inputs=[asset_state, hdri_file, tone_mapper_name, hdri_rot,
983
+ fps, num_views, fov, radius, full_video, shadow_video],
984
+ outputs=[camera_video_output, status_md],
985
+ )
986
+
987
+ hdri_video_button.click(
988
+ render_hdri_video,
989
+ inputs=[asset_state, hdri_file, tone_mapper_name,
990
+ fps, num_frames, yaw, pitch, fov, radius, full_video, shadow_video],
991
+ outputs=[hdri_roll_video_output, hdri_render_video_output, status_md],
992
+ )
993
+
994
+ export_glb_button.click(
995
+ export_glb,
996
+ inputs=[asset_state, hdri_file, tone_mapper_name, hdri_rot, simplify, texture_size],
997
+ outputs=[pbr_viewer, status_md],
998
+ )
999
+
1000
+ clear_button.click(
1001
+ clear_session_dir,
1002
+ outputs=[status_md],
1003
+ ).then(
1004
+ lambda: ({}, None, None, None, None, None, None, None, None, None, None),
1005
+ outputs=[
1006
+ asset_state,
1007
+ mesh_viewer,
1008
+ pbr_viewer,
1009
+ color_output,
1010
+ base_color_output,
1011
+ metallic_output,
1012
+ roughness_output,
1013
+ shadow_output,
1014
+ camera_video_output,
1015
+ hdri_roll_video_output,
1016
+ hdri_render_video_output,
1017
+ ],
1018
+ )
1019
+
1020
+ return demo
1021
+
1022
+
1023
+ demo = build_app()
1024
+ demo.queue(max_size=8)
1025
+
1026
+ # ---------------------------------------------------------------------------
1027
+ # Entry point
1028
+ # ---------------------------------------------------------------------------
1029
+ if __name__ == "__main__":
1030
+ import argparse
1031
+
1032
+ parser = argparse.ArgumentParser()
1033
+ parser.add_argument(
1034
+ "--host",
1035
+ type=str,
1036
+ default=os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"),
1037
+ )
1038
+ parser.add_argument(
1039
+ "--port",
1040
+ type=int,
1041
+ default=int(
1042
+ os.environ.get("PORT", os.environ.get("GRADIO_SERVER_PORT", str(DEFAULT_PORT)))
1043
+ ),
1044
+ )
1045
+ parser.add_argument("--share", action="store_true")
1046
+ args = parser.parse_args()
1047
 
1048
+ demo.launch(
1049
+ server_name=args.host,
1050
+ server_port=args.port,
1051
+ share=args.share,
1052
+ )