Compare commits
17 Commits
7a43e5da08
...
d5d207d19c
| Author | SHA1 | Date | |
|---|---|---|---|
| d5d207d19c | |||
| 5a79e84fbb | |||
| cfb0041286 | |||
| 7f720033e2 | |||
| 4cf5a322ae | |||
| eafe02200c | |||
| abcc993c03 | |||
| ba4d6685ec | |||
| 26a50b1453 | |||
| 91fce08509 | |||
| 7468fce40f | |||
| 562a9ad2e6 | |||
| 801007fe7f | |||
| 483c0fe9a2 | |||
| aaca619640 | |||
| 877a5fa8d5 | |||
| 1905638407 |
78
.gitignore
vendored
Normal file
78
.gitignore
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg
|
||||
*.egg-info/
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# Virtual environments
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
|
||||
# Local configuration and secrets
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
settings.json
|
||||
secrets.*
|
||||
*.pem
|
||||
*.key
|
||||
*.p12
|
||||
*.pfx
|
||||
id_rsa*
|
||||
id_ed25519*
|
||||
|
||||
# App output and caches
|
||||
web/output/
|
||||
PyWebview/web/output/
|
||||
.hf_cache/
|
||||
*.log
|
||||
build/
|
||||
dist/
|
||||
|
||||
# Generated media and 3D assets
|
||||
*.png
|
||||
*.jpg
|
||||
*.jpeg
|
||||
*.webp
|
||||
*.glb
|
||||
*.gltf
|
||||
*.blend
|
||||
*.hdr
|
||||
*.exr
|
||||
*.mp4
|
||||
*.mov
|
||||
*.zip
|
||||
*.tar.gz
|
||||
!icon.png
|
||||
!icon.icns
|
||||
|
||||
# Local model weights
|
||||
*.safetensors
|
||||
*.ckpt
|
||||
*.pt
|
||||
*.pth
|
||||
*.onnx
|
||||
*.bin
|
||||
|
||||
# Node and lockfile leftovers
|
||||
node_modules/
|
||||
package-lock.json
|
||||
npmCargo.lock
|
||||
*.lock
|
||||
|
||||
# Native build artifacts
|
||||
*.o
|
||||
*.dylib
|
||||
*.a
|
||||
*.so
|
||||
*.exe
|
||||
debug
|
||||
121
3D Model Generator.spec
Normal file
121
3D Model Generator.spec
Normal file
@@ -0,0 +1,121 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
from PyInstaller.utils.hooks import collect_data_files
|
||||
from PyInstaller.utils.hooks import collect_submodules
|
||||
from PyInstaller.utils.hooks import copy_metadata
|
||||
|
||||
datas = [('web/index.html', 'web'), ('icon.png', '.'), ('scene_setup.py', '.')]
|
||||
hiddenimports = [
|
||||
'image_to_3d',
|
||||
'generate_equirect',
|
||||
'diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion',
|
||||
'diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_inpaint',
|
||||
'diffusers.pipelines.stable_diffusion.pipeline_output',
|
||||
'diffusers.models.autoencoders.autoencoder_kl',
|
||||
'diffusers.models.autoencoders.vae',
|
||||
'diffusers.models.unets.unet_2d_condition',
|
||||
'diffusers.models.unets.unet_2d_blocks',
|
||||
'diffusers.schedulers.scheduling_ddim',
|
||||
'diffusers.schedulers.scheduling_ddpm',
|
||||
'diffusers.schedulers.scheduling_dpmsolver_multistep',
|
||||
'diffusers.schedulers.scheduling_euler_discrete',
|
||||
'diffusers.schedulers.scheduling_lms_discrete',
|
||||
'diffusers.schedulers.scheduling_pndm',
|
||||
'transformers.models.auto.configuration_auto',
|
||||
'transformers.models.auto.feature_extraction_auto',
|
||||
'transformers.models.auto.image_processing_auto',
|
||||
'transformers.models.auto.modeling_auto',
|
||||
'transformers.models.auto.tokenization_auto',
|
||||
'transformers.models.clip.configuration_clip',
|
||||
'transformers.models.clip.image_processing_clip',
|
||||
'transformers.models.clip.modeling_clip',
|
||||
'transformers.models.clip.processing_clip',
|
||||
'transformers.models.clip.tokenization_clip',
|
||||
'transformers.models.clip.tokenization_clip_fast',
|
||||
'safetensors.torch',
|
||||
]
|
||||
datas += collect_data_files('diffusers')
|
||||
datas += collect_data_files('transformers')
|
||||
datas += collect_data_files('huggingface_hub')
|
||||
datas += copy_metadata('replicate')
|
||||
hiddenimports += collect_submodules(
|
||||
'diffusers.schedulers',
|
||||
filter=lambda name: '.scheduling_' in name and '_flax' not in name,
|
||||
)
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['main.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=datas,
|
||||
hiddenimports=hiddenimports,
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[
|
||||
'gi',
|
||||
'PyQt5',
|
||||
'PyQt6',
|
||||
'PySide2',
|
||||
'PySide6',
|
||||
'tkinter',
|
||||
'altair',
|
||||
'cv2',
|
||||
'datasets',
|
||||
'flax',
|
||||
'jax',
|
||||
'jaxlib',
|
||||
'librosa',
|
||||
'llvmlite',
|
||||
'matplotlib',
|
||||
'nltk',
|
||||
'numba',
|
||||
'onnx',
|
||||
'onnxruntime',
|
||||
'pandas',
|
||||
'pygame',
|
||||
'sklearn',
|
||||
'soundfile',
|
||||
'spacy',
|
||||
'tensorflow',
|
||||
'torchaudio',
|
||||
'torchvision',
|
||||
],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name='3D Model Generator',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon=['icon.icns'],
|
||||
)
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name='3D Model Generator',
|
||||
)
|
||||
app = BUNDLE(
|
||||
coll,
|
||||
name='3D Model Generator.app',
|
||||
icon='icon.icns',
|
||||
bundle_identifier=None,
|
||||
)
|
||||
209
README.md
Normal file
209
README.md
Normal file
@@ -0,0 +1,209 @@
|
||||
# 3D Model Generator
|
||||
|
||||
A local desktop tool for turning short object ideas into image prompts, generated concept images, Replicate Hunyuan 3D models, and optional HDRI scene lighting.
|
||||
|
||||
The app runs as a `pywebview` desktop window. It uses Ollama for prompt generation, local Diffusers or Replicate for image generation, Replicate for image-to-3D conversion, and Blender for viewing generated `.glb` files.
|
||||
|
||||
## Features
|
||||
|
||||
- Generate Stable Diffusion-style prompts from a short object name.
|
||||
- Convert prompts into editable JSON generation settings.
|
||||
- Generate image batches with either:
|
||||
- local Diffusers using a local DreamShaper model
|
||||
- Replicate API using one of three configured image models
|
||||
- Generate 3D `.glb` models from selected images with `tencent/hunyuan-3d-3.1`.
|
||||
- Generate an equirectangular HDRI panorama and open the model in Blender.
|
||||
- Store image metadata in sidecar JSON files so prompts can be reused later.
|
||||
- Browse generated 2D images and generated 3D models in the built-in gallery.
|
||||
|
||||
## Requirements
|
||||
|
||||
- macOS
|
||||
- Python 3.11
|
||||
- Ollama running locally
|
||||
- An Ollama model named `mistral:latest`
|
||||
- Optional but recommended: a Replicate API token
|
||||
- Optional for local image generation: local Diffusers model files
|
||||
- Optional for HDRI: Topaz Photo AI CLI and local HDRI Diffusers model files
|
||||
- Optional for 3D viewing: Blender installed at `/Applications/Blender.app`
|
||||
|
||||
The launcher creates a local virtual environment in `.venv/` and installs Python dependencies from `requirements.txt`.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
./run.sh
|
||||
```
|
||||
|
||||
The script will:
|
||||
|
||||
1. Create `.venv/` if it does not exist.
|
||||
2. Activate the virtual environment.
|
||||
3. Install dependencies.
|
||||
4. Run `python main.py`.
|
||||
|
||||
If you want to force a specific Python executable:
|
||||
|
||||
```bash
|
||||
PYTHON=/path/to/python3.11 ./run.sh
|
||||
```
|
||||
|
||||
## Ollama Setup
|
||||
|
||||
The app expects Ollama at:
|
||||
|
||||
```text
|
||||
http://localhost:11434/api/generate
|
||||
```
|
||||
|
||||
and uses:
|
||||
|
||||
```text
|
||||
mistral:latest
|
||||
```
|
||||
|
||||
Install and run the model with:
|
||||
|
||||
```bash
|
||||
ollama pull mistral
|
||||
ollama serve
|
||||
```
|
||||
|
||||
## Replicate Setup
|
||||
|
||||
Open Settings in the app and paste your Replicate API token, or set it in your shell:
|
||||
|
||||
```bash
|
||||
export REPLICATE_API_TOKEN="r8_..."
|
||||
./run.sh
|
||||
```
|
||||
|
||||
The token is saved outside the repository at:
|
||||
|
||||
```text
|
||||
~/Library/Application Support/3d-model-generator/settings.json
|
||||
```
|
||||
|
||||
Do not commit this file.
|
||||
|
||||
## Image Generation Backends
|
||||
|
||||
Settings lets you choose between:
|
||||
|
||||
- `Local (Diffusers)`
|
||||
- `Replicate API`
|
||||
|
||||
The generated prompt JSON stays the same for both modes. Replicate mode maps the same positive prompt, negative prompt, dimensions/aspect ratio, steps where supported, and batch count to each model's API.
|
||||
|
||||
Configured Replicate image models:
|
||||
|
||||
| Model | Use | Listed price |
|
||||
| --- | --- | --- |
|
||||
| `black-forest-labs/flux-schnell` | Cheap drafts | $3 / 1000 output images |
|
||||
| `google/imagen-4-fast` | Balanced default | $0.02 / output image |
|
||||
| `bytedance/seedream-4` | Higher quality | $0.03 / output image |
|
||||
|
||||
Prices are listed by Replicate and may change.
|
||||
|
||||
## 3D Generation
|
||||
|
||||
Right-click a generated image and choose:
|
||||
|
||||
- `Generate 3D Model`
|
||||
- `Generate 3D Model + HDRI`
|
||||
|
||||
3D conversion uses:
|
||||
|
||||
```text
|
||||
tencent/hunyuan-3d-3.1
|
||||
```
|
||||
|
||||
Current listed Replicate price in the app:
|
||||
|
||||
```text
|
||||
$0.16 / unit
|
||||
```
|
||||
|
||||
The script waits up to 10 minutes for Replicate predictions before timing out.
|
||||
|
||||
## Local Model Paths
|
||||
|
||||
Some local paths are currently hard-coded and should be edited for your machine.
|
||||
|
||||
In `main.py`:
|
||||
|
||||
```python
|
||||
MODEL_PATH = "/Volumes/SD/ML-Models/diffusers/dreamshaper_8_diffusers"
|
||||
```
|
||||
|
||||
In `generate_equirect.py`:
|
||||
|
||||
```python
|
||||
model_path = "/Volumes/SD/ML-Models/diffusers/hdri-panorama-v1-diffusers"
|
||||
topaz_cli = "/Applications/Topaz Photo AI.app/Contents/MacOS/Topaz Photo AI"
|
||||
```
|
||||
|
||||
In `main.py`, external app paths are also hard-coded:
|
||||
|
||||
```python
|
||||
"/Applications/Blender.app/Contents/MacOS/Blender"
|
||||
"Adobe Photoshop 2024"
|
||||
```
|
||||
|
||||
If you only use Replicate image generation and 3D generation, the local DreamShaper path is not needed for image generation.
|
||||
|
||||
## Output Files
|
||||
|
||||
Generated files are written under:
|
||||
|
||||
```text
|
||||
web/output/
|
||||
```
|
||||
|
||||
Typical outputs:
|
||||
|
||||
- `.png` generated image
|
||||
- `.png.json` prompt metadata sidecar
|
||||
- `.glb` generated 3D model
|
||||
- `_hdri_seamless.png` generated HDRI image
|
||||
|
||||
`web/output/` is ignored by Git.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```text
|
||||
main.py Desktop app, UI API, prompt flow, image generation
|
||||
image_to_3d.py Replicate Hunyuan 3D conversion
|
||||
generate_equirect.py HDRI generation and seam fixing
|
||||
scene_setup.py Blender scene setup for generated models
|
||||
web/index.html App UI
|
||||
requirements.txt Python dependencies
|
||||
run.sh Virtualenv setup and app launcher
|
||||
```
|
||||
|
||||
## Git And Secrets
|
||||
|
||||
The repository ignores local output, model weights, virtual environments, Hugging Face cache files, and local secret/config files.
|
||||
|
||||
Before publishing, it is still worth running:
|
||||
|
||||
```bash
|
||||
git status --short --ignored
|
||||
git ls-files -co --exclude-standard
|
||||
```
|
||||
|
||||
Do not commit:
|
||||
|
||||
- `.env`
|
||||
- `settings.json`
|
||||
- Replicate API tokens
|
||||
- model weights
|
||||
- generated media
|
||||
- `web/output/`
|
||||
|
||||
## Notes
|
||||
|
||||
- `numpy<2` is pinned because some installed ML/runtime dependencies may still be incompatible with NumPy 2.x.
|
||||
- The app currently targets macOS paths and behavior.
|
||||
- HDRI generation depends on local models and Topaz Photo AI CLI.
|
||||
- Replicate image generation and Hunyuan 3D generation spend Replicate credits.
|
||||
@@ -34,23 +34,13 @@ def unshift_image(img: Image.Image, shift: int) -> Image.Image:
|
||||
out.paste(img.crop((0, 0, w - shift, h)), (shift, 0))
|
||||
return out
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate an equirectangular HDRI, make it seamless, and upscale it with Topaz Photo AI CLI."
|
||||
)
|
||||
parser.add_argument("--prompt", required=True,
|
||||
help="Text prompt for generation and inpainting")
|
||||
parser.add_argument("--output", required=True,
|
||||
help="Filename for the final upscaled image (e.g. seamless.png)")
|
||||
parser.add_argument("--work-dir", default=os.path.dirname(os.path.abspath(__file__)),
|
||||
help="Working directory for intermediates and final outputs")
|
||||
args = parser.parse_args()
|
||||
|
||||
def generate_equirect(prompt: str, output: str, work_dir: str | None = None) -> str | None:
|
||||
# Output-Ordner (bleibt wie gehabt)
|
||||
output_abs = os.path.abspath(args.output)
|
||||
output_abs = os.path.abspath(output)
|
||||
work_dir = work_dir or os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# Zwischenschritte landen im eigenem temp-Ordner:
|
||||
with tempfile.TemporaryDirectory(dir=args.work_dir) as tempdir:
|
||||
with tempfile.TemporaryDirectory(dir=work_dir) as tempdir:
|
||||
print(f"→ Using tempdir: {tempdir}")
|
||||
|
||||
model_path = "/Volumes/SD/ML-Models/diffusers/hdri-panorama-v1-diffusers"
|
||||
@@ -76,7 +66,7 @@ def main():
|
||||
|
||||
print("→ Generating equirectangular HDRI…")
|
||||
image = gen_pipe(
|
||||
prompt=args.prompt,
|
||||
prompt=prompt,
|
||||
num_inference_steps=steps,
|
||||
guidance_scale=scale-1.5,
|
||||
width=width,
|
||||
@@ -102,7 +92,7 @@ def main():
|
||||
|
||||
print("→ Inpainting seam for seamless tiling…")
|
||||
inpainted = inpaint_pipe(
|
||||
prompt=args.prompt,
|
||||
prompt=prompt,
|
||||
image=shifted,
|
||||
mask_image=mask,
|
||||
num_inference_steps=steps,
|
||||
@@ -111,7 +101,7 @@ def main():
|
||||
height=height
|
||||
).images[0]
|
||||
|
||||
seamless_path = os.path.join(tempdir, os.path.basename(args.output))
|
||||
seamless_path = os.path.join(tempdir, os.path.basename(output_abs))
|
||||
inpainted = unshift_image(inpainted, shift_amt)
|
||||
inpainted.save(seamless_path)
|
||||
print(f"→ Crafted seamless image: {seamless_path}")
|
||||
@@ -132,11 +122,26 @@ def main():
|
||||
)
|
||||
if not upscaled_files:
|
||||
print("→ No PNG output found in tempdir after Topaz run!")
|
||||
return
|
||||
return None
|
||||
|
||||
upscaled = upscaled_files[0]
|
||||
shutil.move(upscaled, output_abs)
|
||||
print(f"→ Upscaled image moved to {output_abs}")
|
||||
return output_abs
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate an equirectangular HDRI, make it seamless, and upscale it with Topaz Photo AI CLI."
|
||||
)
|
||||
parser.add_argument("--prompt", required=True,
|
||||
help="Text prompt for generation and inpainting")
|
||||
parser.add_argument("--output", required=True,
|
||||
help="Filename for the final upscaled image (e.g. seamless.png)")
|
||||
parser.add_argument("--work-dir", default=os.path.dirname(os.path.abspath(__file__)),
|
||||
help="Working directory for intermediates and final outputs")
|
||||
args = parser.parse_args()
|
||||
generate_equirect(args.prompt, args.output, args.work_dir)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
150
main.py
150
main.py
@@ -1,5 +1,17 @@
|
||||
import os
|
||||
import sys
|
||||
EARLY_IS_FROZEN = getattr(sys, "frozen", False)
|
||||
EARLY_BASE_DIR = os.path.dirname(sys.executable) if EARLY_IS_FROZEN else os.path.dirname(os.path.abspath(__file__))
|
||||
EARLY_APP_SUPPORT_DIR = os.path.join(
|
||||
os.path.expanduser("~"),
|
||||
"Library",
|
||||
"Application Support",
|
||||
"3d-model-generator",
|
||||
)
|
||||
EARLY_HF_HOME = os.path.join(EARLY_APP_SUPPORT_DIR if EARLY_IS_FROZEN else EARLY_BASE_DIR, ".hf_cache")
|
||||
os.environ.setdefault("HF_HOME", EARLY_HF_HOME)
|
||||
os.environ.setdefault("HUGGINGFACE_HUB_CACHE", os.path.join(EARLY_HF_HOME, "hub"))
|
||||
|
||||
import json
|
||||
import re
|
||||
import requests
|
||||
@@ -11,6 +23,7 @@ import webview
|
||||
import random
|
||||
import subprocess
|
||||
import concurrent.futures
|
||||
import shutil
|
||||
from PIL import Image, PngImagePlugin
|
||||
from diffusers import StableDiffusionPipeline, DPMSolverMultistepScheduler
|
||||
import torch
|
||||
@@ -24,22 +37,43 @@ MODEL = "mistral:latest" # or "mistral-small3.1:24b"
|
||||
# Path to your diffusers-converted model
|
||||
MODEL_PATH = "/Volumes/SD/ML-Models/diffusers/dreamshaper_8_diffusers"
|
||||
|
||||
# Where to save generated images
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
# Store everything under web/output instead of project_root/output
|
||||
WEB_DIR = os.path.join(BASE_DIR, "web")
|
||||
OUTPUT_DIR = os.path.join(WEB_DIR, "output")
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
APP_SUPPORT_DIR = os.path.join(
|
||||
os.path.expanduser("~"),
|
||||
"Library",
|
||||
"Application Support",
|
||||
"3d-model-generator",
|
||||
)
|
||||
IS_FROZEN = getattr(sys, "frozen", False)
|
||||
BASE_DIR = os.path.dirname(sys.executable) if IS_FROZEN else os.path.dirname(os.path.abspath(__file__))
|
||||
BUNDLE_DIR = getattr(sys, "_MEIPASS", BASE_DIR)
|
||||
APP_SUPPORT_DIR = EARLY_APP_SUPPORT_DIR
|
||||
SETTINGS_PATH = os.path.join(APP_SUPPORT_DIR, "settings.json")
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def resource_path(*parts: str) -> str:
|
||||
return os.path.join(BUNDLE_DIR, *parts)
|
||||
|
||||
|
||||
def prepare_web_dir() -> str:
|
||||
if not IS_FROZEN:
|
||||
return os.path.join(BASE_DIR, "web")
|
||||
|
||||
src = resource_path("web")
|
||||
dst = os.path.join(APP_SUPPORT_DIR, "web")
|
||||
os.makedirs(dst, exist_ok=True)
|
||||
|
||||
for root, dirs, files in os.walk(src):
|
||||
dirs[:] = [d for d in dirs if d != "output"]
|
||||
rel = os.path.relpath(root, src)
|
||||
out_root = dst if rel == "." else os.path.join(dst, rel)
|
||||
os.makedirs(out_root, exist_ok=True)
|
||||
for fname in files:
|
||||
shutil.copy2(os.path.join(root, fname), os.path.join(out_root, fname))
|
||||
|
||||
return dst
|
||||
|
||||
|
||||
WEB_DIR = prepare_web_dir()
|
||||
WEB_INDEX = os.path.join(WEB_DIR, "index.html")
|
||||
OUTPUT_DIR = os.path.join(WEB_DIR, "output")
|
||||
APP_ICON_PATH = resource_path("icon.png")
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
|
||||
IMAGE_BACKEND_LOCAL = "local"
|
||||
IMAGE_BACKEND_REPLICATE = "replicate"
|
||||
DEFAULT_IMAGE_BACKEND = IMAGE_BACKEND_LOCAL
|
||||
@@ -810,7 +844,7 @@ class Api:
|
||||
# oder als Startscript mitgibst!
|
||||
cmd = [
|
||||
"/Applications/Blender.app/Contents/MacOS/Blender",
|
||||
"--python", "scene_setup.py",
|
||||
"--python", resource_path("scene_setup.py"),
|
||||
"--", glb_path, hdri_path or ""
|
||||
]
|
||||
try:
|
||||
@@ -824,7 +858,7 @@ class Api:
|
||||
def edit_external(self, filepath: str):
|
||||
# filepath kommt von 2D als "output/foo.png" oder von 3D als "output/foo.png"
|
||||
ext = os.path.splitext(filepath)[1].lower()
|
||||
full_path = os.path.join(BASE_DIR, "web", filepath)
|
||||
full_path = os.path.join(WEB_DIR, filepath)
|
||||
# Ist es ein Bild oder das PNG eines 3D-Modells?
|
||||
if ext == ".png":
|
||||
# Prüfe, ob ein GLB daneben liegt
|
||||
@@ -852,7 +886,7 @@ class Api:
|
||||
# ------------- Generate 3D Model -------------
|
||||
def generate_3d_model(self, filepath: str):
|
||||
# filepath kommt aus JS als "output/flower-1.png"
|
||||
full_path = os.path.join(BASE_DIR, "web", filepath)
|
||||
full_path = os.path.join(WEB_DIR, filepath)
|
||||
if not os.path.isfile(full_path):
|
||||
self._js("window.on_error", f"Image not found: {full_path}")
|
||||
return {"status": "missing_file"}
|
||||
@@ -874,33 +908,8 @@ class Api:
|
||||
)
|
||||
return
|
||||
|
||||
# Construct the command: sys.executable ensures we run with same Python interpreter
|
||||
script_path = os.path.join(BASE_DIR, "image_to_3d.py")
|
||||
cmd = [sys.executable, script_path, img_path]
|
||||
base, _ = os.path.splitext(img_path)
|
||||
glb_path = base + ".glb"
|
||||
previous_mtime = os.path.getmtime(glb_path) if os.path.isfile(glb_path) else None
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=self._subprocess_env(),
|
||||
)
|
||||
|
||||
# Check for success: sidecar or actual .glb in same folder
|
||||
current_mtime = os.path.getmtime(glb_path) if os.path.isfile(glb_path) else None
|
||||
|
||||
if current_mtime is not None and current_mtime != previous_mtime:
|
||||
# Notify JavaScript
|
||||
self._js("window.on_3d_generated", glb_path)
|
||||
else:
|
||||
# If the script failed or no .glb was produced, send an error
|
||||
err_msg = (
|
||||
result.stderr.strip()
|
||||
or result.stdout.strip()
|
||||
or "3D conversion did not produce .glb"
|
||||
)
|
||||
self._js("window.on_error", f"3D Model failed: {err_msg}")
|
||||
glb_path = self._run_generate_3d_and_return(img_path)
|
||||
self._js("window.on_3d_generated", glb_path)
|
||||
except Exception as e:
|
||||
self._js("window.on_error", f"3D conversion exception: {e}")
|
||||
|
||||
@@ -915,7 +924,7 @@ class Api:
|
||||
Called when user wants both 3D model and equirectangular HDRI for an image.
|
||||
Kicks off both processes, waits for both, and (later) calls the next step.
|
||||
"""
|
||||
full_path = os.path.join(BASE_DIR, "web", filepath)
|
||||
full_path = os.path.join(WEB_DIR, filepath)
|
||||
if not os.path.isfile(full_path):
|
||||
self._js("window.on_error", f"Image not found: {full_path}")
|
||||
return {"status": "missing_file"}
|
||||
@@ -1041,45 +1050,32 @@ Do NOT mention the object itself. Describe the environment in a concise way, e.g
|
||||
if not self._get_replicate_api_token():
|
||||
raise RuntimeError("Replicate API token missing. Open settings and add it.")
|
||||
|
||||
script_path = os.path.join(BASE_DIR, "image_to_3d.py")
|
||||
cmd = [sys.executable, script_path, img_path]
|
||||
base, _ = os.path.splitext(img_path)
|
||||
glb_path = base + ".glb"
|
||||
previous_mtime = os.path.getmtime(glb_path) if os.path.isfile(glb_path) else None
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=self._subprocess_env(),
|
||||
)
|
||||
current_mtime = os.path.getmtime(glb_path) if os.path.isfile(glb_path) else None
|
||||
if current_mtime is not None and current_mtime != previous_mtime:
|
||||
self._js("window.on_3d_generated", glb_path)
|
||||
return glb_path
|
||||
else:
|
||||
err_msg = (
|
||||
result.stderr.strip()
|
||||
or result.stdout.strip()
|
||||
or "3D conversion did not produce .glb"
|
||||
)
|
||||
raise RuntimeError(err_msg)
|
||||
import image_to_3d
|
||||
|
||||
result = image_to_3d.process_image(img_path, self._get_replicate_api_token())
|
||||
if result and os.path.isfile(result):
|
||||
return result
|
||||
|
||||
raise RuntimeError("3D conversion did not produce .glb")
|
||||
|
||||
def _run_equirect_map(self, prompt: str, output_path: str):
|
||||
"""
|
||||
Calls your equi map generator script.
|
||||
Logs command and full output for debugging.
|
||||
"""
|
||||
script_path = os.path.join(BASE_DIR, "generate_equirect.py")
|
||||
cmd = [sys.executable, script_path, "--prompt", prompt, "--output", output_path]
|
||||
print(f"[HDRI CMD] {' '.join(cmd)}")
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
print(f"[HDRI STDOUT]\n{result.stdout}")
|
||||
print(f"[HDRI STDERR]\n{result.stderr}")
|
||||
if result.returncode == 0 and os.path.isfile(output_path):
|
||||
import generate_equirect
|
||||
|
||||
print(f"[HDRI] Generating to: {output_path}")
|
||||
result = generate_equirect.generate_equirect(
|
||||
prompt,
|
||||
output_path,
|
||||
work_dir=APP_SUPPORT_DIR if IS_FROZEN else BASE_DIR,
|
||||
)
|
||||
if result and os.path.isfile(output_path):
|
||||
print(f"[HDRI OK] Created: {output_path}")
|
||||
return output_path
|
||||
else:
|
||||
raise RuntimeError(result.stderr.strip() or f"HDRI generation failed for {output_path}")
|
||||
raise RuntimeError(f"HDRI generation failed for {output_path}")
|
||||
|
||||
def _on_3d_and_hdri_ready(self, glb_path: str, hdri_path: str):
|
||||
"""
|
||||
@@ -1168,7 +1164,7 @@ Do NOT mention the object itself. Describe the environment in a concise way, e.g
|
||||
|
||||
def get_image_json(self, filepath: str):
|
||||
# filepath kommt aus JS als "output/flower-1.png"
|
||||
full_path = os.path.join(BASE_DIR, "web", filepath)
|
||||
full_path = os.path.join(WEB_DIR, filepath)
|
||||
try:
|
||||
# Always prefer the sidecar if available (contains most up-to-date data)
|
||||
sidecar = full_path + ".json"
|
||||
@@ -1230,7 +1226,7 @@ if __name__ == "__main__":
|
||||
api = Api()
|
||||
window = webview.create_window(
|
||||
"SD 3D Model Image Gen",
|
||||
"web/index.html",
|
||||
WEB_INDEX,
|
||||
js_api=api,
|
||||
width=900,
|
||||
height=1000,
|
||||
@@ -1238,4 +1234,4 @@ if __name__ == "__main__":
|
||||
resizable=True
|
||||
)
|
||||
api.set_window(window)
|
||||
webview.start(debug=True)
|
||||
webview.start(debug=not IS_FROZEN, icon=APP_ICON_PATH if os.path.isfile(APP_ICON_PATH) else None)
|
||||
|
||||
Reference in New Issue
Block a user