File size: 11,055 Bytes
0d2d373
 
 
 
77e37fc
 
 
 
0d2d373
 
77e37fc
 
 
 
0d2d373
 
77e37fc
 
 
 
0d2d373
 
 
 
77e37fc
 
 
 
 
 
 
 
 
52fd31f
77e37fc
0d2d373
77e37fc
 
 
 
0d2d373
 
 
 
52fd31f
 
 
 
77e37fc
 
 
 
 
 
 
 
 
 
 
0d2d373
 
 
 
77e37fc
 
 
 
 
52fd31f
 
 
77e37fc
52fd31f
 
 
 
77e37fc
52fd31f
 
77e37fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52fd31f
77e37fc
 
 
 
 
 
 
52fd31f
 
 
77e37fc
52fd31f
 
 
 
 
77e37fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0d2d373
 
af54811
52fd31f
 
77e37fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0d2d373
 
77e37fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0d2d373
 
77e37fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52fd31f
77e37fc
 
 
 
 
0d2d373
77e37fc
 
 
 
 
 
 
 
 
 
 
 
 
 
0d2d373
 
 
af54811
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
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"""
        <div class='hero'>
          <div class='hero-title'>{TITLE}</div>
          <p class='hero-sub'><strong>{TAGLINE}</strong><br>{SUBTITLE}</p>
          <div class='flow'>
            <div><strong>1. Describe</strong>Write what you want.</div>
            <div><strong>2. Generate blueprint</strong>{MODEL_TEXT3D} runs, then its AI mesh is turned into a particle blueprint.</div>
            <div><strong>3. Inspect</strong>Rotate, zoom and pan the blueprint on iPhone before committing.</div>
            <div><strong>4. Make mesh</strong>Export the AI mesh as GLB, with {MODEL_OMNI} preloaded for later refinement work.</div>
          </div>
        </div>
        """
    )

    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("<span class='small-note'>On iPhone: one finger orbits. Two fingers pan and zoom.</span>")

    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)