nanye commited on
Commit
785e85b
·
1 Parent(s): 033e505

- added support for google/ddpm-cifar10-32

Browse files

- click image to show it in its original resolution

Files changed (10) hide show
  1. .gitattributes +1 -0
  2. README.md +10 -3
  3. app.js +47 -9
  4. app.py +61 -33
  5. assets/cifar10_train_23_32.png +3 -0
  6. download_cifar10.py +83 -0
  7. generate.py +18 -13
  8. index.html +5 -2
  9. server.py +12 -8
  10. styles.css +2 -2
.gitattributes CHANGED
@@ -1 +1,2 @@
1
  assets/celeba_hq_validation_91_256.png filter=lfs diff=lfs merge=lfs -text
 
 
1
  assets/celeba_hq_validation_91_256.png filter=lfs diff=lfs merge=lfs -text
2
+ assets/cifar10_train_23_32.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -11,7 +11,7 @@ pinned: false
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
 
@@ -24,7 +24,7 @@ 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
 
@@ -40,11 +40,18 @@ 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 and places the GPU model on CUDA at startup when `SPACES_ZERO_GPU` is enabled. Reverse sampling tries GPU first and automatically restarts on CPU if GPU allocation, quota, or supported CUDA availability errors prevent the run. Locally, the app prefers CUDA, then Apple Silicon MPS, then CPU. MPS availability failures also fall back to CPU. Separate pipelines are cached for each device; the CPU pipeline loads on first use. CPU sampling is substantially slower. GPU sampling runs inside a decorated function called by the 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
 
 
11
 
12
  # Diffusion Lab
13
 
14
+ The Gradio app supports ZeroGPU and streams checkpoint images. The model selector offers `google/ddpm-celebahq-256` (256×256 faces, the default) and `google/ddpm-cifar10-32` (32×32 CIFAR-10 objects and animals) through Hugging Face Diffusers. Forward images and noise use the selected model’s resolution in Gradio. 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
 
 
24
  python app.py
25
  ```
26
 
27
+ Open <http://127.0.0.1:7860>. On first use, each selected model downloads its 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
 
 
40
  python download_celeba_hq.py 42 --split train --output face-42.png
41
  ```
42
 
43
+ To download a 32×32 CIFAR-10 image (test split by default):
44
+
45
+ ```sh
46
+ python download_cifar10.py 1374
47
+ python download_cifar10.py 42 --split train --output assets/cifar10-train-42.png
48
+ ```
49
+
50
  ## ZeroGPU deployment
51
 
52
  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.
53
 
54
+ The app imports `spaces` before PyTorch and places both GPU models on CUDA at startup when `SPACES_ZERO_GPU` is enabled. Reverse sampling tries GPU first and automatically restarts on CPU if GPU allocation, quota, or supported CUDA availability errors prevent the run. Locally, the app prefers CUDA, then Apple Silicon MPS, then CPU. MPS availability failures also fall back to CPU. Separate pipelines are cached for each model and device; the CPU pipeline loads on first use. CPU sampling is substantially slower. GPU sampling runs inside a decorated function called by the 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.
55
 
56
  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.
57
 
app.js CHANGED
@@ -1,5 +1,5 @@
1
  const TOTAL_STEPS = 1000;
2
- const MODEL_NAME = 'google/ddpm-celebahq-256';
3
  const DEFAULT_CHECKPOINTS = [0, 50, 100, 200, 350, 500, 700, 1000];
4
  const SIZE = 192;
5
  const PIXEL_COUNT = SIZE * SIZE * 3;
@@ -58,7 +58,7 @@ function computeForwardStates() {
58
  forwardStates = checkpoints.map((step) => { if (step === 0) return new Float32Array(sourcePixels); const alphaBar = schedule[step - 1]; const state = new Float32Array(PIXEL_COUNT); for (let index = 0; index < PIXEL_COUNT; index += 1) state[index] = Math.sqrt(alphaBar) * sourcePixels[index] + Math.sqrt(1 - alphaBar) * noise[index]; return state; });
59
  }
60
  function makeNoiseState(seed) { const random = randomNormal(seed); const state = new Float32Array(PIXEL_COUNT); for (let index = 0; index < PIXEL_COUNT; index += 1) state[index] = random(); return state; }
61
- function stateToPng(state) { const canvas = document.createElement('canvas'); canvas.width = 256; canvas.height = 256; const small = document.createElement('canvas'); small.width = SIZE; small.height = SIZE; drawState(small, state); canvas.getContext('2d').drawImage(small, 0, 0, 256, 256); return canvas.toDataURL('image/png'); }
62
  function setModelStatus(message) { if (elements.modelStatus) elements.modelStatus.textContent = message; }
63
  function resampleNoise() {
64
  const values = new Uint32Array(1); crypto.getRandomValues(values); const seed = values[0] & 0x7fffffff; elements.seedInput.value = String(seed); reverseInputMode = 'seed'; reverseNoiseState = makeNoiseState(seed); reverseStates = new Map(); resetProgress('reverse'); setModelStatus(`Resampled Gaussian noise · seed ${seed}.`); renderTracks();
@@ -81,12 +81,29 @@ function makeCard(step, state, type, index) {
81
  if (type === 'forward' && index === 0) { const change = document.createElement('button'); change.className = 'change-image'; change.type = 'button'; change.textContent = 'Change image'; change.addEventListener('click', () => elements.imageUpload.click()); canvasWrap.append(change); }
82
  if (type === 'reverse' && index === checkpoints.length - 1) { const noiseActions = document.createElement('div'); noiseActions.className = 'noise-actions'; noiseActions.append(makeActionButton('Resample', 'resample-button', resampleNoise), makeActionButton('Forward noise', 'forward-noise-button', useForwardNoise)); canvasWrap.append(noiseActions); }
83
  const caption = document.createElement('div'); caption.className = 'stage-caption';
84
- const titleText = type === 'forward' && index === 0 ? imageLabel : step === 0 ? 'Generated face' : step === TOTAL_STEPS ? 'Gaussian noise' : '';
85
  if (titleText) { const title = document.createElement('strong'); title.textContent = titleText; caption.append(title); }
86
  const time = document.createElement('span'); time.textContent = `t = ${step}`; caption.append(time);
87
- const preview = document.createElement('div'); preview.className = 'hover-preview'; const previewCanvas = document.createElement('canvas'); previewCanvas.width = 256; previewCanvas.height = 256; if (state) { const context = previewCanvas.getContext('2d'); context.imageSmoothingEnabled = true; if (state instanceof HTMLCanvasElement) context.drawImage(state, 0, 0, 256, 256); else { const temporary = document.createElement('canvas'); temporary.width = SIZE; temporary.height = SIZE; drawState(temporary, state); context.drawImage(temporary, 0, 0, 256, 256); } } preview.append(previewCanvas);
88
- canvas.addEventListener('mouseenter', () => { const rect = canvasWrap.getBoundingClientRect(); preview.style.left = `${Math.max(8, Math.min(window.innerWidth - 264, rect.left + rect.width / 2 - 128))}px`; preview.style.top = `${Math.max(8, Math.min(window.innerHeight - 264, rect.top + rect.height / 2 - 128))}px`; preview.classList.add('is-visible'); });
89
- canvas.addEventListener('mouseleave', () => preview.classList.remove('is-visible'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  card.append(actionSlot, canvasWrap, caption, preview); return card;
91
  }
92
  function renderTracks() {
@@ -94,6 +111,7 @@ function renderTracks() {
94
  elements.denoisingTrack.style.setProperty('--stage-count', checkpoints.length);
95
  elements.diffusionTrack.replaceChildren(...checkpoints.map((step, index) => makeCard(step, forwardStates[index], 'forward', index)));
96
  elements.denoisingTrack.replaceChildren(...checkpoints.map((step, index) => makeCard(step, reverseStates.get(step) || (step === TOTAL_STEPS ? reverseNoiseState : null), 'reverse', index)));
 
97
  const button = document.querySelector('#denoise-button'); if (button) { button.disabled = generationBusy; button.textContent = generationBusy ? 'Denoising…' : 'Denoise'; }
98
  const forwardNoiseButton = document.querySelector('#forward-noise-button'); if (forwardNoiseButton) { forwardNoiseButton.disabled = generationBusy || !forwardCompleted; forwardNoiseButton.title = forwardCompleted ? 'Use the last forward-process image' : 'Run Diffuse first'; }
99
  const resampleButton = document.querySelector('#resample-button'); if (resampleButton) resampleButton.disabled = generationBusy;
@@ -144,7 +162,7 @@ async function generateReverse() {
144
  stopAnimation(); generationController = new AbortController(); generationBusy = true; reverseStates = new Map(); resetProgress('reverse'); setModelStatus('Loading model weights if needed, then sampling 1,000 reverse steps…'); renderTracks();
145
  try {
146
  const initialImage = reverseInputMode === 'forward' ? stateToPng(reverseNoiseState) : null;
147
- const response = await fetch('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ checkpoints, seed, initial_image: initialImage }), signal: generationController.signal }); if (!response.ok || !response.body) throw new Error(`Backend request failed (${response.status}).`);
148
  const reader = response.body.getReader(); const decoder = new TextDecoder(); let pending = '';
149
  while (true) { const { value, done } = await reader.read(); pending += decoder.decode(value || new Uint8Array(), { stream: !done }); const lines = pending.split('\n'); pending = lines.pop(); for (const line of lines) if (line.trim()) await handleModelEvent(JSON.parse(line)); if (done) break; }
150
  if (pending.trim()) await handleModelEvent(JSON.parse(pending));
@@ -155,9 +173,20 @@ async function handleModelEvent(event) {
155
  if (event.type === 'status') setModelStatus(`Loading ${event.model} on the first generation…`);
156
  if (event.type === 'progress') { const completed = event.total - event.step; if (progressStartedAt.reverse === null) beginProgress('reverse'); setProgress('reverse', completed); setModelStatus(`Denoising · ${completed}/${event.total} steps complete.`); }
157
  if (event.type === 'frame') { reverseStates.set(event.step, await decodeFrame(event.image)); setProgress('reverse', TOTAL_STEPS - event.step); setModelStatus(`Real model inference · received t = ${event.step}.`); renderTracks(); const card = elements.denoisingTrack.querySelector(`[data-step="${event.step}"]`); if (card) { document.querySelectorAll('#denoising-track .is-active').forEach((item) => item.classList.remove('is-active')); card.classList.add('is-active'); } }
158
- if (event.type === 'complete') { setProgress('reverse', TOTAL_STEPS); setModelStatus(`Generated with ${MODEL_NAME} on ${event.device.toUpperCase()} · seed ${event.seed}.`); } if (event.type === 'error') throw new Error(event.message);
159
  }
160
  function loadImage(image, label) { const canvas = document.createElement('canvas'); canvas.width = SIZE; canvas.height = SIZE; const context = canvas.getContext('2d'); const scale = Math.max(SIZE / image.width, SIZE / image.height); const width = image.width * scale; const height = image.height * scale; context.drawImage(image, (SIZE - width) / 2, (SIZE - height) / 2, width, height); sourcePixels = canvasPixels(canvas); imageLabel = label; reverseInputMode = 'seed'; reverseNoiseState = makeNoiseState(readSeed()); forwardRevealCount = 1; forwardCompleted = false; resetProgress('forward'); computeForwardStates(); renderTracks(); }
 
 
 
 
 
 
 
 
 
 
 
161
  elements.applyTimesteps.addEventListener('click', applyTimesteps); elements.timestepsInput.addEventListener('keydown', (event) => { if (event.key === 'Enter') applyTimesteps(); });
162
  elements.randomizeSeed.addEventListener('click', () => { resampleNoise(); elements.configError.textContent = ''; forwardRevealCount = 1; forwardCompleted = false; resetProgress('forward'); computeForwardStates(); renderTracks(); });
163
  elements.imageUpload.addEventListener('change', (event) => { const file = event.target.files?.[0]; if (!file) return; const image = new Image(); image.onload = () => { loadImage(image, file.name.replace(/\.[^.]+$/, '')); URL.revokeObjectURL(image.src); }; image.src = URL.createObjectURL(file); });
@@ -165,5 +194,14 @@ elements.imageUpload.addEventListener('change', (event) => { const file = event.
165
  // and as a fallback if the image asset cannot be read.
166
  sourcePixels = canvasPixels(makeSourceArt()); computeForwardStates(); reverseNoiseState = makeNoiseState(readSeed()); resetProgress('forward'); resetProgress('reverse'); renderTracks();
167
  const defaultSourceImage = new Image();
168
- defaultSourceImage.onload = () => loadImage(defaultSourceImage, 'Human face');
 
 
169
  defaultSourceImage.src = 'assets/celeba_hq_validation_91_256.png';
 
 
 
 
 
 
 
 
1
  const TOTAL_STEPS = 1000;
2
+ const modelInput = document.querySelector('#model-input');
3
  const DEFAULT_CHECKPOINTS = [0, 50, 100, 200, 350, 500, 700, 1000];
4
  const SIZE = 192;
5
  const PIXEL_COUNT = SIZE * SIZE * 3;
 
58
  forwardStates = checkpoints.map((step) => { if (step === 0) return new Float32Array(sourcePixels); const alphaBar = schedule[step - 1]; const state = new Float32Array(PIXEL_COUNT); for (let index = 0; index < PIXEL_COUNT; index += 1) state[index] = Math.sqrt(alphaBar) * sourcePixels[index] + Math.sqrt(1 - alphaBar) * noise[index]; return state; });
59
  }
60
  function makeNoiseState(seed) { const random = randomNormal(seed); const state = new Float32Array(PIXEL_COUNT); for (let index = 0; index < PIXEL_COUNT; index += 1) state[index] = random(); return state; }
61
+ function stateToPng(state) { const canvas = document.createElement('canvas'); const size = modelInput.value === 'google/ddpm-cifar10-32' ? 32 : 256; canvas.width = size; canvas.height = size; const small = document.createElement('canvas'); small.width = SIZE; small.height = SIZE; drawState(small, state); canvas.getContext('2d').drawImage(small, 0, 0, size, size); return canvas.toDataURL('image/png'); }
62
  function setModelStatus(message) { if (elements.modelStatus) elements.modelStatus.textContent = message; }
63
  function resampleNoise() {
64
  const values = new Uint32Array(1); crypto.getRandomValues(values); const seed = values[0] & 0x7fffffff; elements.seedInput.value = String(seed); reverseInputMode = 'seed'; reverseNoiseState = makeNoiseState(seed); reverseStates = new Map(); resetProgress('reverse'); setModelStatus(`Resampled Gaussian noise · seed ${seed}.`); renderTracks();
 
81
  if (type === 'forward' && index === 0) { const change = document.createElement('button'); change.className = 'change-image'; change.type = 'button'; change.textContent = 'Change image'; change.addEventListener('click', () => elements.imageUpload.click()); canvasWrap.append(change); }
82
  if (type === 'reverse' && index === checkpoints.length - 1) { const noiseActions = document.createElement('div'); noiseActions.className = 'noise-actions'; noiseActions.append(makeActionButton('Resample', 'resample-button', resampleNoise), makeActionButton('Forward noise', 'forward-noise-button', useForwardNoise)); canvasWrap.append(noiseActions); }
83
  const caption = document.createElement('div'); caption.className = 'stage-caption';
84
+ const titleText = type === 'forward' && index === 0 ? imageLabel : step === 0 ? 'Generated image' : step === TOTAL_STEPS ? 'Gaussian noise' : '';
85
  if (titleText) { const title = document.createElement('strong'); title.textContent = titleText; caption.append(title); }
86
  const time = document.createElement('span'); time.textContent = `t = ${step}`; caption.append(time);
87
+ const preview = document.createElement('div'); preview.className = 'hover-preview';
88
+ const previewCanvas = document.createElement('canvas');
89
+ const nativeSize = modelInput.value === 'google/ddpm-cifar10-32' ? 32 : 256;
90
+ previewCanvas.width = state instanceof HTMLCanvasElement ? state.width : nativeSize;
91
+ previewCanvas.height = state instanceof HTMLCanvasElement ? state.height : nativeSize;
92
+ preview.style.width = `${previewCanvas.width + 4}px`; preview.style.height = `${previewCanvas.height + 4}px`;
93
+ previewCanvas.style.width = `${previewCanvas.width}px`; previewCanvas.style.height = `${previewCanvas.height}px`;
94
+ if (state) {
95
+ const context = previewCanvas.getContext('2d');
96
+ if (state instanceof HTMLCanvasElement) context.drawImage(state, 0, 0);
97
+ else { const temporary = document.createElement('canvas'); temporary.width = SIZE; temporary.height = SIZE; drawState(temporary, state); context.drawImage(temporary, 0, 0, previewCanvas.width, previewCanvas.height); }
98
+ }
99
+ preview.append(previewCanvas);
100
+ canvas.addEventListener('click', () => {
101
+ document.querySelectorAll('.hover-preview.is-visible').forEach(item => item.classList.remove('is-visible'));
102
+ const rect = canvasWrap.getBoundingClientRect();
103
+ preview.style.left = `${Math.max(8, Math.min(window.innerWidth - previewCanvas.width - 12, rect.left + rect.width / 2 - previewCanvas.width / 2))}px`;
104
+ preview.style.top = `${Math.max(8, Math.min(window.innerHeight - previewCanvas.height - 12, rect.top + rect.height / 2 - previewCanvas.height / 2))}px`;
105
+ preview.classList.add('is-visible');
106
+ });
107
  card.append(actionSlot, canvasWrap, caption, preview); return card;
108
  }
109
  function renderTracks() {
 
111
  elements.denoisingTrack.style.setProperty('--stage-count', checkpoints.length);
112
  elements.diffusionTrack.replaceChildren(...checkpoints.map((step, index) => makeCard(step, forwardStates[index], 'forward', index)));
113
  elements.denoisingTrack.replaceChildren(...checkpoints.map((step, index) => makeCard(step, reverseStates.get(step) || (step === TOTAL_STEPS ? reverseNoiseState : null), 'reverse', index)));
114
+ modelInput.disabled = generationBusy;
115
  const button = document.querySelector('#denoise-button'); if (button) { button.disabled = generationBusy; button.textContent = generationBusy ? 'Denoising…' : 'Denoise'; }
116
  const forwardNoiseButton = document.querySelector('#forward-noise-button'); if (forwardNoiseButton) { forwardNoiseButton.disabled = generationBusy || !forwardCompleted; forwardNoiseButton.title = forwardCompleted ? 'Use the last forward-process image' : 'Run Diffuse first'; }
117
  const resampleButton = document.querySelector('#resample-button'); if (resampleButton) resampleButton.disabled = generationBusy;
 
162
  stopAnimation(); generationController = new AbortController(); generationBusy = true; reverseStates = new Map(); resetProgress('reverse'); setModelStatus('Loading model weights if needed, then sampling 1,000 reverse steps…'); renderTracks();
163
  try {
164
  const initialImage = reverseInputMode === 'forward' ? stateToPng(reverseNoiseState) : null;
165
+ const response = await fetch('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: modelInput.value, checkpoints, seed, initial_image: initialImage }), signal: generationController.signal }); if (!response.ok || !response.body) throw new Error(`Backend request failed (${response.status}).`);
166
  const reader = response.body.getReader(); const decoder = new TextDecoder(); let pending = '';
167
  while (true) { const { value, done } = await reader.read(); pending += decoder.decode(value || new Uint8Array(), { stream: !done }); const lines = pending.split('\n'); pending = lines.pop(); for (const line of lines) if (line.trim()) await handleModelEvent(JSON.parse(line)); if (done) break; }
168
  if (pending.trim()) await handleModelEvent(JSON.parse(pending));
 
173
  if (event.type === 'status') setModelStatus(`Loading ${event.model} on the first generation…`);
174
  if (event.type === 'progress') { const completed = event.total - event.step; if (progressStartedAt.reverse === null) beginProgress('reverse'); setProgress('reverse', completed); setModelStatus(`Denoising · ${completed}/${event.total} steps complete.`); }
175
  if (event.type === 'frame') { reverseStates.set(event.step, await decodeFrame(event.image)); setProgress('reverse', TOTAL_STEPS - event.step); setModelStatus(`Real model inference · received t = ${event.step}.`); renderTracks(); const card = elements.denoisingTrack.querySelector(`[data-step="${event.step}"]`); if (card) { document.querySelectorAll('#denoising-track .is-active').forEach((item) => item.classList.remove('is-active')); card.classList.add('is-active'); } }
176
+ if (event.type === 'complete') { setProgress('reverse', TOTAL_STEPS); setModelStatus(`Generated with ${event.model || modelInput.value} on ${event.device.toUpperCase()} · seed ${event.seed}.`); } if (event.type === 'error') throw new Error(event.message);
177
  }
178
  function loadImage(image, label) { const canvas = document.createElement('canvas'); canvas.width = SIZE; canvas.height = SIZE; const context = canvas.getContext('2d'); const scale = Math.max(SIZE / image.width, SIZE / image.height); const width = image.width * scale; const height = image.height * scale; context.drawImage(image, (SIZE - width) / 2, (SIZE - height) / 2, width, height); sourcePixels = canvasPixels(canvas); imageLabel = label; reverseInputMode = 'seed'; reverseNoiseState = makeNoiseState(readSeed()); forwardRevealCount = 1; forwardCompleted = false; resetProgress('forward'); computeForwardStates(); renderTracks(); }
179
+ modelInput.addEventListener('change', () => {
180
+ applyTimesteps();
181
+ if (modelInput.value === 'google/ddpm-cifar10-32') {
182
+ const image = new Image();
183
+ image.onload = () => {
184
+ if (modelInput.value === 'google/ddpm-cifar10-32') loadImage(image, 'CIFAR-10 train 23');
185
+ };
186
+ image.onerror = () => setModelStatus('Could not load the CIFAR-10 source image.');
187
+ image.src = 'assets/cifar10_train_23_32.png';
188
+ }
189
+ });
190
  elements.applyTimesteps.addEventListener('click', applyTimesteps); elements.timestepsInput.addEventListener('keydown', (event) => { if (event.key === 'Enter') applyTimesteps(); });
191
  elements.randomizeSeed.addEventListener('click', () => { resampleNoise(); elements.configError.textContent = ''; forwardRevealCount = 1; forwardCompleted = false; resetProgress('forward'); computeForwardStates(); renderTracks(); });
192
  elements.imageUpload.addEventListener('change', (event) => { const file = event.target.files?.[0]; if (!file) return; const image = new Image(); image.onload = () => { loadImage(image, file.name.replace(/\.[^.]+$/, '')); URL.revokeObjectURL(image.src); }; image.src = URL.createObjectURL(file); });
 
194
  // and as a fallback if the image asset cannot be read.
195
  sourcePixels = canvasPixels(makeSourceArt()); computeForwardStates(); reverseNoiseState = makeNoiseState(readSeed()); resetProgress('forward'); resetProgress('reverse'); renderTracks();
196
  const defaultSourceImage = new Image();
197
+ defaultSourceImage.onload = () => {
198
+ if (modelInput.value === 'google/ddpm-celebahq-256') loadImage(defaultSourceImage, 'Human face');
199
+ };
200
  defaultSourceImage.src = 'assets/celeba_hq_validation_91_256.png';
201
+
202
+ document.addEventListener('pointerdown', event => {
203
+ if (!event.target.closest('.stage-canvas canvas')) document.querySelectorAll('.hover-preview.is-visible').forEach(item => item.classList.remove('is-visible'));
204
+ });
205
+ document.addEventListener('keydown', event => {
206
+ if (event.key === 'Escape') document.querySelectorAll('.hover-preview.is-visible').forEach(item => item.classList.remove('is-visible'));
207
+ });
app.py CHANGED
@@ -13,7 +13,7 @@ import gradio as gr
13
  import numpy as np
14
  from PIL import Image
15
 
16
- from generate import CHECKPOINTS, ROOT, generate_images, load_model
17
 
18
  ZERO_GPU = os.environ.get("SPACES_ZERO_GPU", "").lower() in {"1", "true", "yes"}
19
  def gpu_unavailable(error):
@@ -42,7 +42,8 @@ GPU_READY = False
42
  MPS_READY = False
43
  if ZERO_GPU:
44
  try:
45
- load_model(device="cuda")
 
46
  GPU_READY = True
47
  except Exception as error:
48
  if not gpu_unavailable(error):
@@ -76,8 +77,8 @@ def checkpoint_slots(checkpoints, frames=None):
76
  return [(frames.get(step, BLANK_IMAGE), f"t = {step}") for step in checkpoints]
77
 
78
 
79
- def gaussian_noise(seed):
80
- noise = np.random.default_rng(int(seed)).standard_normal((256, 256, 3))
81
  return Image.fromarray(np.round((noise.clip(-1, 1) + 1) * 127.5).astype(np.uint8))
82
 
83
 
@@ -96,15 +97,15 @@ def progress_text(completed=0, started=None):
96
  f"{f'{rate:.2f}' if rate else '?'}it/s]")
97
 
98
 
99
- def diffuse(image, seed, checkpoint_text):
100
  seed, checkpoints = parse_settings(seed, checkpoint_text)
101
  if image is None:
102
  raise gr.Error("Upload an image first.")
103
- pixels = np.asarray(image.convert("RGB").resize((256, 256)), dtype=np.float32) / 127.5 - 1
104
  noise = np.random.default_rng(seed).standard_normal(pixels.shape)
105
  alpha_bars = np.cumprod(1 - np.linspace(0.0001, 0.02, 1000))
106
  started = time.monotonic()
107
- frames = {0: image.convert("RGB").resize((256, 256))}
108
  final_noise = None
109
  yield checkpoint_slots(checkpoints, frames), None, progress_html(0, started)
110
  for step in checkpoints:
@@ -116,12 +117,12 @@ def diffuse(image, seed, checkpoint_text):
116
  yield checkpoint_slots(checkpoints, frames), final_noise if step == 1000 else None, progress_html(step, started)
117
 
118
 
119
- def stream_denoising(seed, checkpoints, initial_image, device):
120
  started = time.monotonic()
121
- frames = {1000: initial_image if initial_image is not None else gaussian_noise(seed)}
122
  yield checkpoint_slots(checkpoints, frames), progress_html(0, started)
123
  print('using', device)
124
- for step, frame in generate_images(seed, checkpoints, initial_image, device=device):
125
  if frame is not None:
126
  frames[step] = frame
127
  yield (checkpoint_slots(checkpoints, frames) if frame is not None else gr.skip(),
@@ -129,18 +130,18 @@ def stream_denoising(seed, checkpoints, initial_image, device):
129
 
130
 
131
  @spaces.GPU(duration=int(os.environ.get("GPU_DURATION_SECONDS", "120")))
132
- def denoise_gpu(seed, checkpoints, initial_image):
133
- yield from stream_denoising(seed, checkpoints, initial_image, "cuda")
134
 
135
 
136
- def denoise(seed, checkpoint_text, input_mode, forward_noise):
137
  seed, checkpoints = parse_settings(seed, checkpoint_text)
138
  initial_image = forward_noise if input_mode == "Forward noise" else None
139
  if input_mode == "Forward noise" and initial_image is None:
140
  raise gr.Error("Run Diffuse first to create forward noise.")
141
  if GPU_READY:
142
  try:
143
- yield from denoise_gpu(seed, checkpoints, initial_image)
144
  return
145
  except Exception as error:
146
  if not gpu_unavailable(error):
@@ -150,7 +151,7 @@ def denoise(seed, checkpoint_text, input_mode, forward_noise):
150
  gr.Info(f"CUDA unavailable or quota exhausted. Running on {fallback}.")
151
  if MPS_READY:
152
  try:
153
- yield from stream_denoising(seed, checkpoints, initial_image, "mps")
154
  return
155
  except (RuntimeError, NotImplementedError) as error:
156
  message = str(error).lower()
@@ -158,7 +159,7 @@ def denoise(seed, checkpoint_text, input_mode, forward_noise):
158
  raise
159
  print(f"MPS unavailable; restarting on CPU: {error}")
160
  gr.Info("MPS unavailable. Running on CPU.")
161
- yield from stream_denoising(seed, checkpoints, initial_image, "cpu")
162
 
163
 
164
  # Reuse the original page's palette, typography, header, and panel styling.
@@ -201,7 +202,23 @@ PAGE_CSS = (ROOT / "styles.css").read_text() + """
201
  .config-field textarea,.config-field input { width:100% !important; max-width:none !important; box-sizing:border-box !important; height:40px !important; min-height:40px !important;
202
  border:1px solid #cfd3c9 !important; border-radius:0 3px 3px 0 !important;
203
  padding:10px 12px !important; font:11px var(--mono) !important; background:white !important; white-space:pre !important; overflow-x:auto !important; resize:none !important; }
204
- #model-field textarea { background:#f8f7f2 !important; color:#687169 !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  .process-button { flex:0 0 auto !important; min-width:76px !important;
206
  height:28px !important; min-height:28px !important; padding:0 12px !important;
207
  background:var(--green) !important; color:white !important; border-radius:3px !important;
@@ -234,10 +251,10 @@ PAGE_CSS = (ROOT / "styles.css").read_text() + """
234
  .checkpoint-preview-host { position:relative; height:0 !important; min-height:0 !important; padding:0 !important;
235
  margin:0 !important; border:0 !important; overflow:visible !important; z-index:1000; }
236
  .checkpoint-preview-host .html-container { padding:0 !important; overflow:visible !important; }
237
- .checkpoint-preview { position:absolute; bottom:8px; width:min(256px,100%); z-index:1000;
238
  border:2px solid #fbfaf6; background:#fbfaf6; box-shadow:0 8px 24px #25312852; }
239
  .checkpoint-preview summary { cursor:pointer; padding:3px 6px; font:10px var(--mono); }
240
- .checkpoint-preview img { width:100%; height:auto; display:block; }
241
  .checkpoint-preview:not([open]) { display:none; }
242
  #reverse-input { justify-self:end; width:max-content !important; min-width:0 !important;
243
  border:0 !important; background:transparent !important; padding:0 !important; }
@@ -287,19 +304,26 @@ def progress_html(completed=0, started=None):
287
  return "<pre>" + progress_text(completed, started) + "</pre>"
288
 
289
 
290
- def reset_tracks(seed, checkpoint_text, image):
291
  seed, steps = parse_settings(seed, checkpoint_text)
292
- initial = {0: image.convert("RGB").resize((256, 256))} if image is not None else {}
293
  return (", ".join(map(str, steps)),
294
  gr.Gallery(value=checkpoint_slots(steps, initial), columns=len(steps), allow_preview=False, interactive=False),
295
- gr.Gallery(value=checkpoint_slots(steps, {1000: gaussian_noise(seed)}), columns=len(steps), allow_preview=False, interactive=False),
296
  None, progress_html(), progress_html())
297
 
298
 
299
- def change_source(filepath, seed, checkpoint_text):
300
  with Image.open(filepath) as uploaded:
301
  image = uploaded.convert("RGB")
302
- return (image, *reset_tracks(seed, checkpoint_text, image))
 
 
 
 
 
 
 
303
 
304
 
305
  def show_checkpoint_preview(gallery, event: gr.SelectData):
@@ -315,14 +339,16 @@ def show_checkpoint_preview(gallery, event: gr.SelectData):
315
  buffer = BytesIO()
316
  image.save(buffer, format="PNG")
317
  encoded = base64.b64encode(buffer.getvalue()).decode("ascii")
 
 
318
  center = 100 * (index + 0.5) / len(gallery)
319
  # Force fresh markup even when the same thumbnail is clicked again, so
320
  # a details element closed in the browser is recreated in its open state.
321
  preview_id = secrets.token_hex(8)
322
  return (f'<details open class="checkpoint-preview" data-preview-id="{preview_id}" '
323
- f'style="left:clamp(0px, calc({center}% - 128px), max(0px, calc(100% - 256px)))">'
324
  '<summary>Close preview</summary>'
325
- f'<img src="data:image/png;base64,{encoded}" alt="{html.escape(caption or "Checkpoint", quote=True)}">'
326
  '</details>')
327
 
328
 
@@ -337,8 +363,8 @@ with gr.Blocks(title="Diffusion — A DDPM in motion", **BLOCK_OPTIONS) as demo:
337
  gr.HTML("<h1>Configure</h1>")
338
  with gr.Row(elem_classes="config-row"):
339
  gr.Button("Model", interactive=False, scale=0, min_width=110, elem_classes="config-label")
340
- gr.Textbox(value="google/ddpm-celebahq-256", interactive=False,
341
- scale=10, min_width=0, lines=1, max_lines=1, show_label=False, container=False, elem_id="model-field", elem_classes="config-field")
342
  with gr.Row(elem_classes="config-row"):
343
  gr.Button("Timesteps", interactive=False, scale=0, min_width=110, elem_classes="config-label")
344
  checkpoints = gr.Textbox(value=", ".join(map(str, CHECKPOINTS)), show_label=False,
@@ -373,11 +399,11 @@ with gr.Blocks(title="Diffusion — A DDPM in motion", **BLOCK_OPTIONS) as demo:
373
  forward_gallery.select(show_checkpoint_preview, [forward_gallery], [forward_preview],
374
  queue=False, show_progress="hidden").then(
375
  fn=None, js=INSTALL_PREVIEW_DISMISS, queue=False)
376
- diffuse_button.click(diffuse, [source, seed, checkpoints],
377
  [forward_gallery, forward_noise, forward_status])
378
 
379
  with gr.Column(elem_classes=["lab-panel", "process-panel"]):
380
- gr.HTML('<div class="process-heading"><h2>Reverse process: noise to face</h2>'
381
  '<p class="reverse-equation">x<sub>t−1</sub> = 1/√α<sub>t</sub>'
382
  ' (x<sub>t</sub> − β<sub>t</sub>/√(1−α̅<sub>t</sub>)'
383
  ' ε<sub>θ</sub>(x<sub>t</sub>,t)) + σ<sub>t</sub>z</p></div>')
@@ -395,7 +421,7 @@ with gr.Blocks(title="Diffusion — A DDPM in motion", **BLOCK_OPTIONS) as demo:
395
  reverse_gallery.select(show_checkpoint_preview, [reverse_gallery], [reverse_preview],
396
  queue=False, show_progress="hidden").then(
397
  fn=None, js=INSTALL_PREVIEW_DISMISS, queue=False)
398
- denoise_button.click(denoise, [seed, checkpoints, input_mode, forward_noise],
399
  [reverse_gallery, status], concurrency_limit=1,
400
  **({"stream_every": 0} if "stream_every" in
401
  inspect.signature(denoise_button.click).parameters else {}))
@@ -408,8 +434,10 @@ with gr.Blocks(title="Diffusion — A DDPM in motion", **BLOCK_OPTIONS) as demo:
408
  queue=False, show_progress="hidden")
409
  reset_outputs = [checkpoints, forward_gallery, reverse_gallery,
410
  forward_noise, forward_status, status]
411
- apply_button.click(reset_tracks, [seed, checkpoints, source], reset_outputs, queue=False)
412
- upload_source.upload(change_source, [upload_source, seed, checkpoints],
 
 
413
  [source, *reset_outputs], queue=False)
414
 
415
  if __name__ == "__main__":
 
13
  import numpy as np
14
  from PIL import Image
15
 
16
+ from generate import CHECKPOINTS, MODEL_ID, MODEL_SIZES, ROOT, generate_images, load_model
17
 
18
  ZERO_GPU = os.environ.get("SPACES_ZERO_GPU", "").lower() in {"1", "true", "yes"}
19
  def gpu_unavailable(error):
 
42
  MPS_READY = False
43
  if ZERO_GPU:
44
  try:
45
+ for model_id in MODEL_SIZES:
46
+ load_model(device="cuda", model_id=model_id)
47
  GPU_READY = True
48
  except Exception as error:
49
  if not gpu_unavailable(error):
 
77
  return [(frames.get(step, BLANK_IMAGE), f"t = {step}") for step in checkpoints]
78
 
79
 
80
+ def gaussian_noise(seed, model_id=MODEL_ID):
81
+ noise = np.random.default_rng(int(seed)).standard_normal((MODEL_SIZES[model_id], MODEL_SIZES[model_id], 3))
82
  return Image.fromarray(np.round((noise.clip(-1, 1) + 1) * 127.5).astype(np.uint8))
83
 
84
 
 
97
  f"{f'{rate:.2f}' if rate else '?'}it/s]")
98
 
99
 
100
+ def diffuse(image, seed, checkpoint_text, model_id=MODEL_ID):
101
  seed, checkpoints = parse_settings(seed, checkpoint_text)
102
  if image is None:
103
  raise gr.Error("Upload an image first.")
104
+ pixels = np.asarray(image.convert("RGB").resize((MODEL_SIZES[model_id], MODEL_SIZES[model_id])), dtype=np.float32) / 127.5 - 1
105
  noise = np.random.default_rng(seed).standard_normal(pixels.shape)
106
  alpha_bars = np.cumprod(1 - np.linspace(0.0001, 0.02, 1000))
107
  started = time.monotonic()
108
+ frames = {0: image.convert("RGB").resize((MODEL_SIZES[model_id], MODEL_SIZES[model_id]))}
109
  final_noise = None
110
  yield checkpoint_slots(checkpoints, frames), None, progress_html(0, started)
111
  for step in checkpoints:
 
117
  yield checkpoint_slots(checkpoints, frames), final_noise if step == 1000 else None, progress_html(step, started)
118
 
119
 
120
+ def stream_denoising(seed, checkpoints, initial_image, device, model_id=MODEL_ID):
121
  started = time.monotonic()
122
+ frames = {1000: initial_image if initial_image is not None else gaussian_noise(seed, model_id)}
123
  yield checkpoint_slots(checkpoints, frames), progress_html(0, started)
124
  print('using', device)
125
+ for step, frame in generate_images(seed, checkpoints, initial_image, device=device, model_id=model_id):
126
  if frame is not None:
127
  frames[step] = frame
128
  yield (checkpoint_slots(checkpoints, frames) if frame is not None else gr.skip(),
 
130
 
131
 
132
  @spaces.GPU(duration=int(os.environ.get("GPU_DURATION_SECONDS", "120")))
133
+ def denoise_gpu(seed, checkpoints, initial_image, model_id=MODEL_ID):
134
+ yield from stream_denoising(seed, checkpoints, initial_image, "cuda", model_id)
135
 
136
 
137
+ def denoise(seed, checkpoint_text, input_mode, forward_noise, model_id=MODEL_ID):
138
  seed, checkpoints = parse_settings(seed, checkpoint_text)
139
  initial_image = forward_noise if input_mode == "Forward noise" else None
140
  if input_mode == "Forward noise" and initial_image is None:
141
  raise gr.Error("Run Diffuse first to create forward noise.")
142
  if GPU_READY:
143
  try:
144
+ yield from denoise_gpu(seed, checkpoints, initial_image, model_id)
145
  return
146
  except Exception as error:
147
  if not gpu_unavailable(error):
 
151
  gr.Info(f"CUDA unavailable or quota exhausted. Running on {fallback}.")
152
  if MPS_READY:
153
  try:
154
+ yield from stream_denoising(seed, checkpoints, initial_image, "mps", model_id)
155
  return
156
  except (RuntimeError, NotImplementedError) as error:
157
  message = str(error).lower()
 
159
  raise
160
  print(f"MPS unavailable; restarting on CPU: {error}")
161
  gr.Info("MPS unavailable. Running on CPU.")
162
+ yield from stream_denoising(seed, checkpoints, initial_image, "cpu", model_id)
163
 
164
 
165
  # Reuse the original page's palette, typography, header, and panel styling.
 
202
  .config-field textarea,.config-field input { width:100% !important; max-width:none !important; box-sizing:border-box !important; height:40px !important; min-height:40px !important;
203
  border:1px solid #cfd3c9 !important; border-radius:0 3px 3px 0 !important;
204
  padding:10px 12px !important; font:11px var(--mono) !important; background:white !important; white-space:pre !important; overflow-x:auto !important; resize:none !important; }
205
+ /* The dropdown wrapper adds spacing that textboxes do not have. */
206
+ .config-row:has(#model-field) { column-gap:0 !important; }
207
+ .config-row:has(#model-field) > .form { margin:0 !important; padding:0 !important; gap:0 !important; }
208
+ #model-field { margin:0 !important; padding:0 !important;
209
+ height:40px !important; min-height:40px !important; max-height:40px !important; }
210
+ #model-field .wrap, #model-field .input-container { margin:0 !important; }
211
+ #model-field .container { padding:0 !important; margin:0 !important; }
212
+ #model-field .wrap { box-sizing:border-box !important;
213
+ height:40px !important; min-height:40px !important; max-height:40px !important;
214
+ border:1px solid #cfd3c9 !important;
215
+ border-radius:0 3px 3px 0 !important; box-shadow:none !important; }
216
+ #model-field .wrap-inner { height:38px !important; min-height:0 !important;
217
+ padding:0 12px !important; gap:0 !important; }
218
+ /* The dropdown input sits inside its own bordered wrapper. */
219
+ #model-field input { border:0 !important; border-radius:0 !important;
220
+ margin:0 !important; padding:0 !important; background:transparent !important; box-shadow:none !important;
221
+ height:38px !important; min-height:38px !important; }
222
  .process-button { flex:0 0 auto !important; min-width:76px !important;
223
  height:28px !important; min-height:28px !important; padding:0 12px !important;
224
  background:var(--green) !important; color:white !important; border-radius:3px !important;
 
251
  .checkpoint-preview-host { position:relative; height:0 !important; min-height:0 !important; padding:0 !important;
252
  margin:0 !important; border:0 !important; overflow:visible !important; z-index:1000; }
253
  .checkpoint-preview-host .html-container { padding:0 !important; overflow:visible !important; }
254
+ .checkpoint-preview { position:absolute; bottom:8px; width:max-content; max-width:none; z-index:1000;
255
  border:2px solid #fbfaf6; background:#fbfaf6; box-shadow:0 8px 24px #25312852; }
256
  .checkpoint-preview summary { cursor:pointer; padding:3px 6px; font:10px var(--mono); }
257
+ .checkpoint-preview img { width:auto; height:auto; max-width:none; display:block; }
258
  .checkpoint-preview:not([open]) { display:none; }
259
  #reverse-input { justify-self:end; width:max-content !important; min-width:0 !important;
260
  border:0 !important; background:transparent !important; padding:0 !important; }
 
304
  return "<pre>" + progress_text(completed, started) + "</pre>"
305
 
306
 
307
+ def reset_tracks(seed, checkpoint_text, image, model_id=MODEL_ID):
308
  seed, steps = parse_settings(seed, checkpoint_text)
309
+ initial = {0: image.convert("RGB").resize((MODEL_SIZES[model_id], MODEL_SIZES[model_id]))} if image is not None else {}
310
  return (", ".join(map(str, steps)),
311
  gr.Gallery(value=checkpoint_slots(steps, initial), columns=len(steps), allow_preview=False, interactive=False),
312
+ gr.Gallery(value=checkpoint_slots(steps, {1000: gaussian_noise(seed, model_id)}), columns=len(steps), allow_preview=False, interactive=False),
313
  None, progress_html(), progress_html())
314
 
315
 
316
+ def change_source(filepath, seed, checkpoint_text, model_id=MODEL_ID):
317
  with Image.open(filepath) as uploaded:
318
  image = uploaded.convert("RGB")
319
+ return (image, *reset_tracks(seed, checkpoint_text, image, model_id))
320
+
321
+
322
+ def change_model(seed, checkpoint_text, image, model_id):
323
+ if model_id == "google/ddpm-cifar10-32":
324
+ return change_source(ROOT / "assets/cifar10_train_23_32.png",
325
+ seed, checkpoint_text, model_id)
326
+ return (image, *reset_tracks(seed, checkpoint_text, image, model_id))
327
 
328
 
329
  def show_checkpoint_preview(gallery, event: gr.SelectData):
 
339
  buffer = BytesIO()
340
  image.save(buffer, format="PNG")
341
  encoded = base64.b64encode(buffer.getvalue()).decode("ascii")
342
+ width, height = image.size
343
+ preview_width = width + 4
344
  center = 100 * (index + 0.5) / len(gallery)
345
  # Force fresh markup even when the same thumbnail is clicked again, so
346
  # a details element closed in the browser is recreated in its open state.
347
  preview_id = secrets.token_hex(8)
348
  return (f'<details open class="checkpoint-preview" data-preview-id="{preview_id}" '
349
+ f'style="left:clamp(0px, calc({center}% - {preview_width / 2}px), max(0px, calc(100% - {preview_width}px)))">'
350
  '<summary>Close preview</summary>'
351
+ f'<img width="{width}" height="{height}" src="data:image/png;base64,{encoded}" alt="{html.escape(caption or "Checkpoint", quote=True)}">'
352
  '</details>')
353
 
354
 
 
363
  gr.HTML("<h1>Configure</h1>")
364
  with gr.Row(elem_classes="config-row"):
365
  gr.Button("Model", interactive=False, scale=0, min_width=110, elem_classes="config-label")
366
+ model = gr.Dropdown(choices=list(MODEL_SIZES), value=MODEL_ID,
367
+ scale=10, min_width=0, show_label=False, container=False, elem_id="model-field", elem_classes="config-field")
368
  with gr.Row(elem_classes="config-row"):
369
  gr.Button("Timesteps", interactive=False, scale=0, min_width=110, elem_classes="config-label")
370
  checkpoints = gr.Textbox(value=", ".join(map(str, CHECKPOINTS)), show_label=False,
 
399
  forward_gallery.select(show_checkpoint_preview, [forward_gallery], [forward_preview],
400
  queue=False, show_progress="hidden").then(
401
  fn=None, js=INSTALL_PREVIEW_DISMISS, queue=False)
402
+ diffuse_button.click(diffuse, [source, seed, checkpoints, model],
403
  [forward_gallery, forward_noise, forward_status])
404
 
405
  with gr.Column(elem_classes=["lab-panel", "process-panel"]):
406
+ gr.HTML('<div class="process-heading"><h2>Reverse process: noise to image</h2>'
407
  '<p class="reverse-equation">x<sub>t−1</sub> = 1/√α<sub>t</sub>'
408
  ' (x<sub>t</sub> − β<sub>t</sub>/√(1−α̅<sub>t</sub>)'
409
  ' ε<sub>θ</sub>(x<sub>t</sub>,t)) + σ<sub>t</sub>z</p></div>')
 
421
  reverse_gallery.select(show_checkpoint_preview, [reverse_gallery], [reverse_preview],
422
  queue=False, show_progress="hidden").then(
423
  fn=None, js=INSTALL_PREVIEW_DISMISS, queue=False)
424
+ denoise_button.click(denoise, [seed, checkpoints, input_mode, forward_noise, model],
425
  [reverse_gallery, status], concurrency_limit=1,
426
  **({"stream_every": 0} if "stream_every" in
427
  inspect.signature(denoise_button.click).parameters else {}))
 
434
  queue=False, show_progress="hidden")
435
  reset_outputs = [checkpoints, forward_gallery, reverse_gallery,
436
  forward_noise, forward_status, status]
437
+ apply_button.click(reset_tracks, [seed, checkpoints, source, model], reset_outputs, queue=False)
438
+ model.change(change_model, [seed, checkpoints, source, model], [source, *reset_outputs])
439
+ model.change(clear_previews, outputs=[forward_preview, reverse_preview], queue=False)
440
+ upload_source.upload(change_source, [upload_source, seed, checkpoints, model],
441
  [source, *reset_outputs], queue=False)
442
 
443
  if __name__ == "__main__":
assets/cifar10_train_23_32.png ADDED

Git LFS Details

  • SHA256: d706c00bcded01f20f412bb2d747bf1959b896ebec6cc19710a917b1c5d49939
  • Pointer size: 129 Bytes
  • Size of remote file: 2.24 kB
download_cifar10.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import json
3
+ from io import BytesIO
4
+ from pathlib import Path
5
+ from urllib.error import HTTPError, URLError
6
+ from urllib.parse import urlencode
7
+ from urllib.request import Request, urlopen
8
+
9
+ from PIL import Image
10
+
11
+
12
+ DATASET = "uoft-cs/cifar10"
13
+ API_URL = "https://datasets-server.huggingface.co/rows"
14
+
15
+
16
+ def download_row(row_index, split="test", output=None):
17
+ """Download one CIFAR-10 row and save it as a 32x32 RGB PNG."""
18
+ if row_index < 0:
19
+ raise ValueError("Row index must be zero or greater.")
20
+
21
+ if split not in {"train", "test"}:
22
+ raise ValueError("Split must be train or test.")
23
+
24
+ query = urlencode(
25
+ {
26
+ "dataset": DATASET,
27
+ "config": "plain_text",
28
+ "split": split,
29
+ "offset": row_index,
30
+ "length": 1,
31
+ }
32
+ )
33
+ request = Request(f"{API_URL}?{query}", headers={"User-Agent": "diffusion-lab/1.0"})
34
+ with urlopen(request, timeout=30) as response:
35
+ result = json.load(response)
36
+
37
+ rows = result.get("rows", [])
38
+ if not rows:
39
+ total = result.get("num_rows_total", "the available")
40
+ raise ValueError(f"Row {row_index} is outside {split!r} ({total} rows).")
41
+
42
+ image_url = rows[0]["row"]["img"]["src"]
43
+ image_request = Request(image_url, headers={"User-Agent": "diffusion-lab/1.0"})
44
+ with urlopen(image_request, timeout=60) as response:
45
+ image_bytes = response.read()
46
+
47
+ destination = Path(output or f"cifar10_{split}_{row_index}_32.png")
48
+ destination.parent.mkdir(parents=True, exist_ok=True)
49
+ with Image.open(BytesIO(image_bytes)) as image:
50
+ image = image.convert("RGB").resize((32, 32), Image.Resampling.LANCZOS)
51
+ image.save(destination, format="PNG", optimize=True)
52
+
53
+ return destination
54
+
55
+
56
+ def main():
57
+ parser = argparse.ArgumentParser(
58
+ description="Download a specific row from the CIFAR-10 32x32 dataset."
59
+ )
60
+ parser.add_argument("row_index", type=int, help="Zero-based row index")
61
+ parser.add_argument(
62
+ "--split",
63
+ choices=("train", "test"),
64
+ default="test",
65
+ help="Dataset split (default: test)",
66
+ )
67
+ parser.add_argument(
68
+ "--output",
69
+ type=Path,
70
+ help="Output PNG path (default: cifar10_<split>_<row>_32.png)",
71
+ )
72
+ args = parser.parse_args()
73
+
74
+ try:
75
+ destination = download_row(args.row_index, args.split, args.output)
76
+ except (HTTPError, URLError, TimeoutError, ValueError, KeyError, OSError) as error:
77
+ parser.exit(1, f"Download failed: {error}\n")
78
+
79
+ print(f"Saved row {args.row_index} from {args.split!r} to {destination}")
80
+
81
+
82
+ if __name__ == "__main__":
83
+ main()
generate.py CHANGED
@@ -9,6 +9,7 @@ from PIL import Image
9
 
10
  ROOT = Path(__file__).resolve().parent
11
  MODEL_ID = "google/ddpm-celebahq-256"
 
12
  CHECKPOINTS = (0, 50, 100, 200, 350, 500, 700, 1000)
13
 
14
  # Gradio may resume a generator on a different worker thread between yields.
@@ -21,10 +22,12 @@ _TORCH = None
21
  _DEVICE = None
22
 
23
 
24
- def load_model(device=None):
25
  """Load and cache the diffusion pipeline, returning it and its device."""
26
  global _PIPELINE, _TORCH, _DEVICE
27
 
 
 
28
  with _CACHE_LOCK:
29
  try:
30
  import torch
@@ -44,13 +47,14 @@ def load_model(device=None):
44
  else:
45
  device = "cpu"
46
  device = str(device)
47
- if device not in _PIPELINES:
48
- print(f"Loading model on device: {device}")
 
49
  pipeline = DDPMPipeline.from_pretrained(
50
- MODEL_ID, cache_dir=Path(os.environ.get("MODEL_CACHE_DIR", ROOT / "models")))
51
  pipeline.to(device)
52
- _PIPELINES[device] = pipeline
53
- _PIPELINE = _PIPELINES[device]
54
  _TORCH = torch
55
  _DEVICE = device
56
  return _PIPELINE, _TORCH, _DEVICE
@@ -67,10 +71,10 @@ def sample_to_image(sample):
67
  return Image.fromarray(image, mode="RGB")
68
 
69
 
70
- def generate_images(seed, checkpoints=CHECKPOINTS, initial_image=None, device=None):
71
  """Yield every remaining-step count and an image at requested checkpoints."""
72
  with _MODEL_LOCK:
73
- pipeline, torch, device = load_model(device=device)
74
  scheduler = pipeline.scheduler
75
  training_steps = scheduler.config.num_train_timesteps
76
  scheduler.set_timesteps(training_steps, device=device)
@@ -117,10 +121,10 @@ def generate_images(seed, checkpoints=CHECKPOINTS, initial_image=None, device=No
117
  yield remaining_steps, image
118
 
119
 
120
- def generate_image(seed):
121
  """Generate and return the final image for a seed."""
122
  final_image = None
123
- for _, image in generate_images(seed, checkpoints=(0,)):
124
  if image is not None:
125
  final_image = image
126
  if final_image is None:
@@ -129,25 +133,26 @@ def generate_image(seed):
129
 
130
 
131
  def main():
132
- parser = argparse.ArgumentParser(description="Generate a CelebA-HQ face image.")
133
  parser.add_argument(
134
  "--seed",
135
  type=int,
136
  default=None,
137
  help="Random seed (defaults to a randomly generated seed)",
138
  )
 
139
  args = parser.parse_args()
140
  seed = args.seed if args.seed is not None else secrets.randbelow(2**31)
141
  if seed < 0 or seed >= 2**63:
142
  parser.error("seed must be between 0 and 2^63 - 1")
143
 
144
  print(f"Generating with seed: {seed}")
145
- image = generate_image(seed)
146
 
147
  import matplotlib.pyplot as plt
148
 
149
  plt.imshow(image)
150
- plt.title(f"{MODEL_ID} — seed {seed}")
151
  plt.axis("off")
152
  plt.tight_layout()
153
  plt.show()
 
9
 
10
  ROOT = Path(__file__).resolve().parent
11
  MODEL_ID = "google/ddpm-celebahq-256"
12
+ MODEL_SIZES = {MODEL_ID: 256, "google/ddpm-cifar10-32": 32}
13
  CHECKPOINTS = (0, 50, 100, 200, 350, 500, 700, 1000)
14
 
15
  # Gradio may resume a generator on a different worker thread between yields.
 
22
  _DEVICE = None
23
 
24
 
25
+ def load_model(device=None, model_id=MODEL_ID):
26
  """Load and cache the diffusion pipeline, returning it and its device."""
27
  global _PIPELINE, _TORCH, _DEVICE
28
 
29
+ if model_id not in MODEL_SIZES:
30
+ raise ValueError(f"Unsupported model: {model_id}")
31
  with _CACHE_LOCK:
32
  try:
33
  import torch
 
47
  else:
48
  device = "cpu"
49
  device = str(device)
50
+ key = (model_id, device)
51
+ if key not in _PIPELINES:
52
+ print(f"Loading {model_id} on device: {device}")
53
  pipeline = DDPMPipeline.from_pretrained(
54
+ model_id, cache_dir=Path(os.environ.get("MODEL_CACHE_DIR", ROOT / "models")))
55
  pipeline.to(device)
56
+ _PIPELINES[key] = pipeline
57
+ _PIPELINE = _PIPELINES[key]
58
  _TORCH = torch
59
  _DEVICE = device
60
  return _PIPELINE, _TORCH, _DEVICE
 
71
  return Image.fromarray(image, mode="RGB")
72
 
73
 
74
+ def generate_images(seed, checkpoints=CHECKPOINTS, initial_image=None, device=None, model_id=MODEL_ID):
75
  """Yield every remaining-step count and an image at requested checkpoints."""
76
  with _MODEL_LOCK:
77
+ pipeline, torch, device = load_model(device=device, model_id=model_id)
78
  scheduler = pipeline.scheduler
79
  training_steps = scheduler.config.num_train_timesteps
80
  scheduler.set_timesteps(training_steps, device=device)
 
121
  yield remaining_steps, image
122
 
123
 
124
+ def generate_image(seed, model_id=MODEL_ID):
125
  """Generate and return the final image for a seed."""
126
  final_image = None
127
+ for _, image in generate_images(seed, checkpoints=(0,), model_id=model_id):
128
  if image is not None:
129
  final_image = image
130
  if final_image is None:
 
133
 
134
 
135
  def main():
136
+ parser = argparse.ArgumentParser(description="Generate an unconditional DDPM image.")
137
  parser.add_argument(
138
  "--seed",
139
  type=int,
140
  default=None,
141
  help="Random seed (defaults to a randomly generated seed)",
142
  )
143
+ parser.add_argument("--model", choices=list(MODEL_SIZES), default=MODEL_ID)
144
  args = parser.parse_args()
145
  seed = args.seed if args.seed is not None else secrets.randbelow(2**31)
146
  if seed < 0 or seed >= 2**63:
147
  parser.error("seed must be between 0 and 2^63 - 1")
148
 
149
  print(f"Generating with seed: {seed}")
150
+ image = generate_image(seed, model_id=args.model)
151
 
152
  import matplotlib.pyplot as plt
153
 
154
  plt.imshow(image)
155
+ plt.title(f"{args.model} — seed {seed}")
156
  plt.axis("off")
157
  plt.tight_layout()
158
  plt.show()
index.html CHANGED
@@ -23,7 +23,10 @@
23
  <div class="config-rows">
24
  <div class="config-row">
25
  <label for="model-input">Model</label>
26
- <input id="model-input" type="text" value="google/ddpm-celebahq-256" readonly>
 
 
 
27
  </div>
28
  <div class="config-row">
29
  <label for="timesteps-input">Timesteps</label>
@@ -48,7 +51,7 @@
48
  </section>
49
  <section class="process-panel lab-panel" aria-labelledby="denoising-title">
50
  <div class="process-heading"><div class="panel-heading"><div>
51
- <h2 id="denoising-title">Reverse process: noise to face</h2></div></div>
52
  <p class="reverse-equation">x<sub>t−1</sub> = 1/√α<sub>t</sub> (x<sub>t</sub> − β<sub>t</sub>/√(1−α̅<sub>t</sub>) ε<sub>θ</sub>(x<sub>t</sub>,t)) + σ<sub>t</sub>z</p>
53
  </div>
54
  <output id="reverse-progress-label" class="step-progress">0/1000</output>
 
23
  <div class="config-rows">
24
  <div class="config-row">
25
  <label for="model-input">Model</label>
26
+ <select id="model-input">
27
+ <option value="google/ddpm-celebahq-256">google/ddpm-celebahq-256</option>
28
+ <option value="google/ddpm-cifar10-32">google/ddpm-cifar10-32</option>
29
+ </select>
30
  </div>
31
  <div class="config-row">
32
  <label for="timesteps-input">Timesteps</label>
 
51
  </section>
52
  <section class="process-panel lab-panel" aria-labelledby="denoising-title">
53
  <div class="process-heading"><div class="panel-heading"><div>
54
+ <h2 id="denoising-title">Reverse process: noise to image</h2></div></div>
55
  <p class="reverse-equation">x<sub>t−1</sub> = 1/√α<sub>t</sub> (x<sub>t</sub> − β<sub>t</sub>/√(1−α̅<sub>t</sub>) ε<sub>θ</sub>(x<sub>t</sub>,t)) + σ<sub>t</sub>z</p>
56
  </div>
57
  <output id="reverse-progress-label" class="step-progress">0/1000</output>
server.py CHANGED
@@ -8,7 +8,7 @@ from io import BytesIO
8
  from pathlib import Path
9
  from urllib.parse import urlparse
10
 
11
- from generate import CHECKPOINTS, MODEL_ID, generate_images, is_model_loaded, load_model
12
 
13
 
14
  ROOT = Path(__file__).resolve().parent
@@ -20,17 +20,17 @@ def encode_frame(image):
20
  return base64.b64encode(buffer.getvalue()).decode("ascii")
21
 
22
 
23
- def generate_frames(seed, checkpoints=CHECKPOINTS, initial_image=None):
24
- yield {"type": "status", "status": "loading", "model": MODEL_ID}
25
 
26
  try:
27
- for step, image in generate_images(seed, checkpoints=checkpoints, initial_image=initial_image):
28
  yield {"type": "progress", "step": step, "total": 1000}
29
  if image is not None:
30
  yield {"type": "frame", "step": step, "image": encode_frame(image)}
31
 
32
- _, _, device = load_model()
33
- yield {"type": "complete", "seed": seed, "device": device}
34
  except Exception as error:
35
  yield {"type": "error", "message": str(error)}
36
 
@@ -47,7 +47,7 @@ class DiffusionHandler(BaseHTTPRequestHandler):
47
  def do_GET(self):
48
  path = urlparse(self.path).path
49
  if path == "/api/health":
50
- body = json.dumps({"model": MODEL_ID, "loaded": is_model_loaded()}).encode()
51
  self.send_bytes(200, "application/json", body)
52
  return
53
 
@@ -57,6 +57,7 @@ class DiffusionHandler(BaseHTTPRequestHandler):
57
  "app.js",
58
  "styles.css",
59
  "assets/celeba_hq_validation_91_256.png",
 
60
  }:
61
  self.send_error(404)
62
  return
@@ -73,6 +74,9 @@ class DiffusionHandler(BaseHTTPRequestHandler):
73
  try:
74
  length = int(self.headers.get("Content-Length", "0"))
75
  request = json.loads(self.rfile.read(length) or b"{}")
 
 
 
76
  seed = int(request.get("seed", secrets.randbelow(2**31)))
77
  if seed < 0 or seed >= 2**63:
78
  raise ValueError("Seed must be between 0 and 2^63 - 1.")
@@ -106,7 +110,7 @@ class DiffusionHandler(BaseHTTPRequestHandler):
106
  self.send_header("X-Accel-Buffering", "no")
107
  self.end_headers()
108
  try:
109
- for event in generate_frames(seed, checkpoints, initial_image):
110
  self.wfile.write((json.dumps(event) + "\n").encode())
111
  self.wfile.flush()
112
  except (BrokenPipeError, ConnectionResetError):
 
8
  from pathlib import Path
9
  from urllib.parse import urlparse
10
 
11
+ from generate import CHECKPOINTS, MODEL_ID, MODEL_SIZES, generate_images, is_model_loaded, load_model
12
 
13
 
14
  ROOT = Path(__file__).resolve().parent
 
20
  return base64.b64encode(buffer.getvalue()).decode("ascii")
21
 
22
 
23
+ def generate_frames(seed, checkpoints=CHECKPOINTS, initial_image=None, model_id=MODEL_ID):
24
+ yield {"type": "status", "status": "loading", "model": model_id}
25
 
26
  try:
27
+ for step, image in generate_images(seed, checkpoints=checkpoints, initial_image=initial_image, model_id=model_id):
28
  yield {"type": "progress", "step": step, "total": 1000}
29
  if image is not None:
30
  yield {"type": "frame", "step": step, "image": encode_frame(image)}
31
 
32
+ _, _, device = load_model(model_id=model_id)
33
+ yield {"type": "complete", "seed": seed, "device": device, "model": model_id}
34
  except Exception as error:
35
  yield {"type": "error", "message": str(error)}
36
 
 
47
  def do_GET(self):
48
  path = urlparse(self.path).path
49
  if path == "/api/health":
50
+ body = json.dumps({"model": MODEL_ID, "models": list(MODEL_SIZES), "loaded": is_model_loaded()}).encode()
51
  self.send_bytes(200, "application/json", body)
52
  return
53
 
 
57
  "app.js",
58
  "styles.css",
59
  "assets/celeba_hq_validation_91_256.png",
60
+ "assets/cifar10_train_23_32.png",
61
  }:
62
  self.send_error(404)
63
  return
 
74
  try:
75
  length = int(self.headers.get("Content-Length", "0"))
76
  request = json.loads(self.rfile.read(length) or b"{}")
77
+ model_id = request.get("model", MODEL_ID)
78
+ if not isinstance(model_id, str) or model_id not in MODEL_SIZES:
79
+ raise ValueError("Unsupported model.")
80
  seed = int(request.get("seed", secrets.randbelow(2**31)))
81
  if seed < 0 or seed >= 2**63:
82
  raise ValueError("Seed must be between 0 and 2^63 - 1.")
 
110
  self.send_header("X-Accel-Buffering", "no")
111
  self.end_headers()
112
  try:
113
+ for event in generate_frames(seed, checkpoints, initial_image, model_id):
114
  self.wfile.write((json.dumps(event) + "\n").encode())
115
  self.wfile.flush()
116
  except (BrokenPipeError, ConnectionResetError):
styles.css CHANGED
@@ -23,8 +23,8 @@ h1,h2 { margin:0; font:600 20px var(--display); } h1{font-size:18px}
23
  .config-rows { max-width:820px; display:grid; gap:8px; }
24
  .config-row { display:grid; grid-template-columns:110px minmax(0,1fr) 70px; align-items:center; min-height:40px; }
25
  .config-row label,.row-label-button { align-self:stretch; display:flex; align-items:center; padding:0 12px; border:1px solid #cfd3c9; border-right:0; border-radius:3px 0 0 3px; color:var(--green); background:#f0f2eb; font:10px var(--mono); }
26
- .config-row input { min-width:0; width:100%; height:40px; padding:0 12px; color:var(--ink); border:1px solid #cfd3c9; border-radius:0 3px 3px 0; outline:none; background:white; font:11px var(--mono); }
27
- .config-row input:focus { position:relative; border-color:var(--green); box-shadow:inset 0 0 0 1px var(--green); }
28
  .config-row input[readonly] { color:#687169; background:#f8f7f2; }
29
  .config-row #model-input,.config-row #seed-input { grid-column:2 / 4; }
30
  .config-row:has(#apply-timesteps) input { border-radius:0; }
 
23
  .config-rows { max-width:820px; display:grid; gap:8px; }
24
  .config-row { display:grid; grid-template-columns:110px minmax(0,1fr) 70px; align-items:center; min-height:40px; }
25
  .config-row label,.row-label-button { align-self:stretch; display:flex; align-items:center; padding:0 12px; border:1px solid #cfd3c9; border-right:0; border-radius:3px 0 0 3px; color:var(--green); background:#f0f2eb; font:10px var(--mono); }
26
+ .config-row input, .config-row select { min-width:0; width:100%; height:40px; padding:0 12px; color:var(--ink); border:1px solid #cfd3c9; border-radius:0 3px 3px 0; outline:none; background:white; font:11px var(--mono); }
27
+ .config-row input:focus, .config-row select:focus { position:relative; border-color:var(--green); box-shadow:inset 0 0 0 1px var(--green); }
28
  .config-row input[readonly] { color:#687169; background:#f8f7f2; }
29
  .config-row #model-input,.config-row #seed-input { grid-column:2 / 4; }
30
  .config-row:has(#apply-timesteps) input { border-radius:0; }