File size: 18,159 Bytes
5ddd413
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0796c7e
 
 
5ddd413
 
 
 
 
 
 
0796c7e
 
 
 
 
 
5ddd413
 
 
 
0796c7e
 
 
 
 
 
 
5ddd413
 
 
0796c7e
5ddd413
0796c7e
5ddd413
0796c7e
 
 
 
 
 
 
 
 
 
 
5ddd413
 
 
 
 
 
 
 
 
0796c7e
 
 
 
 
 
 
5ddd413
c179ef4
fd34f8f
 
c179ef4
fd34f8f
5ddd413
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0de805c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5ddd413
c179ef4
0de805c
 
5ddd413
 
 
 
 
 
 
 
 
 
 
0796c7e
 
5ddd413
0de805c
 
 
 
 
 
 
0796c7e
 
0de805c
 
 
 
 
 
 
0796c7e
0de805c
 
 
 
08572f5
 
0de805c
 
 
 
5ddd413
0796c7e
 
5ddd413
 
 
 
 
0de805c
 
 
0796c7e
5ddd413
 
 
 
08572f5
 
5ddd413
 
 
 
 
0de805c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5ddd413
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fd34f8f
5ddd413
 
 
a0e2b41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
08572f5
 
a0e2b41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5ddd413
 
 
 
 
 
 
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
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
"""
Evoneural MVP - Local 3D Mesh + Skybox Generation
Run: streamlit run app.py
Open: http://localhost:8501
"""

import os
import sys
from pathlib import Path

# Ensure project root is on path
ROOT = Path(__file__).resolve().parent
if str(ROOT) not in sys.path:
    sys.path.insert(0, str(ROOT))

import streamlit as st

OUTPUTS = ROOT / "outputs"
OUTPUTS.mkdir(exist_ok=True)

# Hugging Face Space: HF sets SPACE_ID when running in a Space
IS_HF_SPACE = bool(os.environ.get("SPACE_ID") or os.environ.get("SPACE_REPO_ID"))


def main() -> None:
    st.set_page_config(
        page_title="Evoneural MVP - Mesh & Skybox",
        page_icon="🎮",
        layout="wide",
    )
    if IS_HF_SPACE:
        st.title("EvoneuralIn3D – Mesh & Skybox")
        st.caption("Text → 3D mesh (TripoSR) and Text → 360° skybox (Stable Diffusion). Running on Hugging Face Space.")
    else:
        st.title("Evoneural MVP – Local Mesh & Skybox")
        st.caption("Text → 3D mesh (TripoSR) and Text → 360° skybox (Stable Diffusion). Runs on localhost.")

    # Sidebar: model setup (token + download)
    with st.sidebar:
        st.subheader("Stable Diffusion model")
        hf_token_env = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
        if IS_HF_SPACE:
            if hf_token_env:
                st.success("HF_TOKEN is set (from Space secrets)")
            else:
                st.error("HF_TOKEN not set")
                st.caption("Add it in this Space: **Settings** → **Variables and secrets** → New secret: `HF_TOKEN`. Then restart the Space.")
        from scripts.skybox_generator import _default_local_weights_dir
        local_model = _default_local_weights_dir()
        if local_model:
            st.success("Local model: found")
            st.caption(os.path.basename(local_model))
        elif not IS_HF_SPACE:
            st.warning("No local model. Download below or need internet on first generate.")
        if not IS_HF_SPACE:
            hf_token = st.text_input(
                "Hugging Face token (optional, if behind firewall)",
                type="password",
                key="hf_token",
                placeholder="hf_...",
                help="Get a token at huggingface.co/settings/tokens",
            )
            if hf_token:
                os.environ["HF_TOKEN"] = hf_token
        if not IS_HF_SPACE and st.button("Download model (~4GB to ./weights/sd-v1-5)", key="btn_download"):
            with st.spinner("Downloading model... (may take several minutes)"):
                try:
                    from scripts.download_sd_model import download_sd_model
                    path = download_sd_model(token=hf_token or os.environ.get("HF_TOKEN"))
                    st.success(f"Model saved. Try generating a skybox.")
                    st.rerun()
                except Exception as e:
                    st.error(str(e))
                    st.caption("Set a Hugging Face token above if your network blocks Hugging Face.")
        # Environment check: TripoSR (useful in Space)
        from scripts.mesh_generator import find_triposr_root as _find_triposr
        triposr_ok = _find_triposr(str(ROOT)) is not None
        if triposr_ok:
            st.caption("TripoSR: ready")
        else:
            st.caption("TripoSR: not found (mesh tab will show instructions)")

    # In Space, Skybox and mesh (text→mesh and image→mesh) need Hub access: SD and TripoSR download models. Disable if no token to avoid 403.
    can_use_hub = bool(hf_token_env) or not IS_HF_SPACE
    if IS_HF_SPACE and not hf_token_env:
        st.warning("Set **HF_TOKEN** in Settings → Variables and secrets to enable Skybox and mesh generation (TripoSR also downloads its model from the Hub).")

    tab_mesh, tab_skybox = st.tabs(["🟦 Text → 3D Mesh", "🌅 Text → Skybox"])

    with tab_mesh:
        st.subheader("Generate 3D mesh from text")
        st.markdown(
            "Uses **Stable Diffusion** for text→image, then **TripoSR** for image→mesh. "
            "TripoSR repo must be cloned into `./TripoSR` (see README)."
        )
        prompt_mesh = st.text_input(
            "Prompt (e.g. for mesh)",
            value="A highly detailed, sci-fi mechanical drone with glowing blue accents.",
            key="mesh_prompt",
        )
        col1, col2 = st.columns(2)
        with col1:
            mesh_format = st.selectbox("Mesh format", ["glb", "obj"], key="mesh_fmt")
            seed_mesh = st.number_input("Seed (optional)", value=42, min_value=0, key="mesh_seed")
        with col2:
            use_image = st.checkbox("Use image (from outputs or upload)", value=False, key="use_img")

        with st.expander("Quality options (TripoSR)", expanded=True):
            mc_resolution = st.selectbox(
                "Mesh resolution",
                options=[256, 512],
                index=1,
                format_func=lambda x: f"{x} (faster)" if x == 256 else f"{x} (higher quality)",
                key="mesh_mc_res",
                help="Marching cubes grid. 512 gives finer, less blocky meshes.",
            )
            bake_texture = st.checkbox(
                "Bake texture atlas",
                value=True,
                key="mesh_bake_tex",
                help="Produces a texture map instead of vertex colors; usually looks cleaner.",
            )
            smooth_mesh = st.checkbox(
                "Smooth mesh",
                value=True,
                key="mesh_smooth",
                help="Light Laplacian smoothing to reduce blockiness.",
            )
            output_images = sorted(Path(OUTPUTS).glob("*.png"), key=lambda p: p.stat().st_mtime, reverse=True)
            output_images += sorted(Path(OUTPUTS).glob("*.jpg"), key=lambda p: p.stat().st_mtime, reverse=True)
            selected_from_outputs = None
            if use_image and output_images:
                opt_names = [f.name for f in output_images]
                k = "mesh_pick_output_img"
                if k in st.session_state and st.session_state[k] not in opt_names:
                    del st.session_state[k]
                pick = st.selectbox("Pick from outputs (e.g. previous mesh input)", ["(upload below)"] + opt_names, key=k)
                if pick and pick != "(upload below)":
                    selected_from_outputs = Path(OUTPUTS) / pick
            uploaded = st.file_uploader("Or upload image for mesh", type=["png", "jpg"], key="mesh_upload") if use_image else None

        image_path_to_use = None
        if use_image and (selected_from_outputs and selected_from_outputs.exists() or uploaded):
            image_path_to_use = str(selected_from_outputs) if (selected_from_outputs and selected_from_outputs.exists()) else "upload"

        if st.button("Generate mesh", key="btn_mesh", disabled=not can_use_hub):
            if not image_path_to_use and not prompt_mesh.strip():
                st.warning("Enter a prompt or choose/upload an image.")
            else:
                with st.spinner("Running pipeline..."):
                    try:
                        from scripts.mesh_generator import (
                            generate_mesh_from_image,
                            generate_mesh_from_text,
                            find_triposr_root,
                        )
                        triposr_root = find_triposr_root(str(ROOT))
                        if not triposr_root:
                            st.error(
                                "TripoSR not found. In this Space the Docker image should include it. "
                                "If you see this, rebuild the Space or check the Dockerfile."
                            )
                        elif image_path_to_use:
                            path = image_path_to_use
                            if path == "upload" and uploaded:
                                path = os.path.join(OUTPUTS, "uploaded_mesh_input.png")
                                with open(path, "wb") as f:
                                    f.write(uploaded.getvalue())
                            if path != "upload" and os.path.isfile(path):
                                import torch as _torch
                                _dev = "cuda:0" if _torch.cuda.is_available() else "cpu"
                                mesh_path, elapsed, msg = generate_mesh_from_image(
                                    path,
                                    output_dir=str(OUTPUTS / "mesh_run"),
                                    mesh_format=mesh_format,
                                    mc_resolution=mc_resolution,
                                    bake_texture=bake_texture,
                                    smooth_mesh=smooth_mesh,
                                    device=_dev,
                                )
                                if mesh_path:
                                    st.success(f"Done in {elapsed:.1f}s. {msg}")
                                    with open(mesh_path, "rb") as f:
                                        mesh_data = f.read()
                                    st.download_button("Download mesh", data=mesh_data, file_name=os.path.basename(mesh_path), key="dl_mesh_upload")
                                else:
                                    st.error(msg)
                            elif path == "upload":
                                st.warning("Upload an image first.")
                        else:
                            import torch as _torch
                            _dev = "cuda:0" if _torch.cuda.is_available() else "cpu"
                            mesh_path, elapsed, msg = generate_mesh_from_text(
                                prompt_mesh,
                                output_dir=str(OUTPUTS),
                                mesh_format=mesh_format,
                                seed=seed_mesh,
                                mc_resolution=mc_resolution,
                                bake_texture=bake_texture,
                                smooth_mesh=smooth_mesh,
                                device=_dev,
                            )
                            if mesh_path:
                                st.success(f"Done in {elapsed:.1f}s. {msg}")
                                with open(mesh_path, "rb") as f:
                                    mesh_data = f.read()
                                st.download_button("Download mesh", data=mesh_data, file_name=os.path.basename(mesh_path), key="dl_mesh")
                            else:
                                st.error(msg)
                    except Exception as e:
                        st.exception(e)

        # View 3D mesh (GLB): path, upload, or pick from outputs
        with st.expander("View 3D mesh", expanded=False):
            st.caption("Open a .glb file by path, upload, or pick from outputs. Drag to rotate, scroll to zoom.")
            from scripts.mesh_viewer import mesh_viewer_html
            import streamlit.components.v1 as components

            viewer_glb_path: str | None = None
            viewer_glb_bytes: bytes | None = None

            path_input = st.text_input(
                "Path to .glb file",
                value="",
                key="mesh_viewer_path",
                placeholder=r"e.g. C:\Users\...\Downloads\mesh (1).glb",
            )
            if path_input and Path(path_input.strip()).is_file():
                viewer_glb_path = path_input.strip()

            uploaded_glb = st.file_uploader("Or upload a .glb file", type=["glb"], key="mesh_viewer_upload")
            if uploaded_glb is not None:
                viewer_glb_bytes = uploaded_glb.getvalue()

            output_glbs = sorted(Path(OUTPUTS).rglob("*.glb"), key=lambda p: p.stat().st_mtime, reverse=True)
            if not viewer_glb_path and not viewer_glb_bytes and output_glbs:
                opt_names = [str(p.relative_to(OUTPUTS)) for p in output_glbs]
                k = "mesh_viewer_pick"
                if k in st.session_state and st.session_state[k] not in opt_names:
                    del st.session_state[k]
                picked = st.selectbox("Or pick from outputs", ["(none)"] + opt_names, key=k)
                if picked and picked != "(none)":
                    viewer_glb_path = str(OUTPUTS / picked)

            if viewer_glb_path or viewer_glb_bytes:
                html = mesh_viewer_html(glb_path=viewer_glb_path, glb_bytes=viewer_glb_bytes, height_px=480)
                components.html(html, height=500, scrolling=False)
            else:
                st.info("Enter a path to a .glb file, upload one, or generate a mesh above and pick it from outputs.")

    with tab_skybox:
        st.subheader("Generate 2:1 equirectangular skybox")
        st.markdown(
            "Uses **Stable Diffusion 2.1** at 2:1 aspect (e.g. 1024×512). "
            "Optional seamless check compares left/right edges."
        )
        prompt_sky = st.text_input(
            "Prompt (e.g. for skybox)",
            value="Cyberpunk city skyline at dusk, neon reflections, cinematic lighting.",
            key="sky_prompt",
        )
        col1, col2 = st.columns(2)
        with col1:
            width = st.selectbox("Width", [1024, 2048], key="sky_w")
            height = width // 2
            seed_sky = st.number_input("Seed (optional)", value=42, min_value=0, key="sky_seed")
        with col2:
            check_seamless = st.checkbox("Run seamless edge check", value=True, key="seamless")

        if st.button("Generate skybox", key="btn_sky", disabled=not can_use_hub):
            if not prompt_sky.strip():
                st.warning("Enter a prompt.")
            else:
                try:
                    from scripts.skybox_generator import generate_skybox
                    from scripts.check_seamless import check_seamless as run_seamless

                    progress_placeholder = st.empty()
                    status_placeholder = st.empty()
                    progress_placeholder.progress(0)
                    status_placeholder.caption("Loading model and starting generation…")

                    def on_step(step: int, total: int) -> None:
                        progress = min(step / total, 1.0)
                        progress_placeholder.progress(progress)
                        status_placeholder.caption(f"Step {min(step, total)} / {total}")

                    out_path, elapsed, vram_mb = generate_skybox(
                        prompt_sky,
                        output_dir=str(OUTPUTS),
                        width=width,
                        height=height,
                        seed=seed_sky,
                        progress_callback=on_step,
                    )
                    progress_placeholder.progress(1.0)
                    status_placeholder.caption("Done.")

                    st.success(f"Done in {elapsed:.1f}s. Peak VRAM: {vram_mb:.0f} MB")
                    st.image(out_path, use_container_width=True)
                    with open(out_path, "rb") as f:
                        skybox_data = f.read()
                    st.download_button("Download skybox", data=skybox_data, file_name=os.path.basename(out_path), key="dl_sky")

                    if check_seamless:
                        result = run_seamless(out_path)
                        st.info(result["message"])

                    st.session_state["last_skybox_path"] = str(Path(out_path).resolve())
                except Exception as e:
                    st.exception(e)

        # Show 360° viewer for last generated skybox (same session)
        if "last_skybox_path" in st.session_state:
            last_path = Path(st.session_state["last_skybox_path"]).resolve()
            if last_path.exists():
                with st.expander("View in 360°", expanded=False):
                    st.caption("Drag to look around, scroll to zoom. Fullscreen available in the viewer.")
                    from scripts.panorama_viewer import panorama_html
                    import streamlit.components.v1 as components
                    components.html(panorama_html(last_path, height_px=480), height=500, scrolling=False)

        # View existing image from outputs (or upload) in 360° – test without regenerating
        with st.expander("View existing image in 360°", expanded=False):
            st.caption("Pick an image from outputs or upload a 2:1 equirectangular image to test the viewer.")
            from scripts.panorama_viewer import panorama_html
            import streamlit.components.v1 as components

            output_files = sorted(Path(OUTPUTS).glob("*.png"), key=lambda p: p.stat().st_mtime, reverse=True)
            viewer_path = None
            option_names = [f.name for f in output_files]

            if output_files:
                key = "skybox_select_existing"
                if key in st.session_state and st.session_state[key] not in option_names:
                    del st.session_state[key]
                selected_name = st.selectbox(
                    "Choose image from outputs",
                    options=option_names,
                    key=key,
                )
                if selected_name:
                    viewer_path = Path(OUTPUTS) / selected_name

            uploaded = st.file_uploader("Or upload a 2:1 equirectangular image", type=["png", "jpg", "jpeg"], key="skybox_upload_360")
            if uploaded is not None:
                upload_path = OUTPUTS / "uploaded_360_view.png"
                upload_path.write_bytes(uploaded.getvalue())
                viewer_path = upload_path

            if viewer_path is not None and viewer_path.exists():
                components.html(panorama_html(Path(viewer_path).resolve(), height_px=480), height=500, scrolling=False)
            elif not output_files and uploaded is None:
                st.info("No skybox images in outputs yet. Generate one above or upload an image.")

    st.divider()
    st.caption("Evoneural AI – Local ML Deployment MVP. Models run locally (no API).")


if __name__ == "__main__":
    main()