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.
665 lines
30 KiB
665 lines
30 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>
|
|
|
|
<div class="button-row">
|
|
<button type="submit" id="generate-btn">Generate</button>
|
|
<button type="button" id="stop-btn" style="display: none;">Stop</button>
|
|
</div>
|
|
</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 stopBtn = document.getElementById('stop-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');
|
|
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');
|
|
|
|
let timePerImage = null;
|
|
let progressInterval = null;
|
|
let imageStartTime = null;
|
|
let isGenerating = false;
|
|
let abortController = null;
|
|
|
|
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 = '';
|
|
});
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
stopBtn.addEventListener('click', async () => {
|
|
stopBtn.disabled = true;
|
|
stopBtn.textContent = 'Stopping...';
|
|
try {
|
|
await fetch('/stop', { method: 'POST' });
|
|
if (abortController) {
|
|
abortController.abort();
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to stop:', e);
|
|
}
|
|
});
|
|
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const prompt = document.getElementById('prompt').value.trim();
|
|
if (!prompt) {
|
|
setStatus('Please enter a prompt', 'error');
|
|
return;
|
|
}
|
|
|
|
isGenerating = true;
|
|
abortController = new AbortController();
|
|
generateBtn.disabled = true;
|
|
generateBtn.textContent = 'Generating...';
|
|
stopBtn.style.display = 'inline-block';
|
|
stopBtn.disabled = false;
|
|
stopBtn.textContent = 'Stop';
|
|
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 = seedValue === '' || incrementSeedCheckbox.checked || varyGuidanceCheckbox.checked || varyStepsCheckbox.checked;
|
|
if (requestedCount > 1 && !hasVaryOption) {
|
|
requestedCount = 1;
|
|
}
|
|
|
|
setStatus(requestedCount === 1 ? 'Generating image...' : `Generating image 1 of ${requestedCount}...`, 'loading');
|
|
|
|
localStorage.setItem('settings', JSON.stringify(getSettings()));
|
|
|
|
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),
|
|
signal: abortController.signal
|
|
});
|
|
|
|
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) {
|
|
if (error.name === 'AbortError') {
|
|
setStatus(imageCount > 0 ? `Stopped after ${imageCount} image(s)` : 'Generation stopped', 'success');
|
|
} else {
|
|
setStatus('Error: ' + error.message, 'error');
|
|
}
|
|
stopProgressTimer();
|
|
showProgress(false);
|
|
} finally {
|
|
isGenerating = false;
|
|
abortController = null;
|
|
generateBtn.disabled = false;
|
|
generateBtn.textContent = 'Generate';
|
|
stopBtn.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
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);
|
|
}
|
|
|
|
window.addEventListener('load', function() {
|
|
let settings = localStorage.getItem('settings');
|
|
if (settings !== null) {
|
|
applySettings(JSON.parse(settings));
|
|
}
|
|
})
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|