Spaces:
Running
Running
| import os | |
| import shutil | |
| import html | |
| import logging | |
| import numpy as np | |
| import gradio as gr | |
| # Setup professional logging | |
| logging.basicConfig(level=logging.INFO, format="[%(asctime)s] %(levelname)s - %(message)s") | |
| logger = logging.getLogger("PaGeR-Showcase") | |
| # Setup static folders | |
| assets_dir = "assets" | |
| os.makedirs(assets_dir, exist_ok=True) | |
| # Create fallback simulation room point cloud if none exists | |
| def generate_fallback_ply(filepath): | |
| """ | |
| Generates a beautiful 3D test room shell (walls, floor, ceiling) | |
| with a red sphere and green sphere. Approximately 110,000 colored points. | |
| """ | |
| logger.info(f"Generating fallback point cloud at: {filepath}") | |
| points = [] | |
| num_points_wall = 15000 | |
| # Floor (y = -2.5) | |
| for _ in range(num_points_wall): | |
| x = np.random.uniform(-5, 5) | |
| z = np.random.uniform(-5, 5) | |
| y = -2.5 | |
| r = int((x + 5) * 25.5) | |
| g = 45 | |
| b = int((z + 5) * 25.5) | |
| points.append(f"{x} {y} {z} {r} {g} {b}") | |
| # Ceiling (y = 2.5) | |
| for _ in range(num_points_wall): | |
| x = np.random.uniform(-5, 5) | |
| z = np.random.uniform(-5, 5) | |
| y = 2.5 | |
| r = 45 | |
| g = int((x + 5) * 25.5) | |
| b = int((z + 5) * 25.5) | |
| points.append(f"{x} {y} {z} {r} {g} {b}") | |
| # 4 Walls (x = -5, x = 5, z = -5, z = 5) | |
| for _ in range(num_points_wall * 4): | |
| wall = np.random.choice([0, 1, 2, 3]) | |
| y = np.random.uniform(-2.5, 2.5) | |
| if wall == 0: # back wall z = -5 | |
| x = np.random.uniform(-5, 5) | |
| z = -5 | |
| r = 100 | |
| g = int((y + 2.5) * 51) | |
| b = int((x + 5) * 25.5) | |
| elif wall == 1: # front wall z = 5 | |
| x = np.random.uniform(-5, 5) | |
| z = 5 | |
| r = int((x + 5) * 25.5) | |
| g = 100 | |
| b = int((y + 2.5) * 51) | |
| elif wall == 2: # left wall x = -5 | |
| x = -5 | |
| z = np.random.uniform(-5, 5) | |
| r = int((z + 5) * 25.5) | |
| g = int((y + 2.5) * 51) | |
| b = 100 | |
| else: # right wall x = 5 | |
| x = 5 | |
| z = np.random.uniform(-5, 5) | |
| r = int((y + 2.5) * 51) | |
| g = int((z + 5) * 25.5) | |
| b = 150 | |
| points.append(f"{x} {y} {z} {r} {g} {b}") | |
| # Decorative Interior Sphere 1 (Red) | |
| for _ in range(20000): | |
| u = np.random.random() | |
| v = np.random.random() | |
| theta = u * 2.0 * np.pi | |
| phi = np.arccos(2.0 * v - 1.0) | |
| r = 1.0 | |
| x = 1.8 + r * np.sin(phi) * np.cos(theta) | |
| y = -1.2 + r * np.sin(phi) * np.sin(theta) | |
| z = 1.8 + r * np.cos(phi) | |
| points.append(f"{x} {y} {z} 239 68 68") | |
| # Decorative Interior Sphere 2 (Green) | |
| for _ in range(20000): | |
| u = np.random.random() | |
| v = np.random.random() | |
| theta = u * 2.0 * np.pi | |
| phi = np.arccos(2.0 * v - 1.0) | |
| r = 1.2 | |
| x = -2.2 + r * np.sin(phi) * np.cos(theta) | |
| y = -0.8 + r * np.sin(phi) * np.sin(theta) | |
| z = -2.2 + r * np.cos(phi) | |
| points.append(f"{x} {y} {z} 34 197 94") | |
| # Construct ASCII PLY format header | |
| header = f"""ply | |
| format ascii 1.0 | |
| element vertex {len(points)} | |
| property float x | |
| property float y | |
| property float z | |
| property uchar red | |
| property uchar green | |
| property uchar blue | |
| end_header | |
| """ | |
| with open(filepath, "w") as f: | |
| f.write(header + "\n".join(points)) | |
| logger.info(f"Fallback point cloud generated successfully with {len(points)} points.") | |
| # Pre-generate preview1 if missing | |
| preview_ply = os.path.join(assets_dir, "preview1.ply") | |
| if not os.path.exists(preview_ply): | |
| generate_fallback_ply(preview_ply) | |
| def get_ply_files(): | |
| """Scans the assets directory and returns a sorted list of PLY point cloud files.""" | |
| files = [f for f in os.listdir(assets_dir) if f.endswith(".ply")] | |
| logger.info(f"Scanning 'assets/' folder. Found PLY files: {files}") | |
| if not files: | |
| return [] | |
| return sorted([os.path.join(assets_dir, f) for f in files]) | |
| def get_viewer_html(ply_url): | |
| """Generates the embedded HTML template for the ThreeJS WebGL view.""" | |
| return f"""<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>3D Point Cloud Viewer</title> | |
| <style> | |
| * {{ box-sizing: border-box; }} | |
| body, html {{ | |
| margin: 0; padding: 0; width: 100%; height: 100%; | |
| overflow: hidden; background-color: #0b0f19; | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | |
| user-select: none; | |
| }} | |
| #canvas-container {{ width: 100%; height: 100%; position: relative; }} | |
| #three-canvas {{ width: 100%; height: 100%; cursor: grab; }} | |
| #three-canvas:active {{ cursor: grabbing; }} | |
| .overlay {{ | |
| position: absolute; | |
| background: rgba(15, 23, 42, 0.85); | |
| backdrop-filter: blur(8px); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| color: #fff; | |
| border-radius: 8px; | |
| padding: 14px; | |
| z-index: 10; | |
| }} | |
| .controls-left {{ | |
| top: 15px; left: 15px; | |
| max-width: 260px; | |
| pointer-events: none; | |
| }} | |
| .controls-right {{ | |
| bottom: 15px; right: 15px; | |
| width: 250px; | |
| display: flex; flex-direction: column; gap: 12px; | |
| }} | |
| h3 {{ | |
| margin: 0 0 8px 0; font-size: 11px; color: #94a3b8; | |
| text-transform: uppercase; letter-spacing: 0.05em; | |
| }} | |
| .instruction-row {{ | |
| font-size: 11px; display: flex; align-items: center; gap: 8px; margin-bottom: 6px; color: #cbd5e1; | |
| }} | |
| .key-badge {{ | |
| background: rgba(255,255,255,0.15); border-radius: 4px; padding: 2px 6px; | |
| font-family: monospace; font-size: 10px; border: 1px solid rgba(255,255,255,0.2); | |
| color: #3b82f6; font-weight: bold; | |
| }} | |
| .slider-group {{ display: flex; flex-direction: column; gap: 4px; }} | |
| .slider-group label {{ font-size: 11px; color: #cbd5e1; display: flex; justify-content: space-between; }} | |
| .slider-group input[type="range"] {{ | |
| width: 100%; height: 4px; border-radius: 2px; outline: none; background: rgba(255,255,255,0.2); -webkit-appearance: none; | |
| }} | |
| .slider-group input[type="range"]::-webkit-slider-thumb {{ | |
| -webkit-appearance: none; width: 12px; height: 12px; border-radius: 50%; background: #3b82f6; cursor: pointer; | |
| }} | |
| .btn {{ | |
| background: #3b82f6; border: none; color: white; padding: 8px 10px; border-radius: 4px; | |
| font-size: 11px; cursor: pointer; transition: background 0.2s; font-weight: 600; | |
| text-transform: uppercase; width: 100%; letter-spacing: 0.05em; | |
| }} | |
| .btn:hover {{ background: #2563eb; }} | |
| .stats-row {{ | |
| border-top: 1px solid rgba(255,255,255,0.1); padding-top: 8px; font-size: 10px; color: #94a3b8; display: flex; justify-content: space-between; | |
| }} | |
| #loading-overlay {{ | |
| position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: #0b0f19; | |
| display: flex; flex-direction: column; justify-content: center; align-items: center; | |
| z-index: 100; transition: opacity 0.4s ease; | |
| }} | |
| .spinner {{ | |
| width: 40px; height: 40px; border: 3px solid rgba(255,255,255,0.1); border-radius: 50%; | |
| border-top-color: #3b82f6; animation: spin 0.8s linear infinite; | |
| }} | |
| #loading-text {{ color: #e2e8f0; font-size: 13px; margin-top: 15px; font-weight: 500; }} | |
| #loading-progress {{ color: #64748b; font-size: 11px; margin-top: 4px; }} | |
| @keyframes spin {{ 0% {{ transform: rotate(0deg); }} 100% {{ transform: rotate(360deg); }} }} | |
| </style> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/PLYLoader.js"></script> | |
| </head> | |
| <body> | |
| <div id="canvas-container"> | |
| <div id="three-canvas"></div> | |
| <div id="loading-overlay"> | |
| <div class="spinner"></div> | |
| <div id="loading-text">Loading 3D Point Cloud...</div> | |
| <div id="loading-progress">0%</div> | |
| </div> | |
| <div class="overlay controls-left"> | |
| <h3>Navigation</h3> | |
| <div class="instruction-row"><span class="key-badge">Drag Mouse</span> Look Around</div> | |
| <div class="instruction-row"><span class="key-badge">W, A, S, D</span> Fly / Move Ground</div> | |
| <div class="instruction-row"><span class="key-badge">SPACE</span> / <span class="key-badge">E</span> Move UP</div> | |
| <div class="instruction-row"><span class="key-badge">SHIFT</span> / <span class="key-badge">Q</span> Move DOWN</div> | |
| </div> | |
| <div class="overlay controls-right"> | |
| <h3>Settings</h3> | |
| <div class="slider-group"> | |
| <label><span>Point Size</span><span id="val-point-size">0.02</span></label> | |
| <input id="slider-point-size" type="range" min="0.001" max="0.100" step="0.001" value="0.020"> | |
| </div> | |
| <div class="slider-group"> | |
| <label><span>Movement Speed</span><span id="val-speed">0.05</span></label> | |
| <input id="slider-speed" type="range" min="0.01" max="0.40" step="0.01" value="0.05"> | |
| </div> | |
| <button id="btn-reset" class="btn">Reset to Center</button> | |
| <div class="stats-row"> | |
| <span>Points: <span id="stat-points">-</span></span> | |
| <span>FPS: <span id="stat-fps">0</span></span> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let pointSize = 0.02; | |
| let flightSpeed = 0.05; | |
| let cloudCenter = new THREE.Vector3(0, 0, 0); | |
| let scene, camera, renderer, pointsObject; | |
| let lat = 0, lon = 0; | |
| let isDragging = false; | |
| let startX, startY, startLon, startLat; | |
| const keys = {{}}; | |
| const canvasContainer = document.getElementById('canvas-container'); | |
| const pointSizeSlider = document.getElementById('slider-point-size'); | |
| const pointSizeVal = document.getElementById('val-point-size'); | |
| const speedSlider = document.getElementById('slider-speed'); | |
| const speedVal = document.getElementById('val-speed'); | |
| const resetBtn = document.getElementById('btn-reset'); | |
| const statPoints = document.getElementById('stat-points'); | |
| const statFps = document.getElementById('stat-fps'); | |
| const loadingOverlay = document.getElementById('loading-overlay'); | |
| const loadingProgress = document.getElementById('loading-progress'); | |
| let lastTime = performance.now(); | |
| let frameCount = 0; | |
| function init() {{ | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x0b0f19); | |
| camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.01, 1000); | |
| camera.position.set(0, 0, 0); | |
| renderer = new THREE.WebGLRenderer({{ antialias: true, powerPreference: "high-performance" }}); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| document.getElementById('three-canvas').appendChild(renderer.domElement); | |
| window.addEventListener('resize', onWindowResize); | |
| renderer.domElement.addEventListener('mousedown', onMouseDown); | |
| window.addEventListener('mousemove', onMouseMove); | |
| window.addEventListener('mouseup', onMouseUp); | |
| renderer.domElement.addEventListener('mouseenter', () => {{ window.focus(); }}); | |
| window.addEventListener('keydown', (e) => {{ keys[e.key.toLowerCase()] = true; }}); | |
| window.addEventListener('keyup', (e) => {{ keys[e.key.toLowerCase()] = false; }}); | |
| pointSizeSlider.addEventListener('input', (e) => {{ | |
| pointSize = parseFloat(e.target.value); | |
| pointSizeVal.textContent = pointSize.toFixed(3); | |
| if (pointsObject) pointsObject.material.size = pointSize; | |
| }}); | |
| speedSlider.addEventListener('input', (e) => {{ | |
| flightSpeed = parseFloat(e.target.value); | |
| speedVal.textContent = flightSpeed.toFixed(2); | |
| }}); | |
| resetBtn.addEventListener('click', () => {{ | |
| camera.position.copy(cloudCenter); | |
| lat = 0; | |
| lon = 0; | |
| }}); | |
| loadPointCloud(); | |
| animate(); | |
| }} | |
| function loadPointCloud() {{ | |
| const loader = new THREE.PLYLoader(); | |
| const plyFile = "{ply_url}"; | |
| console.log("Attempting to load point cloud from URL:", plyFile); | |
| loader.load( | |
| plyFile, | |
| function (geometry) {{ | |
| console.log("Point cloud parsed successfully!"); | |
| const hasColors = geometry.hasAttribute('color'); | |
| const material = new THREE.PointsMaterial({{ | |
| size: pointSize, | |
| vertexColors: hasColors, | |
| sizeAttenuation: true | |
| }}); | |
| if (!hasColors) {{ | |
| material.color = new THREE.Color(0x3b82f6); | |
| }} | |
| pointsObject = new THREE.Points(geometry, material); | |
| scene.add(pointsObject); | |
| const count = geometry.attributes.position.count; | |
| statPoints.textContent = count.toLocaleString(); | |
| geometry.computeBoundingBox(); | |
| const box = geometry.boundingBox; | |
| box.getCenter(cloudCenter); | |
| const size = new THREE.Vector3(); | |
| box.getSize(size); | |
| const maxDim = Math.max(size.x, size.y, size.z); | |
| if (maxDim > 50) {{ | |
| pointSize = maxDim * 0.001; | |
| flightSpeed = maxDim * 0.005; | |
| }} else if (maxDim > 5) {{ | |
| pointSize = maxDim * 0.005; | |
| flightSpeed = maxDim * 0.01; | |
| }} else {{ | |
| pointSize = 0.015; | |
| flightSpeed = 0.04; | |
| }} | |
| pointSizeSlider.min = (pointSize * 0.05).toFixed(4); | |
| pointSizeSlider.max = (pointSize * 15.0).toFixed(4); | |
| pointSizeSlider.value = pointSize; | |
| pointSizeVal.textContent = pointSize.toFixed(3); | |
| speedSlider.min = (flightSpeed * 0.1).toFixed(3); | |
| speedSlider.max = (flightSpeed * 8.0).toFixed(3); | |
| speedSlider.value = flightSpeed; | |
| speedVal.textContent = flightSpeed.toFixed(2); | |
| if (pointsObject) pointsObject.material.size = pointSize; | |
| // Center the camera inside the middle POV of the bounding box | |
| camera.position.copy(cloudCenter); | |
| loadingOverlay.style.opacity = 0; | |
| setTimeout(() => {{ loadingOverlay.style.display = 'none'; }}, 400); | |
| }}, | |
| function (xhr) {{ | |
| if (xhr.total) {{ | |
| const percent = Math.round((xhr.loaded / xhr.total) * 100); | |
| loadingProgress.textContent = percent + '%'; | |
| }} else {{ | |
| loadingProgress.textContent = (xhr.loaded / (1024 * 1024)).toFixed(1) + ' MB'; | |
| }} | |
| }}, | |
| function (error) {{ | |
| console.error("Loader Error Detailed Log:", error); | |
| document.getElementById('loading-text').textContent = 'Error loading point cloud file.'; | |
| loadingProgress.textContent = 'Please make sure that the PLY file is placed in the assets/ directory.'; | |
| }} | |
| ); | |
| }} | |
| function onWindowResize() {{ | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| }} | |
| function onMouseDown(e) {{ | |
| isDragging = true; | |
| startX = e.clientX; | |
| startY = e.clientY; | |
| startLon = lon; | |
| startLat = lat; | |
| }} | |
| function onMouseMove(e) {{ | |
| if (!isDragging) return; | |
| const dx = e.clientX - startX; | |
| const dy = e.clientY - startY; | |
| lon = startLon - dx * 0.15; | |
| lat = startLat + dy * 0.15; | |
| }} | |
| function onMouseUp() {{ | |
| isDragging = false; | |
| }} | |
| function animate() {{ | |
| requestAnimationFrame(animate); | |
| lat = Math.max(-85, Math.min(85, lat)); | |
| const phi = THREE.MathUtils.degToRad(90 - lat); | |
| const theta = THREE.MathUtils.degToRad(lon); | |
| const target = new THREE.Vector3(); | |
| target.x = camera.position.x + Math.sin(phi) * Math.cos(theta); | |
| target.y = camera.position.y + Math.cos(phi); | |
| target.z = camera.position.z + Math.sin(phi) * Math.sin(theta); | |
| camera.lookAt(target); | |
| const forward = new THREE.Vector3(); | |
| camera.getWorldDirection(forward); | |
| forward.y = 0; | |
| if (forward.lengthSq() < 0.001) {{ | |
| forward.set(Math.sin(THREE.MathUtils.degToRad(lon)), 0, Math.cos(THREE.MathUtils.degToRad(lon))); | |
| }} | |
| forward.normalize(); | |
| const right = new THREE.Vector3(); | |
| right.crossVectors(camera.up, forward).normalize(); | |
| if (keys['w'] || keys['arrowup']) {{ | |
| camera.position.addScaledVector(forward, flightSpeed); | |
| }} | |
| if (keys['s'] || keys['arrowdown']) {{ | |
| camera.position.addScaledVector(forward, -flightSpeed); | |
| }} | |
| if (keys['a'] || keys['arrowleft']) {{ | |
| camera.position.addScaledVector(right, flightSpeed); | |
| }} | |
| if (keys['d'] || keys['arrowright']) {{ | |
| camera.position.addScaledVector(right, -flightSpeed); | |
| }} | |
| if (keys[' '] || keys['e']) {{ | |
| camera.position.y += flightSpeed; | |
| }} | |
| if (keys['shift'] || keys['q']) {{ | |
| camera.position.y -= flightSpeed; | |
| }} | |
| renderer.render(scene, camera); | |
| frameCount++; | |
| const time = performance.now(); | |
| if (time >= lastTime + 1000) {{ | |
| statFps.textContent = Math.round((frameCount * 1000) / (time - lastTime)); | |
| frameCount = 0; | |
| lastTime = time; | |
| }} | |
| }} | |
| window.onload = init; | |
| </script> | |
| </body> | |
| </html>""" | |
| def update_viewer_iframe(selected_file): | |
| """ | |
| Constructs a responsive iframe utilizing a secure inline HTML source. | |
| Also handles route mappings based on the Gradio major version. | |
| """ | |
| if not selected_file: | |
| logger.warning("No file selected in update_viewer_iframe!") | |
| return "No file selected." | |
| # Map to the absolute path on the host, correcting cross-platform path separators | |
| abs_ply_path = os.path.abspath(selected_file).replace("\\", "/") | |
| # Check the Gradio version to decide correct file route mapping | |
| gradio_major_version = int(gr.__version__.split(".")[0]) | |
| if gradio_major_version >= 5: | |
| # Gradio 5 and 6 require /gradio_api/ prefix for custom assets | |
| ply_url = f"/gradio_api/file={abs_ply_path}" | |
| else: | |
| # Legacy fallback | |
| ply_url = f"/file={abs_ply_path}" | |
| logger.info(f"Target file: '{selected_file}'") | |
| logger.info(f"Resolved path: '{abs_ply_path}'") | |
| logger.info(f"Gradio Major Version: {gradio_major_version}. Generated URL mapping: '{ply_url}'") | |
| raw_html = get_viewer_html(ply_url) | |
| escaped_html = html.escape(raw_html) | |
| return f'<iframe srcdoc="{escaped_html}" style="width:100%; height:750px; border:none; border-radius:12px; background:#0b0f19; box-shadow: 0 4px 20px rgba(0,0,0,0.45);"></iframe>' | |
| def handle_file_upload(file_obj): | |
| """Saves user-uploaded .ply files to assets/ and selects them.""" | |
| if file_obj is None: | |
| return gr.update() | |
| dest_filename = os.path.basename(file_obj.name) | |
| dest_path = os.path.join(assets_dir, dest_filename) | |
| logger.info(f"User uploaded file. Storing to: {dest_path}") | |
| shutil.copy(file_obj.name, dest_path) | |
| updated_files = get_ply_files() | |
| return gr.update(choices=updated_files, value=dest_path) | |
| def refresh_file_list(): | |
| """Manual scan update for files placed into assets/ via git or CLI.""" | |
| logger.info("Manual assets/ directory scan triggered.") | |
| updated_files = get_ply_files() | |
| default_val = updated_files[0] if updated_files else None | |
| return gr.update(choices=updated_files, value=default_val) | |
| # Handle theme configuration based on Gradio version | |
| theme = gr.themes.Default( | |
| primary_hue="blue", | |
| neutral_hue="slate", | |
| ).set( | |
| body_background_fill="*neutral_950", | |
| block_background_fill="*neutral_900", | |
| block_border_color="*neutral_800", | |
| button_secondary_background_fill="*neutral_800" | |
| ) | |
| gradio_major_version = int(gr.__version__.split(".")[0]) | |
| logger.info(f"Starting application layout. Detected Gradio Major: {gradio_major_version}") | |
| blocks_kwargs = {} | |
| launch_kwargs = { | |
| "allowed_paths": [os.path.abspath(assets_dir)] | |
| } | |
| # Resolve the Gradio 6 theme warning by adjusting kwarg targets | |
| if gradio_major_version >= 6: | |
| launch_kwargs["theme"] = theme | |
| else: | |
| blocks_kwargs["theme"] = theme | |
| with gr.Blocks(title="PaGeR Point Cloud Showcase", **blocks_kwargs) as demo: | |
| gr.Markdown( | |
| """ | |
| # ๐ช Panoramic Point Cloud Viewer | |
| Load and navigate large panoramic point cloud files (up to **1,000,000+ points**). The camera places you directly in the **middle POV** on load, rendering optimal spatial depth. | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=3): | |
| file_dropdown = gr.Dropdown( | |
| choices=get_ply_files(), | |
| value=get_ply_files()[0] if get_ply_files() else None, | |
| label="๐ Select Point Cloud File", | |
| interactive=True, | |
| ) | |
| with gr.Column(scale=1): | |
| refresh_btn = gr.Button("๐ Scan assets/", size="sm") | |
| with gr.Column(scale=2): | |
| upload_zone = gr.File( | |
| label="๐ค Drag & Drop New .PLY File Here", | |
| file_types=[".ply"], | |
| type="filepath", | |
| height=80 | |
| ) | |
| # Core interactive WebGL viewport component | |
| viewer_component = gr.HTML( | |
| value=update_viewer_iframe(file_dropdown.value) if file_dropdown.value else "" | |
| ) | |
| # Event routing | |
| file_dropdown.change( | |
| fn=update_viewer_iframe, | |
| inputs=file_dropdown, | |
| outputs=viewer_component | |
| ) | |
| upload_zone.upload( | |
| fn=handle_file_upload, | |
| inputs=upload_zone, | |
| outputs=file_dropdown | |
| ) | |
| refresh_btn.click( | |
| fn=refresh_file_list, | |
| inputs=[], | |
| outputs=file_dropdown | |
| ) | |
| # Static asset paths for custom resource streaming | |
| gr.set_static_paths(paths=[assets_dir]) | |
| if __name__ == "__main__": | |
| logger.info(f"Launching Gradio with arguments: {launch_kwargs}") | |
| demo.launch(**launch_kwargs) |