Spaces:
Sleeping
Sleeping
- added support for google/ddpm-cifar10-32
Browse files- click image to show it in its original resolution
- .gitattributes +1 -0
- README.md +10 -3
- app.js +47 -9
- app.py +61 -33
- assets/cifar10_train_23_32.png +3 -0
- download_cifar10.py +83 -0
- generate.py +18 -13
- index.html +5 -2
- server.py +12 -8
- 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
|
| 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>.
|
| 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
|
| 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
|
| 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 =
|
| 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
|
| 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 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 ${
|
| 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 = () =>
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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((
|
| 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((
|
| 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((
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 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:
|
| 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((
|
| 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}% -
|
| 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.
|
| 341 |
-
scale=10, min_width=0,
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
| 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
|
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 |
-
|
| 48 |
-
|
|
|
|
| 49 |
pipeline = DDPMPipeline.from_pretrained(
|
| 50 |
-
|
| 51 |
pipeline.to(device)
|
| 52 |
-
_PIPELINES[
|
| 53 |
-
_PIPELINE = _PIPELINES[
|
| 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
|
| 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"{
|
| 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 |
-
<
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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":
|
| 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; }
|