Reverb/open3dforge
0
1"""2UI helpers โ shared functions across Gradio tabs.3 4Keeps `app.py` focused on layout, not formatting logic.5"""6 7from __future__ import annotations8 9from . import quota, workspace10 11 12def get_status_bar() -> str:13 """Build the global status bar shown at the bottom of every tab.14 15 Markdown-formatted single line with quota + workspace info.16 """17 state = workspace.get_state()18 parts = [19 quota.format_status(),20 f"๐ Workspace: {workspace.current_size_mb():.1f} MB",21 f"๐ฆ Exports: {workspace.export_count()}",22 ]23 if state.model_used:24 parts.append(f"๐ง Last: {state.model_used}")25 return " ยท ".join(parts)26 27 28def get_asset_summary() -> str:29 """Multi-line summary of what's currently in `workspace/current/`.30 31 Used in the viewer panel to show pipeline progress.32 """33 state = workspace.get_state()34 lines = [f"**Asset:** `{state.asset_name}`"]35 36 if state.face_count:37 lines.append(f"- Faces: {state.face_count:,}")38 lines.append(f"- Vertices: {state.vertex_count:,}")39 40 stages = []41 if state.high_poly_glb:42 stages.append("โ Generated")43 if state.cleaned_glb:44 stages.append("โ Cleaned")45 if state.low_poly_glb:46 stages.append("โ Decimated")47 if state.unwrapped_glb:48 stages.append("โ UV unwrapped")49 if state.normal_dx_png or state.normal_gl_png:50 stages.append("โ Normal baked")51 if state.albedo_png:52 stages.append("โ Albedo baked")53 if state.orm_png or (state.roughness_png and state.metallic_png):54 stages.append("โ PBR maps")55 if state.lod_glbs:56 stages.append(f"โ LODs ({len(state.lod_glbs)})")57 if state.collision_glb:58 stages.append("โ Collision")59 if state.rigged_glb or state.rigged_fbx:60 stages.append("โ Rigged")61 62 if stages:63 lines.append("")64 lines.append("**Progress:**")65 for s in stages:66 lines.append(f"- {s}")67 else:68 lines.append("")69 lines.append("*No asset loaded yet. Start at the Generate tab.*")70 71 return "\n".join(lines)72 73 74def get_viewer_model_path() -> str | None:75 """Pick the best GLB to show in the 3D viewer.76 77 Reads the filesystem directly so it works across the ZeroGPU subprocess78 boundary (the in-memory state written inside @spaces.GPU is invisible to79 the parent Gradio process).80 """81 from .workspace import CURRENT82 # Order: most processed โ least processed83 candidates = [84 CURRENT / "rigged.glb",85 CURRENT / "scaled.glb",86 CURRENT / "pivoted.glb",87 CURRENT / "lods" / "LOD0.glb",88 CURRENT / "unwrapped.glb",89 CURRENT / "low_poly.glb",90 CURRENT / "cleaned.glb",91 CURRENT / "repaired.glb",92 CURRENT / "raw_gen.glb",93 CURRENT / "high_poly.glb",94 ]95 for path in candidates:96 if path.exists():97 return str(path)98 return None99 100 101def quota_warning(operation: str) -> str:102 """Generate a warning if an operation would exceed the daily quota.103 104 Returns an empty string if the operation fits comfortably.105 """106 estimated = quota.estimate(operation)107 state = quota.get_state()108 remaining = state.remaining_seconds()109 110 if estimated > remaining:111 overage = estimated - remaining112 cost = overage * quota.OVERAGE_RATE_PER_SECOND113 return (114 f"โ ๏ธ **Quota warning:** This will use ~{estimated}s but you only "115 f"have {remaining:.0f}s left today. "116 f"Overage cost: ~${cost:.2f}"117 )118 elif estimated > remaining * 0.5:119 return (120 f"โน๏ธ Estimated GPU time: ~{estimated}s "121 f"({remaining:.0f}s remaining today)"122 )123 return ""124 