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"""
{TAGLINE}
{SUBTITLE}