Spaces:
Runtime error
Runtime error
| """ | |
| Skybox generator: text β 2:1 equirectangular image (Stable Diffusion, local). | |
| Uses FP16 to reduce VRAM. Output 1024x512 or 2048x1024. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import time | |
| from pathlib import Path | |
| from typing import Callable | |
| import torch | |
| # Default: v1.5 works without license acceptance. Use SD_MODEL_ID to prefer SD 2.1. | |
| DEFAULT_MODEL_ID = "runwayml/stable-diffusion-v1-5" | |
| FALLBACK_MODEL_ID = "runwayml/stable-diffusion-v1-5" # Same; alternate if primary fails | |
| def get_device() -> str: | |
| return "cuda" if torch.cuda.is_available() else "cpu" | |
| def _is_complete_sd_dir(path: Path) -> bool: | |
| """True if path looks like a complete Stable Diffusion pipeline (has unet weights).""" | |
| if not path.is_dir(): | |
| return False | |
| unet = path / "unet" | |
| if not unet.is_dir(): | |
| return False | |
| return any( | |
| (unet / f).exists() | |
| for f in ("diffusion_pytorch_model.safetensors", "diffusion_pytorch_model.bin") | |
| ) | |
| def _default_local_weights_dir() -> str | None: | |
| """First complete SD folder under weights/ (sd-v1-5 or stable-diffusion-2-1-base).""" | |
| try: | |
| root = Path(__file__).resolve().parent.parent | |
| for name in ("sd-v1-5", "stable-diffusion-2-1-base"): | |
| local = root / "weights" / name | |
| if _is_complete_sd_dir(local): | |
| return str(local) | |
| return None | |
| except Exception: | |
| return None | |
| def _get_hf_token(): | |
| """Token for Hugging Face Hub. On Spaces, set HF_TOKEN in Settings β Variables and secrets.""" | |
| token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN") | |
| if token: | |
| return token | |
| try: | |
| from huggingface_hub import get_token | |
| return get_token() | |
| except Exception: | |
| return None | |
| # Message shown when Hub returns 403 (missing/invalid token or gated model). | |
| HF_403_MESSAGE = ( | |
| "403 Forbidden from Hugging Face Hub. " | |
| "Add HF_TOKEN in this Space: Settings β Variables and secrets β New secret: HF_TOKEN (get a token at huggingface.co/settings/tokens, read access). " | |
| "If the model is gated, accept its license on the model page first, then restart the Space." | |
| ) | |
| def _raise_if_403(err: Exception) -> None: | |
| """Re-raise with a clear message if the error is a 403 from the Hub.""" | |
| if getattr(err, "response", None) is not None: | |
| status = getattr(err.response, "status_code", None) | |
| if status == 403: | |
| raise RuntimeError(HF_403_MESSAGE) from err | |
| if "403" in str(err).lower() or "forbidden" in str(err).lower(): | |
| raise RuntimeError(HF_403_MESSAGE) from err | |
| def _resolve_model_path_and_token(): | |
| """Use local path if set or default weights/ folder exists, else Hub id. Token from HF_TOKEN or huggingface_hub.""" | |
| local = os.environ.get("SD_MODEL_PATH", "").strip() | |
| if local and os.path.isdir(local): | |
| return local, None | |
| default_local = _default_local_weights_dir() | |
| if default_local: | |
| return default_local, None | |
| model_id = os.environ.get("SD_MODEL_ID", DEFAULT_MODEL_ID) | |
| token = _get_hf_token() | |
| return model_id, token or True | |
| def generate_skybox( | |
| prompt: str, | |
| output_dir: str = "outputs", | |
| width: int = 1024, | |
| height: int = 512, | |
| seed: int | None = None, | |
| model_id: str | None = None, | |
| progress_callback: Callable[[int, int], None] | None = None, | |
| ) -> tuple[str, float, float]: | |
| """ | |
| Generate a 2:1 equirectangular skybox image from a text prompt. | |
| progress_callback(step, total_steps) is called each denoising step if provided. | |
| Returns (path_to_image, inference_time_sec, peak_vram_mb). | |
| """ | |
| from diffusers import StableDiffusionPipeline | |
| device = get_device() | |
| dtype = torch.float16 if device == "cuda" else torch.float32 | |
| Path(output_dir).mkdir(parents=True, exist_ok=True) | |
| pretrained, token = _resolve_model_path_and_token() | |
| load_id = model_id or pretrained | |
| local_only = os.path.isdir(load_id) | |
| # Use explicit token only (no token=True) so we don't rely on get_token() which can be None in Docker/Space | |
| hub_token = token if token is not True else _get_hf_token() | |
| pipe = None | |
| last_error = None | |
| def _load(pid: str, local: bool) -> bool: | |
| nonlocal pipe, last_error | |
| try: | |
| pipe = StableDiffusionPipeline.from_pretrained( | |
| pid, | |
| torch_dtype=dtype, | |
| safety_checker=None, | |
| token=None if local else hub_token, | |
| local_files_only=local, | |
| ) | |
| return True | |
| except Exception as err: | |
| last_error = err | |
| _raise_if_403(err) | |
| return False | |
| if _load(load_id, local_only): | |
| pass | |
| elif not local_only and _load(FALLBACK_MODEL_ID, False): | |
| pass | |
| if pipe is None: | |
| err_msg = ( | |
| "Could not load Stable Diffusion. Need internet to download the model (first run).\n" | |
| " - On Hugging Face Spaces: add HF_TOKEN in Settings β Variables and secrets " | |
| "(create a token at huggingface.co/settings/tokens, read access is enough).\n" | |
| " - Locally: set HF_TOKEN=your_token or run: huggingface-cli download runwayml/stable-diffusion-v1-5 --local-dir ./weights/sd-v1-5" | |
| ) | |
| raise RuntimeError(err_msg) from last_error | |
| pipe = pipe.to(device) | |
| # Optional: enable xformers for lower VRAM (uncomment if installed) | |
| # if device == "cuda": | |
| # pipe.enable_xformers_memory_efficient_attention() | |
| if device == "cuda": | |
| torch.cuda.reset_peak_memory_stats() | |
| torch.cuda.synchronize() | |
| generator = None | |
| if seed is not None: | |
| generator = torch.Generator(device=device).manual_seed(seed) | |
| num_inference_steps = 50 | |
| def _callback(step_idx: int, t, latents): | |
| if progress_callback is not None: | |
| step = min(step_idx + 1, num_inference_steps) | |
| progress_callback(step, num_inference_steps) | |
| t0 = time.perf_counter() | |
| image = pipe( | |
| prompt=prompt, | |
| width=width, | |
| height=height, | |
| num_inference_steps=num_inference_steps, | |
| generator=generator, | |
| callback=_callback if progress_callback else None, | |
| callback_steps=1 if progress_callback else None, | |
| ).images[0] | |
| if device == "cuda": | |
| torch.cuda.synchronize() | |
| t1 = time.perf_counter() | |
| inference_time = t1 - t0 | |
| peak_vram_mb = ( | |
| torch.cuda.max_memory_allocated() / 1024 / 1024 | |
| if device == "cuda" | |
| else 0.0 | |
| ) | |
| # Save with safe filename | |
| safe_name = "".join(c if c.isalnum() or c in " -_" else "_" for c in prompt)[:60] | |
| out_path = os.path.join(output_dir, f"skybox_{safe_name.strip()}.png") | |
| image.save(out_path) | |
| return out_path, inference_time, peak_vram_mb | |