Spaces:
Runtime error
Runtime error
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()
|