from __future__ import annotations import gradio as gr from ai_runtime import MODEL_OMNI, MODEL_TEXT3D, iter_hunyuan_blueprint_session, finalize_ai_mesh_session from fallback_generator import iter_blueprint_session as iter_fallback_blueprint from fallback_generator import iter_meshify_session as iter_fallback_mesh from viewer import empty_viewer_html, load_points_from_cloud_file, point_cloud_viewer_html TITLE = "Particle Blueprint 3D" TAGLINE = "Prompt → AI blueprint → inspect on iPhone → export mesh" SUBTITLE = ( "Rebuilt around Hugging Face-hosted Tencent Hunyuan 3D models. " "The first stage uses Hunyuan3D-1 for prompt-driven 3D generation, then converts that AI output into a particle blueprint for review." ) PROMPTS = { "Cargo hauler": "small cargo hauler with a boxy hull, rear ramp, cargo bay, 4 engines and landing gear", "Compact fighter": "compact fighter with a sleek hull, twin engines, short wings and a small cockpit", "Industrial shuttle": "industrial shuttle with a rounded hull, cargo hold, fin tail and landing gear", } CSS = """ footer {display:none !important} .gradio-container {max-width: 1080px !important; margin: 0 auto; padding: 0 12px 28px !important} .hero, .panel, .status-card {border:1px solid rgba(255,255,255,.08); border-radius:22px; background:rgba(255,255,255,.03); padding:14px 16px} .hero-title {font-size:1.5rem; font-weight:800; margin:0 0 6px 0} .hero-sub {opacity:.88; margin:0} .flow {display:grid; grid-template-columns:repeat(4, minmax(0,1fr)); gap:10px; margin-top:10px} .flow div {border:1px solid rgba(255,255,255,.08); border-radius:16px; padding:12px; background:rgba(255,255,255,.02)} .flow strong {display:block; margin-bottom:4px} .cta-row button {min-height:52px !important; border-radius:16px !important; font-size:1rem !important} .preset-row button {min-height:44px !important; border-radius:14px !important} #status-box p {margin:0} .small-note {font-size:.88rem; opacity:.8} @media (max-width: 820px) { .gradio-container {padding:0 10px 22px !important} .hero,.panel,.status-card {padding:12px} .flow {grid-template-columns:1fr} .cta-row {position:sticky; bottom:10px; z-index:12; background:rgba(10,13,22,.92); backdrop-filter:blur(12px); padding:8px; border:1px solid rgba(255,255,255,.08); border-radius:18px} } """ def _status_md(text: str) -> str: return f"**Status**\n\n{text}" def _blueprint_html_from_ply(path: str | None) -> str: if not path: return empty_viewer_html() try: points = load_points_from_cloud_file(path) return point_cloud_viewer_html(points, status=f"Blueprint • {len(points)} points") except Exception as exc: return empty_viewer_html(f"Blueprint file exists but the viewer could not read it: {exc}") def stream_blueprint(prompt: str, mode: str, save_memory: bool, max_faces: int, detail: int): prompt = (prompt or "").strip() if not prompt: raise gr.Error("Enter a prompt first.") html = empty_viewer_html("Starting…") summary = None state = None blueprint_file = None mesh_preview = None yield ( _status_md("Starting blueprint generation…"), html, None, None, None, gr.update(interactive=False), None, ) if mode == "hunyuan": try: for update in iter_hunyuan_blueprint_session( prompt=prompt, save_memory=save_memory, max_faces_num=int(max_faces), preview_points=max(2200, int(detail) * 140), ): html = update.get("viewer_html", html) summary = update.get("summary", summary) blueprint_file = update.get("blueprint_path", blueprint_file) mesh_preview = update.get("mesh_preview", mesh_preview) state = update.get("state", state) yield ( _status_md(update.get("status", "Working…")), html, summary, blueprint_file, state, gr.update(interactive=bool(state)), mesh_preview, ) return except Exception as exc: fallback_notice = f"AI model path failed, so the app is falling back to the local scaffold builder. Reason: {exc}" yield ( _status_md(fallback_notice), html, {"fallback_reason": str(exc), "requested_mode": mode, "prompt": prompt}, blueprint_file, state, gr.update(interactive=False), mesh_preview, ) for update in iter_fallback_blueprint(prompt=prompt, detail=detail, parser_mode="heuristic"): blueprint_path = update.get("blueprint_path") if blueprint_path: html = _blueprint_html_from_ply(blueprint_path) blueprint_file = blueprint_path summary = update.get("summary", summary) state = update.get("state", state) yield ( _status_md(update.get("status", "Working…")), html, summary, blueprint_file, state, gr.update(interactive=bool(state)), mesh_preview, ) def stream_mesh(state: dict, prepare_omni: bool, voxel_pitch: float): if not state: raise gr.Error("Generate a blueprint first.") yield _status_md("Starting mesh generation…"), None, None, None if state.get("raw_ai_mesh_path"): for update in finalize_ai_mesh_session(state, prepare_omni=prepare_omni): yield ( _status_md(update.get("status", "Meshing…")), update.get("mesh_path"), update.get("mesh_file"), update.get("summary"), ) return for update in iter_fallback_mesh(state=state, voxel_pitch=voxel_pitch, use_target_model_cache=prepare_omni): yield ( _status_md(update.get("status", "Meshing…")), update.get("mesh_path"), update.get("mesh_file"), update.get("summary"), ) with gr.Blocks(title=TITLE, fill_width=True) as demo: session_state = gr.State(value=None) gr.HTML( f"""
{TITLE}

{TAGLINE}
{SUBTITLE}

1. DescribeWrite what you want.
2. Generate blueprint{MODEL_TEXT3D} runs, then its AI mesh is turned into a particle blueprint.
3. InspectRotate, zoom and pan the blueprint on iPhone before committing.
4. Make meshExport the AI mesh as GLB, with {MODEL_OMNI} preloaded for later refinement work.
""" ) with gr.Column(elem_classes=["panel"]): prompt = gr.Textbox( label="Describe the model", lines=4, max_lines=7, placeholder="Example: small cargo hauler with a boxy hull, cargo bay, rear ramp, 4 engines and landing gear", ) with gr.Row(elem_classes=["preset-row"]): p1 = gr.Button("Cargo hauler") p2 = gr.Button("Compact fighter") p3 = gr.Button("Industrial shuttle") with gr.Accordion("Generation settings", open=False): mode = gr.Radio( choices=[("Hunyuan3D-1 AI", "hunyuan"), ("Local fallback scaffold", "fallback")], value="hunyuan", label="Blueprint generation mode", info="Use Hunyuan first. If its repo dependencies are not fully ready in the Space yet, the app will fall back automatically.", ) save_memory = gr.Checkbox( value=True, label="Use save-memory mode for Hunyuan3D-1", info="Useful on tighter ZeroGPU runs.", ) max_faces = gr.Slider(20000, 90000, value=70000, step=5000, label="Max faces for the AI mesh") detail = gr.Slider(14, 34, value=22, step=2, label="Blueprint preview density") voxel_pitch = gr.Slider(0.055, 0.12, value=0.085, step=0.005, label="Fallback mesh density") prepare_omni = gr.Checkbox( value=True, label=f"Preload {MODEL_OMNI} during mesh step", info="This caches the controllable refinement model in the Space even though this build still exports the Hunyuan3D-1 mesh on step two.", ) with gr.Row(elem_classes=["cta-row"]): blueprint_btn = gr.Button("Generate blueprint", variant="primary") mesh_btn = gr.Button("Make mesh", interactive=False) clear_btn = gr.Button("Clear") with gr.Column(elem_classes=["status-card"]): status = gr.Markdown(_status_md("Ready."), elem_id="status-box") gr.Markdown("On iPhone: one finger orbits. Two fingers pan and zoom.") with gr.Tabs(): with gr.TabItem("Blueprint"): blueprint_view = gr.HTML(value=empty_viewer_html(), label="Blueprint viewer") with gr.TabItem("Mesh"): mesh_view = gr.Model3D( label="Mesh preview (.glb)", display_mode="solid", clear_color=(0.02, 0.02, 0.03, 1.0), camera_position=(35, 65, 6), zoom_speed=1.15, pan_speed=0.95, height=560, ) with gr.TabItem("Summary and files"): summary = gr.JSON(label="Session summary") blueprint_file = gr.File(label="Particle blueprint (.ply)") mesh_file = gr.File(label="Mesh export (.glb)") p1.click(lambda: PROMPTS["Cargo hauler"], outputs=prompt) p2.click(lambda: PROMPTS["Compact fighter"], outputs=prompt) p3.click(lambda: PROMPTS["Industrial shuttle"], outputs=prompt) blueprint_btn.click( fn=stream_blueprint, inputs=[prompt, mode, save_memory, max_faces, detail], outputs=[status, blueprint_view, summary, blueprint_file, session_state, mesh_btn, mesh_view], ) mesh_btn.click( fn=stream_mesh, inputs=[session_state, prepare_omni, voxel_pitch], outputs=[status, mesh_view, mesh_file, summary], ) clear_btn.click( lambda: ( "", _status_md("Ready."), empty_viewer_html(), None, None, None, None, gr.update(interactive=False), None, ), outputs=[prompt, status, blueprint_view, mesh_view, summary, blueprint_file, mesh_file, mesh_btn, session_state], ) if __name__ == "__main__": demo.queue(default_concurrency_limit=1).launch(theme=gr.themes.Soft(), css=CSS)