Spaces:
Sleeping
Sleeping
changed to gradio
Browse files- README.md +23 -5
- app.py +85 -0
- generate.py +6 -3
- requirements.txt +1 -1
README.md
CHANGED
|
@@ -3,14 +3,15 @@ title: Diffusion
|
|
| 3 |
emoji: 🌍
|
| 4 |
colorFrom: indigo
|
| 5 |
colorTo: purple
|
| 6 |
-
sdk:
|
| 7 |
-
|
|
|
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
# Diffusion Lab
|
| 12 |
|
| 13 |
-
The reverse process runs the pretrained `google/ddpm-celebahq-256` model through Hugging Face Diffusers. This is an unconditional CelebA-HQ face generator: reverse sampling starts with random Gaussian noise
|
| 14 |
|
| 15 |
## Run
|
| 16 |
|
|
@@ -20,10 +21,10 @@ Use Python 3.10 or newer. Install a PyTorch build appropriate for your machine f
|
|
| 20 |
python -m venv .venv
|
| 21 |
source .venv/bin/activate
|
| 22 |
python -m pip install -r requirements.txt
|
| 23 |
-
python
|
| 24 |
```
|
| 25 |
|
| 26 |
-
Open <http://127.0.0.1:
|
| 27 |
|
| 28 |
To generate and display one image without starting the server, run:
|
| 29 |
|
|
@@ -38,3 +39,20 @@ To download a particular zero-based row from the public CelebA-HQ dataset:
|
|
| 38 |
python download_celeba_hq.py 1374
|
| 39 |
python download_celeba_hq.py 42 --split train --output face-42.png
|
| 40 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
emoji: 🌍
|
| 4 |
colorFrom: indigo
|
| 5 |
colorTo: purple
|
| 6 |
+
sdk: gradio
|
| 7 |
+
app_file: app.py
|
| 8 |
+
python_version: "3.10"
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
# Diffusion Lab
|
| 13 |
|
| 14 |
+
The Gradio app supports ZeroGPU and streams checkpoint images. The reverse process runs the pretrained `google/ddpm-celebahq-256` model through Hugging Face Diffusers. This is an unconditional CelebA-HQ face generator: reverse sampling normally starts with random Gaussian noise. You can also select the final forward-noise image as its input; this is a demonstration, not a guaranteed reconstruction of the uploaded image.
|
| 15 |
|
| 16 |
## Run
|
| 17 |
|
|
|
|
| 21 |
python -m venv .venv
|
| 22 |
source .venv/bin/activate
|
| 23 |
python -m pip install -r requirements.txt
|
| 24 |
+
python app.py
|
| 25 |
```
|
| 26 |
|
| 27 |
+
Open <http://127.0.0.1:7860>. Startup downloads the model weights into `models/`; later runs use that cache. The backend prefers CUDA, then Apple Silicon MPS, then CPU. Model inference uses all 1,000 DDPM steps and sends the selected intermediate images to the browser as they are produced.
|
| 28 |
|
| 29 |
To generate and display one image without starting the server, run:
|
| 30 |
|
|
|
|
| 39 |
python download_celeba_hq.py 1374
|
| 40 |
python download_celeba_hq.py 42 --split train --output face-42.png
|
| 41 |
```
|
| 42 |
+
|
| 43 |
+
## ZeroGPU deployment
|
| 44 |
+
|
| 45 |
+
Push this repository to a Gradio Space and select ZeroGPU in the Space hardware settings. The Dockerfile and legacy server are not used by the Gradio deployment. Account eligibility and daily GPU quotas apply.
|
| 46 |
+
|
| 47 |
+
The app imports `spaces` before PyTorch, loads the model at startup, and places it on CUDA when `SPACES_ZERO_GPU` is enabled. GPU sampling runs inside a decorated Gradio handler. `GPU_DURATION_SECONDS` defaults to 120; benchmark a full 1,000-step run and adjust this Space variable if necessary. A timeout is reported by the Gradio queue; selecting fewer checkpoints does not reduce the sampling steps.
|
| 48 |
+
|
| 49 |
+
For persistent caching, attach a writable storage bucket and set the Space variable `MODEL_CACHE_DIR` to its mount path (for example `/data/models`). The old Docker path `/app/models` is also usable if explicitly configured with that variable.
|
| 50 |
+
|
| 51 |
+
For local use, install `gradio` and `spaces` alongside `requirements.txt` (these packages are provided by the Gradio Spaces runtime):
|
| 52 |
+
|
| 53 |
+
```sh
|
| 54 |
+
python -m pip install gradio spaces
|
| 55 |
+
python app.py
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
The original browser interface remains available with `python server.py` for local or dedicated-GPU use.
|
app.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Gradio entry point for local use and Hugging Face ZeroGPU."""
|
| 2 |
+
import spaces # Must initialize ZeroGPU before importing torch/diffusers.
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
import secrets
|
| 6 |
+
|
| 7 |
+
import gradio as gr
|
| 8 |
+
import numpy as np
|
| 9 |
+
from PIL import Image
|
| 10 |
+
|
| 11 |
+
from generate import CHECKPOINTS, ROOT, generate_images, load_model
|
| 12 |
+
|
| 13 |
+
ZERO_GPU = os.environ.get("SPACES_ZERO_GPU", "").lower() in {"1", "true", "yes"}
|
| 14 |
+
load_model(device="cuda" if ZERO_GPU else None)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def parse_settings(seed, checkpoint_text):
|
| 18 |
+
try:
|
| 19 |
+
seed = int(str(seed).strip())
|
| 20 |
+
checkpoints = sorted(set(int(part.strip()) for part in checkpoint_text.split(",")))
|
| 21 |
+
except (ValueError, TypeError):
|
| 22 |
+
raise gr.Error("Enter an integer seed and comma-separated integer checkpoints.")
|
| 23 |
+
if not 0 <= seed <= 2147483647:
|
| 24 |
+
raise gr.Error("Seed must be between 0 and 2147483647.")
|
| 25 |
+
if not 2 <= len(checkpoints) <= 12 or checkpoints[0] != 0 or checkpoints[-1] != 1000:
|
| 26 |
+
raise gr.Error("Use 2–12 checkpoints between 0 and 1000, including both endpoints.")
|
| 27 |
+
return seed, checkpoints
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def diffuse(image, seed, checkpoint_text):
|
| 31 |
+
seed, checkpoints = parse_settings(seed, checkpoint_text)
|
| 32 |
+
if image is None:
|
| 33 |
+
raise gr.Error("Upload an image first.")
|
| 34 |
+
pixels = np.asarray(image.convert("RGB").resize((256, 256)), dtype=np.float32) / 127.5 - 1
|
| 35 |
+
noise = np.random.default_rng(seed).standard_normal(pixels.shape)
|
| 36 |
+
alpha_bars = np.cumprod(1 - np.linspace(0.0001, 0.02, 1000))
|
| 37 |
+
frames = []
|
| 38 |
+
final_noise = None
|
| 39 |
+
for step in checkpoints:
|
| 40 |
+
alpha = 1 if step == 0 else alpha_bars[step - 1]
|
| 41 |
+
sample = np.sqrt(alpha) * pixels + np.sqrt(1 - alpha) * noise
|
| 42 |
+
frame = Image.fromarray(np.round((sample.clip(-1, 1) + 1) * 127.5).astype(np.uint8))
|
| 43 |
+
frames.append((frame, f"t = {step}"))
|
| 44 |
+
final_noise = frame
|
| 45 |
+
return frames, final_noise
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@spaces.GPU(duration=int(os.environ.get("GPU_DURATION_SECONDS", "120")))
|
| 49 |
+
def denoise(seed, checkpoint_text, input_mode, forward_noise):
|
| 50 |
+
seed, checkpoints = parse_settings(seed, checkpoint_text)
|
| 51 |
+
initial_image = forward_noise if input_mode == "Forward noise" else None
|
| 52 |
+
if input_mode == "Forward noise" and initial_image is None:
|
| 53 |
+
raise gr.Error("Run Diffuse first to create forward noise.")
|
| 54 |
+
frames = []
|
| 55 |
+
for step, frame in generate_images(seed, checkpoints, initial_image):
|
| 56 |
+
if frame is not None:
|
| 57 |
+
frames.append((frame, f"t = {step}"))
|
| 58 |
+
if frame is not None or step % 25 == 0:
|
| 59 |
+
yield list(frames), f"Denoising: {1000 - step}/1000 steps · seed {seed}"
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
with gr.Blocks(title="Diffusion Lab") as demo:
|
| 63 |
+
gr.Markdown("# Diffusion Lab\nExplore forward noising and the 1,000-step DDPM reverse process.")
|
| 64 |
+
with gr.Row():
|
| 65 |
+
seed = gr.Textbox(value="42", label="Seed")
|
| 66 |
+
randomize = gr.Button("Randomize seed")
|
| 67 |
+
checkpoints = gr.Textbox(value=", ".join(map(str, CHECKPOINTS)), label="Checkpoint timesteps")
|
| 68 |
+
randomize.click(lambda: str(secrets.randbelow(2**31)), outputs=seed, queue=False)
|
| 69 |
+
default_image = ROOT / "assets/celeba_hq_validation_91_256.png"
|
| 70 |
+
source = gr.Image(value=str(default_image) if default_image.exists() else None,
|
| 71 |
+
type="pil", label="Source image")
|
| 72 |
+
diffuse_button = gr.Button("Diffuse")
|
| 73 |
+
forward_gallery = gr.Gallery(label="Forward process: image to noise", columns=4)
|
| 74 |
+
forward_noise = gr.State(None)
|
| 75 |
+
diffuse_button.click(diffuse, [source, seed, checkpoints], [forward_gallery, forward_noise])
|
| 76 |
+
input_mode = gr.Radio(["Gaussian noise", "Forward noise"], value="Gaussian noise", label="Reverse input")
|
| 77 |
+
gr.Markdown("The face model is unconditional. Forward noise is an experiment, not a guaranteed reconstruction.")
|
| 78 |
+
denoise_button = gr.Button("Denoise", variant="primary")
|
| 79 |
+
reverse_gallery = gr.Gallery(label="Reverse process: noise to face", columns=4)
|
| 80 |
+
status = gr.Textbox(label="Progress", interactive=False)
|
| 81 |
+
denoise_button.click(denoise, [seed, checkpoints, input_mode, forward_noise],
|
| 82 |
+
[reverse_gallery, status], concurrency_limit=1)
|
| 83 |
+
|
| 84 |
+
if __name__ == "__main__":
|
| 85 |
+
demo.queue().launch()
|
generate.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import argparse
|
|
|
|
| 2 |
import secrets
|
| 3 |
import threading
|
| 4 |
from pathlib import Path
|
|
@@ -16,7 +17,7 @@ _TORCH = None
|
|
| 16 |
_DEVICE = None
|
| 17 |
|
| 18 |
|
| 19 |
-
def load_model():
|
| 20 |
"""Load and cache the diffusion pipeline, returning it and its device."""
|
| 21 |
global _PIPELINE, _TORCH, _DEVICE
|
| 22 |
|
|
@@ -31,7 +32,9 @@ def load_model():
|
|
| 31 |
f"Import detail: {error}"
|
| 32 |
) from error
|
| 33 |
|
| 34 |
-
if
|
|
|
|
|
|
|
| 35 |
device = "cuda"
|
| 36 |
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
|
| 37 |
device = "mps"
|
|
@@ -39,7 +42,7 @@ def load_model():
|
|
| 39 |
device = "cpu"
|
| 40 |
|
| 41 |
print(f"Using device: {device}")
|
| 42 |
-
pipeline = DDPMPipeline.from_pretrained(MODEL_ID, cache_dir=ROOT / "models")
|
| 43 |
pipeline.to(device)
|
| 44 |
_TORCH = torch
|
| 45 |
_DEVICE = device
|
|
|
|
| 1 |
import argparse
|
| 2 |
+
import os
|
| 3 |
import secrets
|
| 4 |
import threading
|
| 5 |
from pathlib import Path
|
|
|
|
| 17 |
_DEVICE = None
|
| 18 |
|
| 19 |
|
| 20 |
+
def load_model(device=None):
|
| 21 |
"""Load and cache the diffusion pipeline, returning it and its device."""
|
| 22 |
global _PIPELINE, _TORCH, _DEVICE
|
| 23 |
|
|
|
|
| 32 |
f"Import detail: {error}"
|
| 33 |
) from error
|
| 34 |
|
| 35 |
+
if device is not None:
|
| 36 |
+
pass
|
| 37 |
+
elif torch.cuda.is_available():
|
| 38 |
device = "cuda"
|
| 39 |
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
|
| 40 |
device = "mps"
|
|
|
|
| 42 |
device = "cpu"
|
| 43 |
|
| 44 |
print(f"Using device: {device}")
|
| 45 |
+
pipeline = DDPMPipeline.from_pretrained(MODEL_ID, cache_dir=Path(os.environ.get("MODEL_CACHE_DIR", ROOT / "models")))
|
| 46 |
pipeline.to(device)
|
| 47 |
_TORCH = torch
|
| 48 |
_DEVICE = device
|
requirements.txt
CHANGED
|
@@ -2,4 +2,4 @@ diffusers>=0.30.0
|
|
| 2 |
matplotlib>=3.8.0
|
| 3 |
Pillow>=10.0.0
|
| 4 |
safetensors>=0.4.0
|
| 5 |
-
torch>=2.
|
|
|
|
| 2 |
matplotlib>=3.8.0
|
| 3 |
Pillow>=10.0.0
|
| 4 |
safetensors>=0.4.0
|
| 5 |
+
torch>=2.8.0
|