Small python webui for SD1.5 and SDXL
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

625 lines
28 KiB

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stable Diffusion Generator</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<div class="panel-left">
<h1>Stable Diffusion Generator</h1>
<form id="generate-form">
<div class="form-group">
<label for="prompt">Prompt</label>
<textarea id="prompt" name="prompt" rows="3" placeholder="Enter your prompt here..."></textarea>
</div>
<div class="form-group">
<label for="negative-prompt">Negative Prompt</label>
<textarea id="negative-prompt" name="negative_prompt" rows="2" placeholder="Things to avoid in the image..."></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="seed">Seed</label>
<div class="seed-input">
<input type="number" id="seed" name="seed" placeholder="Random">
<button type="button" id="random-seed">Random</button>
</div>
</div>
<div class="form-group">
<label for="count">Number of Images</label>
<input type="number" id="count" name="count" value="1" min="1" max="50">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="width">Width</label>
<select id="width" name="width">
<option value="128">128</option>
<option value="256">256</option>
<option value="512" selected>512 (SD 1.5)</option>
<option value="768">768</option>
<option value="1024">1024 (SDXL)</option>
</select>
</div>
<div class="form-group">
<label for="height">Height</label>
<select id="height" name="height">
<option value="128">128</option>
<option value="256">256</option>
<option value="512" selected>512 (SD 1.5)</option>
<option value="768">768</option>
<option value="1024">1024 (SDXL)</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group" id="steps-group">
<label for="steps">Steps: <span id="steps-value">20</span></label>
<input type="range" id="steps" name="steps" min="1" max="100" value="20">
</div>
<div class="form-group" id="guidance-group">
<label for="guidance">Guidance Scale: <span id="guidance-value">7.5</span></label>
<input type="range" id="guidance" name="guidance_scale" min="1" max="20" step="0.5" value="7.5">
</div>
</div>
<div class="form-group checkbox-group">
<label>
<input type="checkbox" id="quality-keywords" name="add_quality_keywords" checked>
Add quality keywords
</label>
</div>
<div class="form-group checkbox-group">
<label>
<input type="checkbox" id="increment-seed" name="increment_seed" checked>
Increment seed
</label>
</div>
<div class="form-group checkbox-group">
<label>
<input type="checkbox" id="vary-guidance" name="vary_guidance">
Vary guidance
</label>
<div class="range-inputs" id="guidance-range" style="display: none;">
<input type="number" id="guidance-low" value="5" min="1" max="20" step="0.5">
<span>to</span>
<input type="number" id="guidance-high" value="12" min="1" max="20" step="0.5">
</div>
</div>
<div class="form-group checkbox-group">
<label>
<input type="checkbox" id="vary-steps" name="vary_steps">
Vary steps
</label>
<div class="range-inputs" id="steps-range" style="display: none;">
<input type="number" id="steps-low" value="20" min="1" max="100">
<span>to</span>
<input type="number" id="steps-high" value="80" min="1" max="100">
</div>
</div>
<button type="submit" id="generate-btn">Generate</button>
</form>
<div class="settings-buttons">
<button type="button" id="export-btn">Export Settings</button>
<button type="button" id="import-btn">Import Settings</button>
<input type="file" id="import-file" accept=".json" hidden>
</div>
<div id="status" class="status">
<div class="spinner"></div>
<span id="status-text"></span>
</div>
<div id="progress-container" class="progress-container">
<div class="progress-bar">
<div id="progress-fill" class="progress-fill"></div>
</div>
<div id="progress-text" class="progress-text"></div>
</div>
</div>
<div class="panel-right">
<div id="results" class="results"></div>
</div>
</div>
<script>
// Configuration constants
const CONFIG = {
GUIDANCE_MIN: 1,
GUIDANCE_MAX: 20,
GUIDANCE_SPREAD: 2.5,
STEPS_MIN: 1,
STEPS_MAX: 100,
STEPS_SPREAD: 15,
DEFAULT_TIME_ESTIMATE: 20
};
const form = document.getElementById('generate-form');
const generateBtn = document.getElementById('generate-btn');
const statusDiv = document.getElementById('status');
const statusText = document.getElementById('status-text');
const progressContainer = document.getElementById('progress-container');
const progressFill = document.getElementById('progress-fill');
const progressText = document.getElementById('progress-text');
const results = document.getElementById('results');
const stepsSlider = document.getElementById('steps');
const stepsValue = document.getElementById('steps-value');
const guidanceSlider = document.getElementById('guidance');
const guidanceValue = document.getElementById('guidance-value');
const randomSeedBtn = document.getElementById('random-seed');
const seedInput = document.getElementById('seed');
let timePerImage = null;
let progressInterval = null;
let imageStartTime = null;
const incrementSeedCheckbox = document.getElementById('increment-seed');
const varyGuidanceCheckbox = document.getElementById('vary-guidance');
const varyStepsCheckbox = document.getElementById('vary-steps');
const guidanceRangeDiv = document.getElementById('guidance-range');
const stepsRangeDiv = document.getElementById('steps-range');
const guidanceGroup = document.getElementById('guidance-group');
const stepsGroup = document.getElementById('steps-group');
const guidanceLow = document.getElementById('guidance-low');
const guidanceHigh = document.getElementById('guidance-high');
const stepsLow = document.getElementById('steps-low');
const stepsHigh = document.getElementById('steps-high');
const countInput = document.getElementById('count');
const incrementSeedGroup = incrementSeedCheckbox.closest('.form-group');
const varyGuidanceGroup = varyGuidanceCheckbox.closest('.form-group');
const varyStepsGroup = varyStepsCheckbox.closest('.form-group');
function updateVaryOptionsVisibility() {
const count = parseInt(countInput.value) || 1;
const showVary = count > 1;
incrementSeedGroup.style.display = showVary ? 'block' : 'none';
varyGuidanceGroup.style.display = showVary ? 'block' : 'none';
varyStepsGroup.style.display = showVary ? 'block' : 'none';
}
countInput.addEventListener('input', updateVaryOptionsVisibility);
updateVaryOptionsVisibility();
stepsSlider.addEventListener('input', () => {
stepsValue.textContent = stepsSlider.value;
timePerImage = null;
if (!varyStepsCheckbox.checked) {
const val = parseInt(stepsSlider.value);
stepsLow.value = Math.max(CONFIG.STEPS_MIN, val - CONFIG.STEPS_SPREAD);
stepsHigh.value = Math.min(CONFIG.STEPS_MAX, val + CONFIG.STEPS_SPREAD);
}
});
guidanceSlider.addEventListener('input', () => {
guidanceValue.textContent = guidanceSlider.value;
if (!varyGuidanceCheckbox.checked) {
const val = parseFloat(guidanceSlider.value);
guidanceLow.value = Math.max(CONFIG.GUIDANCE_MIN, val - CONFIG.GUIDANCE_SPREAD);
guidanceHigh.value = Math.min(CONFIG.GUIDANCE_MAX, val + CONFIG.GUIDANCE_SPREAD);
}
});
randomSeedBtn.addEventListener('click', () => {
seedInput.value = Math.floor(100000000 + Math.random() * 900000000);
});
document.getElementById('prompt').addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'Enter') {
e.preventDefault();
form.requestSubmit();
}
});
varyGuidanceCheckbox.addEventListener('change', () => {
guidanceRangeDiv.style.display = varyGuidanceCheckbox.checked ? 'flex' : 'none';
guidanceGroup.style.display = varyGuidanceCheckbox.checked ? 'none' : 'block';
});
varyStepsCheckbox.addEventListener('change', () => {
stepsRangeDiv.style.display = varyStepsCheckbox.checked ? 'flex' : 'none';
stepsGroup.style.display = varyStepsCheckbox.checked ? 'none' : 'block';
});
stepsLow.addEventListener('input', () => { timePerImage = null; });
stepsHigh.addEventListener('input', () => { timePerImage = null; });
document.getElementById('width').addEventListener('change', () => { timePerImage = null; });
document.getElementById('height').addEventListener('change', () => { timePerImage = null; });
const exportBtn = document.getElementById('export-btn');
const importBtn = document.getElementById('import-btn');
const importFile = document.getElementById('import-file');
// Initialize range values based on default slider values
(function initRanges() {
const gVal = parseFloat(guidanceSlider.value);
guidanceLow.value = Math.max(CONFIG.GUIDANCE_MIN, gVal - CONFIG.GUIDANCE_SPREAD);
guidanceHigh.value = Math.min(CONFIG.GUIDANCE_MAX, gVal + CONFIG.GUIDANCE_SPREAD);
const sVal = parseInt(stepsSlider.value);
stepsLow.value = Math.max(CONFIG.STEPS_MIN, sVal - CONFIG.STEPS_SPREAD);
stepsHigh.value = Math.min(CONFIG.STEPS_MAX, sVal + CONFIG.STEPS_SPREAD);
})();
function getSettings() {
return {
prompt: document.getElementById('prompt').value,
negative_prompt: document.getElementById('negative-prompt').value,
seed: seedInput.value ? parseInt(seedInput.value) : null,
steps: parseInt(stepsSlider.value),
guidance_scale: parseFloat(guidanceSlider.value),
count: parseInt(document.getElementById('count').value),
width: parseInt(document.getElementById('width').value),
height: parseInt(document.getElementById('height').value),
add_quality_keywords: document.getElementById('quality-keywords').checked,
increment_seed: incrementSeedCheckbox.checked,
vary_guidance: varyGuidanceCheckbox.checked,
guidance_low: parseFloat(guidanceLow.value),
guidance_high: parseFloat(guidanceHigh.value),
vary_steps: varyStepsCheckbox.checked,
steps_low: parseInt(stepsLow.value),
steps_high: parseInt(stepsHigh.value)
};
}
function applySettings(settings) {
if (settings.prompt !== undefined) {
document.getElementById('prompt').value = settings.prompt;
}
if (settings.negative_prompt !== undefined) {
document.getElementById('negative-prompt').value = settings.negative_prompt;
}
if (settings.seed !== undefined && settings.seed !== null) {
seedInput.value = settings.seed;
} else {
seedInput.value = '';
}
if (settings.steps !== undefined) {
stepsSlider.value = settings.steps;
stepsValue.textContent = settings.steps;
}
if (settings.guidance_scale !== undefined) {
guidanceSlider.value = settings.guidance_scale;
guidanceValue.textContent = settings.guidance_scale;
}
if (settings.count !== undefined) {
document.getElementById('count').value = settings.count;
}
if (settings.width !== undefined) {
document.getElementById('width').value = settings.width;
}
if (settings.height !== undefined) {
document.getElementById('height').value = settings.height;
}
if (settings.add_quality_keywords !== undefined) {
document.getElementById('quality-keywords').checked = settings.add_quality_keywords;
}
if (settings.increment_seed !== undefined) {
incrementSeedCheckbox.checked = settings.increment_seed;
}
if (settings.vary_guidance !== undefined) {
varyGuidanceCheckbox.checked = settings.vary_guidance;
guidanceRangeDiv.style.display = settings.vary_guidance ? 'flex' : 'none';
guidanceGroup.style.display = settings.vary_guidance ? 'none' : 'block';
}
if (settings.guidance_low !== undefined) {
guidanceLow.value = settings.guidance_low;
}
if (settings.guidance_high !== undefined) {
guidanceHigh.value = settings.guidance_high;
}
if (settings.vary_steps !== undefined) {
varyStepsCheckbox.checked = settings.vary_steps;
stepsRangeDiv.style.display = settings.vary_steps ? 'flex' : 'none';
stepsGroup.style.display = settings.vary_steps ? 'none' : 'block';
}
if (settings.steps_low !== undefined) {
stepsLow.value = settings.steps_low;
}
if (settings.steps_high !== undefined) {
stepsHigh.value = settings.steps_high;
}
// Sync ranges to slider values if vary mode is off
if (!varyGuidanceCheckbox.checked) {
const gVal = parseFloat(guidanceSlider.value);
guidanceLow.value = Math.max(CONFIG.GUIDANCE_MIN, gVal - CONFIG.GUIDANCE_SPREAD);
guidanceHigh.value = Math.min(CONFIG.GUIDANCE_MAX, gVal + CONFIG.GUIDANCE_SPREAD);
}
if (!varyStepsCheckbox.checked) {
const sVal = parseInt(stepsSlider.value);
stepsLow.value = Math.max(CONFIG.STEPS_MIN, sVal - CONFIG.STEPS_SPREAD);
stepsHigh.value = Math.min(CONFIG.STEPS_MAX, sVal + CONFIG.STEPS_SPREAD);
}
}
exportBtn.addEventListener('click', () => {
const settings = getSettings();
const json = JSON.stringify(settings, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'sd-settings.json';
a.click();
URL.revokeObjectURL(url);
});
importBtn.addEventListener('click', () => {
importFile.click();
});
importFile.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
try {
const settings = JSON.parse(event.target.result);
applySettings(settings);
timePerImage = null;
setStatus('Settings imported', 'success');
} catch (err) {
setStatus('Invalid settings file', 'error');
}
};
reader.readAsText(file);
importFile.value = '';
});
function setStatus(text, type) {
statusText.textContent = text;
statusDiv.className = 'status ' + type;
}
function showProgress(show) {
progressContainer.style.display = show ? 'block' : 'none';
}
function updateProgress(percent, eta) {
progressFill.style.width = Math.min(100, percent) + '%';
if (eta !== null) {
progressText.textContent = `${Math.round(percent)}% - ETA: ${Math.ceil(eta)}s`;
} else {
progressText.textContent = `${Math.round(percent)}%`;
}
}
function startProgressTimer(estimatedTime) {
imageStartTime = Date.now();
stopProgressTimer();
progressInterval = setInterval(() => {
const elapsed = (Date.now() - imageStartTime) / 1000;
let percent, eta;
if (elapsed < estimatedTime * 0.9) {
percent = (elapsed / estimatedTime) * 100;
eta = estimatedTime - elapsed;
} else {
const overTime = elapsed - (estimatedTime * 0.9);
const slowFactor = 1 + overTime * 0.5;
percent = 90 + (9 * (1 - 1 / slowFactor));
eta = null;
}
updateProgress(Math.min(99, percent), eta);
}, 100);
}
function stopProgressTimer() {
if (progressInterval) {
clearInterval(progressInterval);
progressInterval = null;
}
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
const prompt = document.getElementById('prompt').value.trim();
if (!prompt) {
setStatus('Please enter a prompt', 'error');
return;
}
generateBtn.disabled = true;
generateBtn.textContent = 'Generating...';
results.innerHTML = '';
showProgress(true);
const seedValue = seedInput.value.trim();
let requestedCount = parseInt(document.getElementById('count').value);
// If count > 1 but no vary options enabled, reduce to 1
const hasVaryOption = incrementSeedCheckbox.checked || varyGuidanceCheckbox.checked || varyStepsCheckbox.checked;
if (requestedCount > 1 && !hasVaryOption) {
requestedCount = 1;
}
setStatus(requestedCount === 1 ? 'Generating image...' : `Generating image 1 of ${requestedCount}...`, 'loading');
const data = {
prompt: prompt,
negative_prompt: document.getElementById('negative-prompt').value.trim(),
seed: seedValue ? parseInt(seedValue) : null,
steps: parseInt(stepsSlider.value),
guidance_scale: parseFloat(guidanceSlider.value),
count: requestedCount,
width: parseInt(document.getElementById('width').value),
height: parseInt(document.getElementById('height').value),
add_quality_keywords: document.getElementById('quality-keywords').checked,
increment_seed: incrementSeedCheckbox.checked,
vary_guidance: varyGuidanceCheckbox.checked,
guidance_low: parseFloat(document.getElementById('guidance-low').value),
guidance_high: parseFloat(document.getElementById('guidance-high').value),
vary_steps: varyStepsCheckbox.checked,
steps_low: parseInt(document.getElementById('steps-low').value),
steps_high: parseInt(document.getElementById('steps-high').value)
};
let imageCount = 0;
let generationStartTime = Date.now();
const estimate = timePerImage || CONFIG.DEFAULT_TIME_ESTIMATE;
updateProgress(0, estimate);
startProgressTimer(estimate);
try {
const response = await fetch('/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop();
for (const line of lines) {
if (line.startsWith('data: ')) {
const jsonStr = line.slice(6);
try {
const eventData = JSON.parse(jsonStr);
if (eventData.error) {
setStatus('Error: ' + eventData.error, 'error');
stopProgressTimer();
showProgress(false);
} else if (eventData.done) {
setStatus(`Generated ${imageCount} image(s)`, 'success');
stopProgressTimer();
showProgress(false);
} else {
const now = Date.now();
if (imageCount === 0) {
timePerImage = (now - generationStartTime) / 1000;
}
imageCount++;
stopProgressTimer();
updateProgress(100, null);
addImageCard(eventData);
if (eventData.index < eventData.total) {
const statusText = eventData.total === 1
? 'Generating image...'
: `Generating image ${eventData.index + 1} of ${eventData.total}...`;
setStatus(statusText, 'loading');
updateProgress(0, timePerImage);
startProgressTimer(timePerImage);
}
}
} catch (e) {
console.error('Failed to parse SSE data:', e);
}
}
}
}
} catch (error) {
setStatus('Error: ' + error.message, 'error');
stopProgressTimer();
showProgress(false);
} finally {
generateBtn.disabled = false;
generateBtn.textContent = 'Generate';
}
});
function addImageCard(img) {
const card = document.createElement('div');
card.className = 'image-card';
const guidance = typeof img.guidance_scale === 'number'
? img.guidance_scale.toFixed(1)
: img.guidance_scale;
const negativePromptHtml = img.negative_prompt
? `<p><strong>Negative:</strong> ${img.negative_prompt}</p>`
: '';
card.innerHTML = `
<a href="${img.url}" target="_blank"><img src="${img.base64}" alt="Generated image"></a>
<div class="image-info">
<p><strong>Seed:</strong> ${img.seed} | <strong>Steps:</strong> ${img.steps} | <strong>Guidance:</strong> ${guidance} | <strong>Size:</strong> ${img.width}x${img.height}</p>
<p><strong>Prompt:</strong> ${img.prompt}</p>
${negativePromptHtml}
<p><a href="${img.url}" target="_blank">Open saved image</a> | <button type="button" class="use-settings-btn">Use Settings</button> | <button type="button" class="download-settings-btn">Download Settings</button></p>
</div>
`;
card.querySelector('.download-settings-btn').addEventListener('click', () => {
const settings = {
prompt: img.prompt,
negative_prompt: img.negative_prompt || '',
seed: img.seed,
steps: img.steps,
guidance_scale: img.guidance_scale,
width: img.width,
height: img.height
};
const json = JSON.stringify(settings, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `sd-settings-${img.seed}.json`;
a.click();
URL.revokeObjectURL(url);
});
card.querySelector('.use-settings-btn').addEventListener('click', () => {
seedInput.value = img.seed;
document.getElementById('negative-prompt').value = img.negative_prompt || '';
if (parseInt(stepsSlider.value) !== img.steps) {
timePerImage = null;
}
stepsSlider.value = img.steps;
stepsValue.textContent = img.steps;
guidanceSlider.value = img.guidance_scale;
guidanceValue.textContent = guidance;
document.getElementById('width').value = img.width;
document.getElementById('height').value = img.height;
countInput.value = 1;
updateVaryOptionsVisibility();
// Disable vary modes to use exact settings
varyGuidanceCheckbox.checked = false;
guidanceRangeDiv.style.display = 'none';
guidanceGroup.style.display = 'block';
varyStepsCheckbox.checked = false;
stepsRangeDiv.style.display = 'none';
stepsGroup.style.display = 'block';
timePerImage = null;
});
results.appendChild(card);
}
</script>
</body>
</html>