Upload folder using huggingface_hub
Browse files- .gitignore +34 -0
- .huggingfaceignore +40 -0
- README.md +155 -0
- app.py +181 -0
- environment.yml +25 -0
- requirements.txt +33 -0
- scripts/__init__.py +1 -0
- scripts/check_seamless.py +36 -0
- scripts/download_sd_model.py +46 -0
- scripts/mesh_generator.py +123 -0
- scripts/skybox_generator.py +149 -0
- scripts/text_to_image.py +80 -0
- scripts/upload_to_hf.py +54 -0
.gitignore
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
.venv/
|
| 3 |
+
venv/
|
| 4 |
+
env/
|
| 5 |
+
__pycache__/
|
| 6 |
+
*.py[cod]
|
| 7 |
+
*.egg-info/
|
| 8 |
+
.eggs/
|
| 9 |
+
dist/
|
| 10 |
+
build/
|
| 11 |
+
|
| 12 |
+
# Outputs and weights
|
| 13 |
+
outputs/
|
| 14 |
+
weights/
|
| 15 |
+
*.obj
|
| 16 |
+
*.glb
|
| 17 |
+
*.png
|
| 18 |
+
!scripts/**/*.png
|
| 19 |
+
TripoSR/
|
| 20 |
+
*.ckpt
|
| 21 |
+
*.safetensors
|
| 22 |
+
|
| 23 |
+
# IDE
|
| 24 |
+
.idea/
|
| 25 |
+
.vscode/
|
| 26 |
+
*.swp
|
| 27 |
+
*.swo
|
| 28 |
+
|
| 29 |
+
# Streamlit
|
| 30 |
+
.streamlit/
|
| 31 |
+
|
| 32 |
+
# Logs
|
| 33 |
+
*.log
|
| 34 |
+
performance_log.txt
|
.huggingfaceignore
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Exclude from Hugging Face upload (hf upload)
|
| 2 |
+
# Only code and config are uploaded; weights/outputs stay local.
|
| 3 |
+
|
| 4 |
+
# Python
|
| 5 |
+
.venv/
|
| 6 |
+
venv/
|
| 7 |
+
env/
|
| 8 |
+
__pycache__/
|
| 9 |
+
*.py[cod]
|
| 10 |
+
*.egg-info/
|
| 11 |
+
.eggs/
|
| 12 |
+
dist/
|
| 13 |
+
build/
|
| 14 |
+
|
| 15 |
+
# Outputs and model weights (large; users download separately)
|
| 16 |
+
outputs/
|
| 17 |
+
weights/
|
| 18 |
+
*.obj
|
| 19 |
+
*.glb
|
| 20 |
+
*.png
|
| 21 |
+
TripoSR/
|
| 22 |
+
*.ckpt
|
| 23 |
+
*.safetensors
|
| 24 |
+
*.bin
|
| 25 |
+
|
| 26 |
+
# IDE and editor
|
| 27 |
+
.idea/
|
| 28 |
+
.vscode/
|
| 29 |
+
*.swp
|
| 30 |
+
*.swo
|
| 31 |
+
|
| 32 |
+
# Streamlit
|
| 33 |
+
.streamlit/
|
| 34 |
+
|
| 35 |
+
# Logs
|
| 36 |
+
*.log
|
| 37 |
+
performance_log.txt
|
| 38 |
+
|
| 39 |
+
# Git (keep .gitignore in repo)
|
| 40 |
+
.git/
|
README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# selfhostedmodels
|
| 2 |
+
|
| 3 |
+
# Evoneural MVP – Local Mesh & Skybox
|
| 4 |
+
|
| 5 |
+
Localhost MVP for **text → 3D mesh** and **text → 360° skybox** using local models (no hosted APIs).
|
| 6 |
+
|
| 7 |
+
- **Mesh**: Text → image (Stable Diffusion) → 3D mesh (TripoSR). Output: `.obj` or `.glb`.
|
| 8 |
+
- **Skybox**: Text → 2:1 equirectangular image (Stable Diffusion). Optional seamless edge check.
|
| 9 |
+
|
| 10 |
+
**Default model:** `runwayml/stable-diffusion-v1-5` (no Hugging Face login required; first run downloads ~4GB).
|
| 11 |
+
|
| 12 |
+
## Prerequisites
|
| 13 |
+
|
| 14 |
+
- **Python 3.10** (recommended) — [python.org](https://www.python.org/downloads/)
|
| 15 |
+
- **NVIDIA GPU** with CUDA (recommended; CPU is slower)
|
| 16 |
+
- **Git** (for cloning TripoSR; mesh only)
|
| 17 |
+
|
| 18 |
+
**No Conda?** Use **venv** (built into Python) — steps below.
|
| 19 |
+
|
| 20 |
+
## 1. Environment
|
| 21 |
+
|
| 22 |
+
### Option A: venv + pip (no Conda)
|
| 23 |
+
|
| 24 |
+
From PowerShell (project folder is `evoneural`):
|
| 25 |
+
|
| 26 |
+
```powershell
|
| 27 |
+
cd D:\project\evoneural
|
| 28 |
+
python -m venv .venv
|
| 29 |
+
.venv\Scripts\Activate.ps1
|
| 30 |
+
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118
|
| 31 |
+
pip install -r requirements.txt
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
- **CPU only:** use `pip install torch torchvision` (no `--index-url`).
|
| 35 |
+
- **CUDA 12.x:** use `cu121` instead of `cu118`.
|
| 36 |
+
|
| 37 |
+
### Option B: Conda
|
| 38 |
+
|
| 39 |
+
```powershell
|
| 40 |
+
cd D:\project\evoneural
|
| 41 |
+
conda env create -f environment.yml
|
| 42 |
+
conda activate evoneural-mvp
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
If you use CPU-only or a different CUDA version, edit `environment.yml` (e.g. remove `pytorch-cuda=11.8` or set `pytorch-cuda=12.1`).
|
| 46 |
+
|
| 47 |
+
## 2. TripoSR (for mesh)
|
| 48 |
+
|
| 49 |
+
Mesh generation needs the TripoSR repo and its dependencies.
|
| 50 |
+
|
| 51 |
+
```powershell
|
| 52 |
+
cd D:\project\evoneural
|
| 53 |
+
git clone https://github.com/VAST-AI-Research/TripoSR.git TripoSR
|
| 54 |
+
pip install -r TripoSR/requirements.txt
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
On Windows, if `torchmcubes` fails, see [TripoSR README](https://github.com/VAST-AI-Research/TripoSR#troubleshooting) (CUDA version match, then reinstall torchmcubes).
|
| 58 |
+
|
| 59 |
+
## 2b. Stable Diffusion model (Hugging Face)
|
| 60 |
+
|
| 61 |
+
If you see **"Cannot load model ... model is not cached locally and an error occurred while trying to fetch metadata"**, the app cannot reach Hugging Face. Use one of these:
|
| 62 |
+
|
| 63 |
+
**Option 1 – Log in (uses cached token)**
|
| 64 |
+
From a terminal with internet:
|
| 65 |
+
|
| 66 |
+
```powershell
|
| 67 |
+
huggingface-cli login
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
Paste a token from [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens) (read access is enough). Then run the app again.
|
| 71 |
+
|
| 72 |
+
**Option 2 – Set token in env**
|
| 73 |
+
Create a token at [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens), then:
|
| 74 |
+
|
| 75 |
+
```powershell
|
| 76 |
+
$env:HF_TOKEN = "hf_xxxxxxxx"
|
| 77 |
+
streamlit run app.py
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
**Option 3 – Download model once, then use offline**
|
| 81 |
+
On a machine that can reach Hugging Face:
|
| 82 |
+
|
| 83 |
+
```powershell
|
| 84 |
+
cd D:\project\evoneural
|
| 85 |
+
.venv\Scripts\Activate.ps1
|
| 86 |
+
python -m scripts.download_sd_model
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
Then set the path and run the app (no Hugging Face needed):
|
| 90 |
+
|
| 91 |
+
```powershell
|
| 92 |
+
$env:SD_MODEL_PATH = "D:\project\evoneural\weights\stable-diffusion-2-1-base"
|
| 93 |
+
streamlit run app.py
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
## How it works
|
| 97 |
+
|
| 98 |
+
1. **Skybox tab:** You enter a text prompt → the app loads Stable Diffusion (from cache or Hugging Face) → generates a 2:1 image → saves to `outputs/` and shows a download button. Optional “seamless” check compares left/right edges.
|
| 99 |
+
2. **Mesh tab:** You enter a prompt (or upload an image) → the app generates an image with SD (if needed) → runs TripoSR on that image → outputs a `.obj` or `.glb` to `outputs/` (requires TripoSR repo cloned in `./TripoSR`).
|
| 100 |
+
3. **Model loading:** The app first tries a local folder (`SD_MODEL_PATH` or `weights/stable-diffusion-2-1-base` if complete). If none, it loads `runwayml/stable-diffusion-v1-5` from the Hub (first run downloads the model; later runs use the cache). No token needed unless your network restricts Hugging Face.
|
| 101 |
+
|
| 102 |
+
## 3. Run the app
|
| 103 |
+
|
| 104 |
+
From the project root (with venv activated):
|
| 105 |
+
|
| 106 |
+
```powershell
|
| 107 |
+
cd D:\project\evoneural
|
| 108 |
+
.venv\Scripts\Activate.ps1
|
| 109 |
+
streamlit run app.py
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
Open **http://localhost:8501**.
|
| 113 |
+
|
| 114 |
+
- **Text → 3D Mesh**: Enter a prompt (or upload an image). First run downloads SD 2.1 and TripoSR weights.
|
| 115 |
+
- **Text → Skybox**: Enter a prompt; image is 2:1 (e.g. 1024×512). Use “Run seamless edge check” to compare left/right edges.
|
| 116 |
+
|
| 117 |
+
Outputs are under `outputs/`. Use the download buttons to save mesh (`.glb`/`.obj`) and skybox (`.png`).
|
| 118 |
+
|
| 119 |
+
## 4. Performance
|
| 120 |
+
|
| 121 |
+
- **Skybox**: ~6–8 GB VRAM (SD 2.1, 1024×512, FP16). Use 2048×1024 only if you have enough VRAM.
|
| 122 |
+
- **Mesh**: ~6 GB for TripoSR + ~6 GB for SD (text-to-image). Total peak can be ~10–12 GB if both run in same process.
|
| 123 |
+
|
| 124 |
+
If you run out of VRAM:
|
| 125 |
+
|
| 126 |
+
- Use 1024×512 for skybox.
|
| 127 |
+
- Close other GPU apps.
|
| 128 |
+
- Consider quantization (e.g. 8-bit) or CPU offload in diffusers (see [Optimization](#optimization)).
|
| 129 |
+
|
| 130 |
+
## 5. Optimization (if VRAM is exceeded)
|
| 131 |
+
|
| 132 |
+
- **Quantization**: Use `load_in_8bit=True` or `load_in_4bit=True` with `bitsandbytes` where supported in diffusers.
|
| 133 |
+
- **Model CPU offload**: In diffusers, `pipe.enable_sequential_cpu_offload()` or `pipe.enable_model_cpu_offload()` to move parts to CPU and reduce peak VRAM (slower).
|
| 134 |
+
- **Smaller resolution**: 512×512 for text-to-image; 1024×512 for skybox.
|
| 135 |
+
|
| 136 |
+
## Project layout
|
| 137 |
+
|
| 138 |
+
```
|
| 139 |
+
evoneural/
|
| 140 |
+
├── README.md
|
| 141 |
+
├── app.py # Streamlit UI
|
| 142 |
+
├── requirements.txt
|
| 143 |
+
├── environment.yml
|
| 144 |
+
├── scripts/
|
| 145 |
+
│ ├── skybox_generator.py
|
| 146 |
+
│ ├── mesh_generator.py
|
| 147 |
+
│ ├── text_to_image.py
|
| 148 |
+
│ └── check_seamless.py
|
| 149 |
+
├── outputs/ # Generated meshes and skybox images
|
| 150 |
+
└── TripoSR/ # Clone here (see step 2)
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
+
## License
|
| 154 |
+
|
| 155 |
+
See TripoSR and Stable Diffusion model licenses (MIT / Stability). This MVP is for local use and evaluation.
|
app.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Evoneural MVP - Local 3D Mesh + Skybox Generation
|
| 3 |
+
Run: streamlit run app.py
|
| 4 |
+
Open: http://localhost:8501
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
# Ensure project root is on path
|
| 12 |
+
ROOT = Path(__file__).resolve().parent
|
| 13 |
+
if str(ROOT) not in sys.path:
|
| 14 |
+
sys.path.insert(0, str(ROOT))
|
| 15 |
+
|
| 16 |
+
import streamlit as st
|
| 17 |
+
|
| 18 |
+
OUTPUTS = ROOT / "outputs"
|
| 19 |
+
OUTPUTS.mkdir(exist_ok=True)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def main() -> None:
|
| 23 |
+
st.set_page_config(
|
| 24 |
+
page_title="Evoneural MVP - Mesh & Skybox",
|
| 25 |
+
page_icon="🎮",
|
| 26 |
+
layout="wide",
|
| 27 |
+
)
|
| 28 |
+
st.title("Evoneural MVP – Local Mesh & Skybox")
|
| 29 |
+
st.caption("Text → 3D mesh (TripoSR) and Text → 360° skybox (Stable Diffusion). Runs on localhost.")
|
| 30 |
+
|
| 31 |
+
# Sidebar: model setup (token + download)
|
| 32 |
+
with st.sidebar:
|
| 33 |
+
st.subheader("Stable Diffusion model")
|
| 34 |
+
from scripts.skybox_generator import _default_local_weights_dir
|
| 35 |
+
local_model = _default_local_weights_dir()
|
| 36 |
+
if local_model:
|
| 37 |
+
st.success(f"Local model: found")
|
| 38 |
+
st.caption(os.path.basename(local_model))
|
| 39 |
+
else:
|
| 40 |
+
st.warning("No local model. Download below or need internet on first generate.")
|
| 41 |
+
hf_token = st.text_input(
|
| 42 |
+
"Hugging Face token (optional, if behind firewall)",
|
| 43 |
+
type="password",
|
| 44 |
+
key="hf_token",
|
| 45 |
+
placeholder="hf_...",
|
| 46 |
+
help="Get a token at huggingface.co/settings/tokens",
|
| 47 |
+
)
|
| 48 |
+
if hf_token:
|
| 49 |
+
os.environ["HF_TOKEN"] = hf_token
|
| 50 |
+
if st.button("Download model (~4GB to ./weights/sd-v1-5)", key="btn_download"):
|
| 51 |
+
with st.spinner("Downloading model... (may take several minutes)"):
|
| 52 |
+
try:
|
| 53 |
+
from scripts.download_sd_model import download_sd_model
|
| 54 |
+
path = download_sd_model(token=hf_token or os.environ.get("HF_TOKEN"))
|
| 55 |
+
st.success(f"Model saved. Try generating a skybox.")
|
| 56 |
+
st.rerun()
|
| 57 |
+
except Exception as e:
|
| 58 |
+
st.error(str(e))
|
| 59 |
+
st.caption("Set a Hugging Face token above if your network blocks Hugging Face.")
|
| 60 |
+
|
| 61 |
+
tab_mesh, tab_skybox = st.tabs(["🟦 Text → 3D Mesh", "🌅 Text → Skybox"])
|
| 62 |
+
|
| 63 |
+
with tab_mesh:
|
| 64 |
+
st.subheader("Generate 3D mesh from text")
|
| 65 |
+
st.markdown(
|
| 66 |
+
"Uses **Stable Diffusion** for text→image, then **TripoSR** for image→mesh. "
|
| 67 |
+
"TripoSR repo must be cloned into `./TripoSR` (see README)."
|
| 68 |
+
)
|
| 69 |
+
prompt_mesh = st.text_input(
|
| 70 |
+
"Prompt (e.g. for mesh)",
|
| 71 |
+
value="A highly detailed, sci-fi mechanical drone with glowing blue accents.",
|
| 72 |
+
key="mesh_prompt",
|
| 73 |
+
)
|
| 74 |
+
col1, col2 = st.columns(2)
|
| 75 |
+
with col1:
|
| 76 |
+
mesh_format = st.selectbox("Mesh format", ["glb", "obj"], key="mesh_fmt")
|
| 77 |
+
seed_mesh = st.number_input("Seed (optional)", value=42, min_value=0, key="mesh_seed")
|
| 78 |
+
with col2:
|
| 79 |
+
use_image = st.checkbox("Use uploaded image instead of text", value=False, key="use_img")
|
| 80 |
+
uploaded = st.file_uploader("Upload image for mesh", type=["png", "jpg"], key="mesh_upload") if use_image else None
|
| 81 |
+
|
| 82 |
+
if st.button("Generate mesh", key="btn_mesh"):
|
| 83 |
+
if not prompt_mesh.strip() and not use_image:
|
| 84 |
+
st.warning("Enter a prompt or upload an image.")
|
| 85 |
+
else:
|
| 86 |
+
with st.spinner("Running pipeline..."):
|
| 87 |
+
try:
|
| 88 |
+
from scripts.mesh_generator import (
|
| 89 |
+
generate_mesh_from_image,
|
| 90 |
+
generate_mesh_from_text,
|
| 91 |
+
find_triposr_root,
|
| 92 |
+
)
|
| 93 |
+
triposr_root = find_triposr_root(str(ROOT))
|
| 94 |
+
if not triposr_root:
|
| 95 |
+
st.error(
|
| 96 |
+
"TripoSR not found. Clone it: "
|
| 97 |
+
"`git clone https://github.com/VAST-AI-Research/TripoSR.git TripoSR`"
|
| 98 |
+
)
|
| 99 |
+
elif use_image and uploaded:
|
| 100 |
+
path = os.path.join(OUTPUTS, "uploaded_mesh_input.png")
|
| 101 |
+
with open(path, "wb") as f:
|
| 102 |
+
f.write(uploaded.getvalue())
|
| 103 |
+
mesh_path, elapsed, msg = generate_mesh_from_image(
|
| 104 |
+
path,
|
| 105 |
+
output_dir=str(OUTPUTS / "mesh_run"),
|
| 106 |
+
mesh_format=mesh_format,
|
| 107 |
+
)
|
| 108 |
+
if mesh_path:
|
| 109 |
+
st.success(f"Done in {elapsed:.1f}s. {msg}")
|
| 110 |
+
with open(mesh_path, "rb") as f:
|
| 111 |
+
st.download_button("Download mesh", f, file_name=os.path.basename(mesh_path), key="dl_mesh_upload")
|
| 112 |
+
else:
|
| 113 |
+
st.error(msg)
|
| 114 |
+
else:
|
| 115 |
+
mesh_path, elapsed, msg = generate_mesh_from_text(
|
| 116 |
+
prompt_mesh,
|
| 117 |
+
output_dir=str(OUTPUTS),
|
| 118 |
+
mesh_format=mesh_format,
|
| 119 |
+
seed=seed_mesh,
|
| 120 |
+
)
|
| 121 |
+
if mesh_path:
|
| 122 |
+
st.success(f"Done in {elapsed:.1f}s. {msg}")
|
| 123 |
+
with open(mesh_path, "rb") as f:
|
| 124 |
+
st.download_button("Download mesh", f, file_name=os.path.basename(mesh_path), key="dl_mesh")
|
| 125 |
+
else:
|
| 126 |
+
st.error(msg)
|
| 127 |
+
except Exception as e:
|
| 128 |
+
st.exception(e)
|
| 129 |
+
|
| 130 |
+
with tab_skybox:
|
| 131 |
+
st.subheader("Generate 2:1 equirectangular skybox")
|
| 132 |
+
st.markdown(
|
| 133 |
+
"Uses **Stable Diffusion 2.1** at 2:1 aspect (e.g. 1024×512). "
|
| 134 |
+
"Optional seamless check compares left/right edges."
|
| 135 |
+
)
|
| 136 |
+
prompt_sky = st.text_input(
|
| 137 |
+
"Prompt (e.g. for skybox)",
|
| 138 |
+
value="Cyberpunk city skyline at dusk, neon reflections, cinematic lighting.",
|
| 139 |
+
key="sky_prompt",
|
| 140 |
+
)
|
| 141 |
+
col1, col2 = st.columns(2)
|
| 142 |
+
with col1:
|
| 143 |
+
width = st.selectbox("Width", [1024, 2048], key="sky_w")
|
| 144 |
+
height = width // 2
|
| 145 |
+
seed_sky = st.number_input("Seed (optional)", value=42, min_value=0, key="sky_seed")
|
| 146 |
+
with col2:
|
| 147 |
+
check_seamless = st.checkbox("Run seamless edge check", value=True, key="seamless")
|
| 148 |
+
|
| 149 |
+
if st.button("Generate skybox", key="btn_sky"):
|
| 150 |
+
if not prompt_sky.strip():
|
| 151 |
+
st.warning("Enter a prompt.")
|
| 152 |
+
else:
|
| 153 |
+
with st.spinner("Generating skybox..."):
|
| 154 |
+
try:
|
| 155 |
+
from scripts.skybox_generator import generate_skybox
|
| 156 |
+
from scripts.check_seamless import check_seamless as run_seamless
|
| 157 |
+
|
| 158 |
+
out_path, elapsed, vram_mb = generate_skybox(
|
| 159 |
+
prompt_sky,
|
| 160 |
+
output_dir=str(OUTPUTS),
|
| 161 |
+
width=width,
|
| 162 |
+
height=height,
|
| 163 |
+
seed=seed_sky,
|
| 164 |
+
)
|
| 165 |
+
st.success(f"Done in {elapsed:.1f}s. Peak VRAM: {vram_mb:.0f} MB")
|
| 166 |
+
st.image(out_path, use_container_width=True)
|
| 167 |
+
with open(out_path, "rb") as f:
|
| 168 |
+
st.download_button("Download skybox", f, file_name=os.path.basename(out_path), key="dl_sky")
|
| 169 |
+
|
| 170 |
+
if check_seamless:
|
| 171 |
+
result = run_seamless(out_path)
|
| 172 |
+
st.info(result["message"])
|
| 173 |
+
except Exception as e:
|
| 174 |
+
st.exception(e)
|
| 175 |
+
|
| 176 |
+
st.divider()
|
| 177 |
+
st.caption("Evoneural AI – Local ML Deployment MVP. Models run locally (no API).")
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
if __name__ == "__main__":
|
| 181 |
+
main()
|
environment.yml
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Conda environment for Evoneural MVP
|
| 2 |
+
# Create: conda env create -f environment.yml
|
| 3 |
+
# Activate: conda activate evoneural-mvp
|
| 4 |
+
|
| 5 |
+
name: evoneural-mvp
|
| 6 |
+
channels:
|
| 7 |
+
- pytorch
|
| 8 |
+
- nvidia
|
| 9 |
+
- conda-forge
|
| 10 |
+
- defaults
|
| 11 |
+
dependencies:
|
| 12 |
+
- python=3.10
|
| 13 |
+
- pip
|
| 14 |
+
- pytorch
|
| 15 |
+
- torchvision
|
| 16 |
+
- pytorch-cuda=11.8 # or 12.1; comment out if CPU-only
|
| 17 |
+
- pip:
|
| 18 |
+
- streamlit>=1.28.0
|
| 19 |
+
- diffusers>=0.25.0
|
| 20 |
+
- transformers>=4.35.0
|
| 21 |
+
- accelerate>=0.25.0
|
| 22 |
+
- safetensors>=0.4.0
|
| 23 |
+
- huggingface-hub>=0.20.0
|
| 24 |
+
- rembg>=2.0.50
|
| 25 |
+
- Pillow>=10.0.0
|
requirements.txt
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Evoneural MVP - Local 3D Mesh + Skybox Generation
|
| 2 |
+
# Python 3.10 recommended. Install PyTorch with CUDA first: https://pytorch.org
|
| 3 |
+
|
| 4 |
+
# Core
|
| 5 |
+
torch>=2.0.0
|
| 6 |
+
torchvision>=0.15.0
|
| 7 |
+
Pillow>=10.0.0
|
| 8 |
+
|
| 9 |
+
# Streamlit UI
|
| 10 |
+
streamlit>=1.28.0
|
| 11 |
+
|
| 12 |
+
# Skybox: Stable Diffusion (diffusers)
|
| 13 |
+
diffusers>=0.25.0
|
| 14 |
+
transformers>=4.35.0
|
| 15 |
+
accelerate>=0.25.0
|
| 16 |
+
safetensors>=0.4.0
|
| 17 |
+
|
| 18 |
+
# Optional: reduce VRAM for skybox
|
| 19 |
+
# xformers # uncomment if you have CUDA and want lower VRAM
|
| 20 |
+
|
| 21 |
+
# Mesh: TripoSR dependencies (also need TripoSR repo cloned - see README)
|
| 22 |
+
huggingface-hub>=0.20.0
|
| 23 |
+
rembg>=2.0.50
|
| 24 |
+
numpy>=1.24.0
|
| 25 |
+
|
| 26 |
+
# Text-to-image for mesh (same as skybox stack)
|
| 27 |
+
# (already above)
|
| 28 |
+
|
| 29 |
+
# TripoSR-specific (install after cloning TripoSR, or use our subprocess runner)
|
| 30 |
+
# omegaconf==2.3.0
|
| 31 |
+
# einops==0.7.0
|
| 32 |
+
# trimesh>=4.0.0
|
| 33 |
+
# xatlas==0.0.9
|
scripts/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Evoneural MVP scripts
|
scripts/check_seamless.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Seamless check for 2:1 equirectangular skybox: compare left vs right edge.
|
| 3 |
+
Returns MSE and a simple pass/fail (low MSE = more seamless).
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
from PIL import Image
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def check_seamless(image_path: str, column_width: int = 5) -> dict:
|
| 11 |
+
"""
|
| 12 |
+
Load image, compare left and right edge columns. Equirectangular wraps,
|
| 13 |
+
so left and right should match for a seamless skybox.
|
| 14 |
+
Returns dict with mse, passed (bool), and message.
|
| 15 |
+
"""
|
| 16 |
+
img = np.array(Image.open(image_path).convert("RGB"))
|
| 17 |
+
h, w = img.shape[:2]
|
| 18 |
+
|
| 19 |
+
if w < 2 * column_width:
|
| 20 |
+
return {
|
| 21 |
+
"mse": float("inf"),
|
| 22 |
+
"passed": False,
|
| 23 |
+
"message": f"Image width {w} too small for column width {column_width}",
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
left = img[:, :column_width].astype(np.float32)
|
| 27 |
+
right = img[:, -column_width:].astype(np.float32)
|
| 28 |
+
mse = float(np.mean((left - right) ** 2))
|
| 29 |
+
|
| 30 |
+
# Heuristic: MSE < 100 often looks reasonably seamless
|
| 31 |
+
passed = mse < 100
|
| 32 |
+
message = (
|
| 33 |
+
f"Left/right edge MSE = {mse:.2f}. "
|
| 34 |
+
+ ("Seamless (edges match)." if passed else "Edges differ (consider 360° model).")
|
| 35 |
+
)
|
| 36 |
+
return {"mse": mse, "passed": passed, "message": message}
|
scripts/download_sd_model.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Download Stable Diffusion v1.5 to ./weights/sd-v1-5 for offline use.
|
| 3 |
+
Run once (with internet). App auto-uses this folder if present.
|
| 4 |
+
|
| 5 |
+
python -m scripts.download_sd_model
|
| 6 |
+
|
| 7 |
+
Set HF_TOKEN=your_token if behind firewall. Can also use "Download model" in the app sidebar.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
import sys
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
ROOT = Path(__file__).resolve().parent.parent
|
| 15 |
+
MODEL_ID = "runwayml/stable-diffusion-v1-5"
|
| 16 |
+
DEFAULT_LOCAL_DIR = ROOT / "weights" / "sd-v1-5"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def download_sd_model(local_dir: str | Path | None = None, token: str | None = None) -> str:
|
| 20 |
+
"""Download runwayml/stable-diffusion-v1-5 to local_dir. Returns path on success, raises on failure."""
|
| 21 |
+
from huggingface_hub import snapshot_download
|
| 22 |
+
out_dir = Path(local_dir or DEFAULT_LOCAL_DIR)
|
| 23 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 24 |
+
tok = token or os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
|
| 25 |
+
snapshot_download(
|
| 26 |
+
repo_id=MODEL_ID,
|
| 27 |
+
local_dir=str(out_dir),
|
| 28 |
+
token=tok,
|
| 29 |
+
)
|
| 30 |
+
return str(out_dir.resolve())
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def main() -> None:
|
| 34 |
+
out_dir = os.environ.get("SD_MODEL_PATH", str(DEFAULT_LOCAL_DIR))
|
| 35 |
+
try:
|
| 36 |
+
path = download_sd_model(local_dir=out_dir)
|
| 37 |
+
print(f"Done. App will use: {path}")
|
| 38 |
+
print("Run: streamlit run app.py")
|
| 39 |
+
except Exception as e:
|
| 40 |
+
print(f"Download failed: {e}", file=sys.stderr)
|
| 41 |
+
print("Set HF_TOKEN=your_token if behind firewall (huggingface.co/settings/tokens)", file=sys.stderr)
|
| 42 |
+
raise SystemExit(1) from e
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
if __name__ == "__main__":
|
| 46 |
+
main()
|
scripts/mesh_generator.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Mesh generator: image → 3D mesh via TripoSR (local).
|
| 3 |
+
Expects TripoSR repo cloned at project_root/TripoSR. Uses subprocess to run run.py.
|
| 4 |
+
For text→mesh: first generate image with text_to_image(), then call this.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import subprocess
|
| 9 |
+
import sys
|
| 10 |
+
import time
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def find_triposr_root(project_root: str | None = None) -> str | None:
|
| 15 |
+
"""Locate TripoSR repo: ./TripoSR or ../TripoSR from script dir."""
|
| 16 |
+
if project_root is None:
|
| 17 |
+
project_root = str(Path(__file__).resolve().parent.parent)
|
| 18 |
+
candidates = [
|
| 19 |
+
os.path.join(project_root, "TripoSR"),
|
| 20 |
+
os.path.join(project_root, "..", "TripoSR"),
|
| 21 |
+
]
|
| 22 |
+
for p in candidates:
|
| 23 |
+
run_py = os.path.join(p, "run.py")
|
| 24 |
+
if os.path.isfile(run_py):
|
| 25 |
+
return p
|
| 26 |
+
return None
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def generate_mesh_from_image(
|
| 30 |
+
image_path: str,
|
| 31 |
+
output_dir: str = "outputs",
|
| 32 |
+
mesh_format: str = "glb",
|
| 33 |
+
triposr_root: str | None = None,
|
| 34 |
+
device: str = "cuda:0",
|
| 35 |
+
) -> tuple[str | None, float, str]:
|
| 36 |
+
"""
|
| 37 |
+
Run TripoSR on an image. Returns (path_to_mesh, inference_time_sec, message).
|
| 38 |
+
If TripoSR is not found, returns (None, 0, error_message).
|
| 39 |
+
"""
|
| 40 |
+
project_root = str(Path(__file__).resolve().parent.parent)
|
| 41 |
+
triposr_root = triposr_root or find_triposr_root(project_root)
|
| 42 |
+
if not triposr_root:
|
| 43 |
+
return (
|
| 44 |
+
None,
|
| 45 |
+
0.0,
|
| 46 |
+
"TripoSR not found. Clone it: git clone https://github.com/VAST-AI-Research/TripoSR.git",
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
| 50 |
+
# TripoSR writes to output_dir/0/mesh.obj (or mesh.glb)
|
| 51 |
+
run_py = os.path.join(triposr_root, "run.py")
|
| 52 |
+
cmd = [
|
| 53 |
+
sys.executable,
|
| 54 |
+
run_py,
|
| 55 |
+
image_path,
|
| 56 |
+
"--output-dir",
|
| 57 |
+
output_dir,
|
| 58 |
+
"--model-save-format",
|
| 59 |
+
mesh_format,
|
| 60 |
+
"--device",
|
| 61 |
+
device,
|
| 62 |
+
]
|
| 63 |
+
|
| 64 |
+
t0 = time.perf_counter()
|
| 65 |
+
try:
|
| 66 |
+
result = subprocess.run(
|
| 67 |
+
cmd,
|
| 68 |
+
cwd=triposr_root,
|
| 69 |
+
capture_output=True,
|
| 70 |
+
text=True,
|
| 71 |
+
timeout=120,
|
| 72 |
+
)
|
| 73 |
+
t1 = time.perf_counter()
|
| 74 |
+
if result.returncode != 0:
|
| 75 |
+
return (
|
| 76 |
+
None,
|
| 77 |
+
t1 - t0,
|
| 78 |
+
f"TripoSR failed: {result.stderr or result.stdout or 'unknown'}",
|
| 79 |
+
)
|
| 80 |
+
# Output is output_dir/0/mesh.glb or mesh.obj
|
| 81 |
+
mesh_path = os.path.join(output_dir, "0", f"mesh.{mesh_format}")
|
| 82 |
+
if not os.path.isfile(mesh_path):
|
| 83 |
+
return (None, t1 - t0, f"TripoSR did not produce {mesh_path}")
|
| 84 |
+
return (os.path.abspath(mesh_path), t1 - t0, "OK")
|
| 85 |
+
except subprocess.TimeoutExpired:
|
| 86 |
+
t1 = time.perf_counter()
|
| 87 |
+
return (None, t1 - t0, "TripoSR timed out (120s)")
|
| 88 |
+
except Exception as e:
|
| 89 |
+
t1 = time.perf_counter()
|
| 90 |
+
return (None, t1 - t0, str(e))
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def generate_mesh_from_text(
|
| 94 |
+
prompt: str,
|
| 95 |
+
output_dir: str = "outputs",
|
| 96 |
+
mesh_format: str = "glb",
|
| 97 |
+
seed: int | None = None,
|
| 98 |
+
) -> tuple[str | None, float, str]:
|
| 99 |
+
"""
|
| 100 |
+
Text → image (SD) → mesh (TripoSR). Returns (path_to_mesh, total_time_sec, message).
|
| 101 |
+
"""
|
| 102 |
+
# Import here so script can run from any cwd
|
| 103 |
+
_root = str(Path(__file__).resolve().parent.parent)
|
| 104 |
+
if _root not in sys.path:
|
| 105 |
+
sys.path.insert(0, _root)
|
| 106 |
+
from scripts.text_to_image import text_to_image
|
| 107 |
+
|
| 108 |
+
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
| 109 |
+
t0 = time.perf_counter()
|
| 110 |
+
try:
|
| 111 |
+
image_path, _ = text_to_image(prompt, output_dir=output_dir, seed=seed)
|
| 112 |
+
except Exception as e:
|
| 113 |
+
return (None, 0.0, f"Text-to-image failed: {e}")
|
| 114 |
+
|
| 115 |
+
mesh_path, mesh_time, msg = generate_mesh_from_image(
|
| 116 |
+
image_path,
|
| 117 |
+
output_dir=os.path.join(output_dir, "mesh_run"),
|
| 118 |
+
mesh_format=mesh_format,
|
| 119 |
+
)
|
| 120 |
+
total_time = time.perf_counter() - t0
|
| 121 |
+
if mesh_path:
|
| 122 |
+
return (mesh_path, total_time, msg)
|
| 123 |
+
return (None, total_time, msg)
|
scripts/skybox_generator.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Skybox generator: text → 2:1 equirectangular image (Stable Diffusion, local).
|
| 3 |
+
Uses FP16 to reduce VRAM. Output 1024x512 or 2048x1024.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import time
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
|
| 12 |
+
# Default: v1.5 works without license acceptance. Use SD_MODEL_ID to prefer SD 2.1.
|
| 13 |
+
DEFAULT_MODEL_ID = "runwayml/stable-diffusion-v1-5"
|
| 14 |
+
FALLBACK_MODEL_ID = "runwayml/stable-diffusion-v1-5" # Same; alternate if primary fails
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def get_device() -> str:
|
| 18 |
+
return "cuda" if torch.cuda.is_available() else "cpu"
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _is_complete_sd_dir(path: Path) -> bool:
|
| 22 |
+
"""True if path looks like a complete Stable Diffusion pipeline (has unet weights)."""
|
| 23 |
+
if not path.is_dir():
|
| 24 |
+
return False
|
| 25 |
+
unet = path / "unet"
|
| 26 |
+
if not unet.is_dir():
|
| 27 |
+
return False
|
| 28 |
+
return any(
|
| 29 |
+
(unet / f).exists()
|
| 30 |
+
for f in ("diffusion_pytorch_model.safetensors", "diffusion_pytorch_model.bin")
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _default_local_weights_dir() -> str | None:
|
| 35 |
+
"""First complete SD folder under weights/ (sd-v1-5 or stable-diffusion-2-1-base)."""
|
| 36 |
+
try:
|
| 37 |
+
root = Path(__file__).resolve().parent.parent
|
| 38 |
+
for name in ("sd-v1-5", "stable-diffusion-2-1-base"):
|
| 39 |
+
local = root / "weights" / name
|
| 40 |
+
if _is_complete_sd_dir(local):
|
| 41 |
+
return str(local)
|
| 42 |
+
return None
|
| 43 |
+
except Exception:
|
| 44 |
+
return None
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _resolve_model_path_and_token():
|
| 48 |
+
"""Use local path if set or default weights/ folder exists, else Hub id. Token from HF_TOKEN or huggingface-cli login."""
|
| 49 |
+
local = os.environ.get("SD_MODEL_PATH", "").strip()
|
| 50 |
+
if local and os.path.isdir(local):
|
| 51 |
+
return local, None
|
| 52 |
+
default_local = _default_local_weights_dir()
|
| 53 |
+
if default_local:
|
| 54 |
+
return default_local, None
|
| 55 |
+
model_id = os.environ.get("SD_MODEL_ID", DEFAULT_MODEL_ID)
|
| 56 |
+
token = os.environ.get("HF_TOKEN") or True # True = use cached login
|
| 57 |
+
return model_id, token
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def generate_skybox(
|
| 61 |
+
prompt: str,
|
| 62 |
+
output_dir: str = "outputs",
|
| 63 |
+
width: int = 1024,
|
| 64 |
+
height: int = 512,
|
| 65 |
+
seed: int | None = None,
|
| 66 |
+
model_id: str | None = None,
|
| 67 |
+
) -> tuple[str, float, float]:
|
| 68 |
+
"""
|
| 69 |
+
Generate a 2:1 equirectangular skybox image from a text prompt.
|
| 70 |
+
Returns (path_to_image, inference_time_sec, peak_vram_mb).
|
| 71 |
+
"""
|
| 72 |
+
from diffusers import StableDiffusionPipeline
|
| 73 |
+
|
| 74 |
+
device = get_device()
|
| 75 |
+
dtype = torch.float16 if device == "cuda" else torch.float32
|
| 76 |
+
|
| 77 |
+
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
| 78 |
+
|
| 79 |
+
pretrained, token = _resolve_model_path_and_token()
|
| 80 |
+
load_id = model_id or pretrained
|
| 81 |
+
local_only = os.path.isdir(load_id)
|
| 82 |
+
pipe = None
|
| 83 |
+
last_error = None
|
| 84 |
+
|
| 85 |
+
def _load(pid: str, local: bool) -> bool:
|
| 86 |
+
nonlocal pipe, last_error
|
| 87 |
+
try:
|
| 88 |
+
pipe = StableDiffusionPipeline.from_pretrained(
|
| 89 |
+
pid,
|
| 90 |
+
torch_dtype=dtype,
|
| 91 |
+
safety_checker=None,
|
| 92 |
+
token=None if local else (token or True),
|
| 93 |
+
local_files_only=local,
|
| 94 |
+
)
|
| 95 |
+
return True
|
| 96 |
+
except Exception as err:
|
| 97 |
+
last_error = err
|
| 98 |
+
return False
|
| 99 |
+
|
| 100 |
+
if _load(load_id, local_only):
|
| 101 |
+
pass
|
| 102 |
+
elif not local_only and _load(FALLBACK_MODEL_ID, False):
|
| 103 |
+
pass
|
| 104 |
+
if pipe is None:
|
| 105 |
+
raise RuntimeError(
|
| 106 |
+
"Could not load Stable Diffusion. Need internet to download the model (first run).\n"
|
| 107 |
+
" - Set HF_TOKEN=your_token if behind firewall (huggingface.co/settings/tokens)\n"
|
| 108 |
+
" - Or download once: huggingface-cli download runwayml/stable-diffusion-v1-5 --local-dir ./weights/sd-v1-5"
|
| 109 |
+
) from last_error
|
| 110 |
+
|
| 111 |
+
pipe = pipe.to(device)
|
| 112 |
+
|
| 113 |
+
# Optional: enable xformers for lower VRAM (uncomment if installed)
|
| 114 |
+
# if device == "cuda":
|
| 115 |
+
# pipe.enable_xformers_memory_efficient_attention()
|
| 116 |
+
|
| 117 |
+
if device == "cuda":
|
| 118 |
+
torch.cuda.reset_peak_memory_stats()
|
| 119 |
+
torch.cuda.synchronize()
|
| 120 |
+
|
| 121 |
+
generator = None
|
| 122 |
+
if seed is not None:
|
| 123 |
+
generator = torch.Generator(device=device).manual_seed(seed)
|
| 124 |
+
|
| 125 |
+
t0 = time.perf_counter()
|
| 126 |
+
image = pipe(
|
| 127 |
+
prompt=prompt,
|
| 128 |
+
width=width,
|
| 129 |
+
height=height,
|
| 130 |
+
num_inference_steps=50,
|
| 131 |
+
generator=generator,
|
| 132 |
+
).images[0]
|
| 133 |
+
|
| 134 |
+
if device == "cuda":
|
| 135 |
+
torch.cuda.synchronize()
|
| 136 |
+
t1 = time.perf_counter()
|
| 137 |
+
inference_time = t1 - t0
|
| 138 |
+
peak_vram_mb = (
|
| 139 |
+
torch.cuda.max_memory_allocated() / 1024 / 1024
|
| 140 |
+
if device == "cuda"
|
| 141 |
+
else 0.0
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
# Save with safe filename
|
| 145 |
+
safe_name = "".join(c if c.isalnum() or c in " -_" else "_" for c in prompt)[:60]
|
| 146 |
+
out_path = os.path.join(output_dir, f"skybox_{safe_name.strip()}.png")
|
| 147 |
+
image.save(out_path)
|
| 148 |
+
|
| 149 |
+
return out_path, inference_time, peak_vram_mb
|
scripts/text_to_image.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Text-to-image for mesh pipeline: generate a single image from prompt (SD 2.1, local).
|
| 3 |
+
Uses same SD_MODEL_PATH / HF_TOKEN as skybox_generator.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import time
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
|
| 12 |
+
from scripts.skybox_generator import _resolve_model_path_and_token, FALLBACK_MODEL_ID
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def get_device() -> str:
|
| 16 |
+
return "cuda" if torch.cuda.is_available() else "cpu"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def text_to_image(
|
| 20 |
+
prompt: str,
|
| 21 |
+
output_dir: str = "outputs",
|
| 22 |
+
size: int = 512,
|
| 23 |
+
seed: int | None = None,
|
| 24 |
+
model_id: str | None = None,
|
| 25 |
+
) -> tuple[str, float]:
|
| 26 |
+
"""Generate one image from text. Returns (path_to_image, inference_time_sec)."""
|
| 27 |
+
from diffusers import StableDiffusionPipeline
|
| 28 |
+
|
| 29 |
+
device = get_device()
|
| 30 |
+
dtype = torch.float16 if device == "cuda" else torch.float32
|
| 31 |
+
|
| 32 |
+
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
| 33 |
+
|
| 34 |
+
pretrained, token = _resolve_model_path_and_token()
|
| 35 |
+
load_id = model_id or pretrained
|
| 36 |
+
local_only = os.path.isdir(load_id)
|
| 37 |
+
pipe = None
|
| 38 |
+
try:
|
| 39 |
+
pipe = StableDiffusionPipeline.from_pretrained(
|
| 40 |
+
load_id,
|
| 41 |
+
torch_dtype=dtype,
|
| 42 |
+
safety_checker=None,
|
| 43 |
+
token=None if local_only else (token or True),
|
| 44 |
+
local_files_only=local_only,
|
| 45 |
+
)
|
| 46 |
+
except Exception:
|
| 47 |
+
if not local_only:
|
| 48 |
+
try:
|
| 49 |
+
pipe = StableDiffusionPipeline.from_pretrained(
|
| 50 |
+
FALLBACK_MODEL_ID,
|
| 51 |
+
torch_dtype=dtype,
|
| 52 |
+
safety_checker=None,
|
| 53 |
+
token=token or True,
|
| 54 |
+
)
|
| 55 |
+
except Exception:
|
| 56 |
+
pass
|
| 57 |
+
if pipe is None:
|
| 58 |
+
raise RuntimeError(
|
| 59 |
+
"Could not load Stable Diffusion. Need internet (first run). Set HF_TOKEN if behind firewall."
|
| 60 |
+
)
|
| 61 |
+
pipe = pipe.to(device)
|
| 62 |
+
|
| 63 |
+
generator = None
|
| 64 |
+
if seed is not None:
|
| 65 |
+
generator = torch.Generator(device=device).manual_seed(seed)
|
| 66 |
+
|
| 67 |
+
t0 = time.perf_counter()
|
| 68 |
+
image = pipe(
|
| 69 |
+
prompt=prompt,
|
| 70 |
+
width=size,
|
| 71 |
+
height=size,
|
| 72 |
+
num_inference_steps=50,
|
| 73 |
+
generator=generator,
|
| 74 |
+
).images[0]
|
| 75 |
+
t1 = time.perf_counter()
|
| 76 |
+
|
| 77 |
+
safe_name = "".join(c if c.isalnum() or c in " -_" else "_" for c in prompt)[:50]
|
| 78 |
+
out_path = os.path.join(output_dir, f"mesh_input_{safe_name.strip()}.png")
|
| 79 |
+
image.save(out_path)
|
| 80 |
+
return out_path, t1 - t0
|
scripts/upload_to_hf.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Upload evoneural code and config to Hugging Face (no weights/outputs).
|
| 3 |
+
Run from project root: python -m scripts.upload_to_hf
|
| 4 |
+
|
| 5 |
+
Requires: pip install huggingface_hub, and HF token (huggingface-cli login or HF_TOKEN).
|
| 6 |
+
"""
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
REPO_ID = "evoneural/evoneuralIn3D"
|
| 10 |
+
ROOT = Path(__file__).resolve().parent.parent
|
| 11 |
+
|
| 12 |
+
# Same exclusions as .huggingfaceignore so only code/config are uploaded
|
| 13 |
+
IGNORE_PATTERNS = [
|
| 14 |
+
".venv/*",
|
| 15 |
+
"venv/*",
|
| 16 |
+
"env/*",
|
| 17 |
+
"__pycache__/*",
|
| 18 |
+
"*.pyc",
|
| 19 |
+
"outputs/*",
|
| 20 |
+
"weights/*",
|
| 21 |
+
"*.obj",
|
| 22 |
+
"*.glb",
|
| 23 |
+
"*.png",
|
| 24 |
+
"TripoSR/*",
|
| 25 |
+
"*.ckpt",
|
| 26 |
+
"*.safetensors",
|
| 27 |
+
"*.bin",
|
| 28 |
+
".idea/*",
|
| 29 |
+
".vscode/*",
|
| 30 |
+
".streamlit/*",
|
| 31 |
+
"*.log",
|
| 32 |
+
"performance_log.txt",
|
| 33 |
+
".git/*",
|
| 34 |
+
]
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def main() -> None:
|
| 38 |
+
from huggingface_hub import HfApi
|
| 39 |
+
|
| 40 |
+
api = HfApi()
|
| 41 |
+
# Create repo if it doesn't exist (needs write token)
|
| 42 |
+
api.create_repo(repo_id=REPO_ID, repo_type="model", exist_ok=True)
|
| 43 |
+
print(f"Uploading to {REPO_ID} (code and config only)...")
|
| 44 |
+
api.upload_folder(
|
| 45 |
+
folder_path=str(ROOT),
|
| 46 |
+
repo_id=REPO_ID,
|
| 47 |
+
repo_type="model",
|
| 48 |
+
ignore_patterns=IGNORE_PATTERNS,
|
| 49 |
+
)
|
| 50 |
+
print(f"Done. See https://huggingface.co/{REPO_ID}")
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
if __name__ == "__main__":
|
| 54 |
+
main()
|