Add background job queue system for generation
- Implements sequential job queue with background worker thread (_enqueue_job, _queue_worker) - All generate routes now return job_id instead of prompt_id; frontend polls /api/queue/<id>/status - Queue management UI in navbar with live badge, job list, pause/resume/remove controls - Fix: replaced url_for() calls inside finalize callbacks with direct string paths (url_for raises RuntimeError without request context in background threads) - Batch cover generation now uses two-phase pattern: queue all jobs upfront, then poll concurrently via Promise.all so page navigation doesn't interrupt the process - Strengths gallery sweep migrated to same two-phase pattern; sgStop() cancels queued jobs server-side - LoRA weight randomisation via lora_weight_min/lora_weight_max already present in _resolve_lora_weight Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -310,73 +310,24 @@
|
||||
const placeholder = document.getElementById('placeholder-text');
|
||||
const resultFooter = document.getElementById('result-footer');
|
||||
|
||||
const clientId = 'generator_view_' + Math.random().toString(36).substring(2, 15);
|
||||
const socket = new WebSocket('{{ COMFYUI_WS_URL }}?clientId=' + clientId);
|
||||
let currentJobId = null;
|
||||
let stopRequested = false;
|
||||
|
||||
const nodeNames = {
|
||||
"3": "Sampling", "4": "Loading Models", "8": "Decoding Image", "9": "Saving Image",
|
||||
"11": "Face Detailing", "13": "Hand Detailing",
|
||||
"16": "Character LoRA", "17": "Outfit LoRA", "18": "Action LoRA", "19": "Style/Detailer LoRA"
|
||||
};
|
||||
|
||||
let currentPromptId = null;
|
||||
let resolveCompletion = null;
|
||||
let stopRequested = false;
|
||||
|
||||
socket.addEventListener('message', (event) => {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (msg.type === 'status') {
|
||||
if (!currentPromptId) {
|
||||
const q = msg.data.status.exec_info.queue_remaining;
|
||||
if (q > 0) progressLbl.textContent = `Queue position: ${q}`;
|
||||
}
|
||||
} else if (msg.type === 'progress') {
|
||||
if (msg.data.prompt_id !== currentPromptId) return;
|
||||
const pct = Math.round((msg.data.value / msg.data.max) * 100);
|
||||
progressBar.style.width = `${pct}%`;
|
||||
progressBar.textContent = `${pct}%`;
|
||||
} else if (msg.type === 'executing') {
|
||||
if (msg.data.prompt_id !== currentPromptId) return;
|
||||
if (msg.data.node === null) {
|
||||
if (resolveCompletion) resolveCompletion();
|
||||
} else {
|
||||
progressLbl.textContent = nodeNames[msg.data.node] || 'Processing...';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function waitForCompletion(promptId) {
|
||||
return new Promise((resolve) => {
|
||||
const done = () => { clearInterval(poll); resolve(); };
|
||||
resolveCompletion = done;
|
||||
async function waitForJob(jobId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const poll = setInterval(async () => {
|
||||
try {
|
||||
const r = await fetch(`/check_status/${promptId}`);
|
||||
if ((await r.json()).status === 'finished') done();
|
||||
} catch (_) {}
|
||||
}, 2000);
|
||||
const resp = await fetch(`/api/queue/${jobId}/status`);
|
||||
const data = await resp.json();
|
||||
if (data.status === 'done') { clearInterval(poll); resolve(data); }
|
||||
else if (data.status === 'failed' || data.status === 'removed') { clearInterval(poll); reject(new Error(data.error || 'Job failed')); }
|
||||
else if (data.status === 'processing') progressLbl.textContent = 'Generating…';
|
||||
else progressLbl.textContent = 'Queued…';
|
||||
} catch (err) {}
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
async function finalizeGeneration(slug, promptId) {
|
||||
progressLbl.textContent = 'Saving image...';
|
||||
try {
|
||||
const r = await fetch(`/generator/finalize/${slug}/${promptId}`, { method: 'POST' });
|
||||
const data = await r.json();
|
||||
if (data.success) {
|
||||
resultImg.src = data.image_url;
|
||||
resultImg.parentElement.classList.remove('d-none');
|
||||
if (placeholder) placeholder.classList.add('d-none');
|
||||
resultFooter.classList.remove('d-none');
|
||||
} else {
|
||||
alert('Save failed: ' + data.error);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('Finalize request failed');
|
||||
}
|
||||
}
|
||||
|
||||
function setGeneratingState(active) {
|
||||
generateBtn.disabled = active;
|
||||
endlessBtn.disabled = active;
|
||||
@@ -388,12 +339,11 @@
|
||||
if (document.getElementById('lucky-dip').checked) applyLuckyDip();
|
||||
|
||||
progressCont.classList.remove('d-none');
|
||||
progressBar.style.width = '0%';
|
||||
progressBar.textContent = '0%';
|
||||
progressBar.style.width = '100%'; progressBar.textContent = '';
|
||||
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
|
||||
progressLbl.textContent = label;
|
||||
|
||||
const fd = new FormData(form);
|
||||
fd.append('client_id', clientId);
|
||||
|
||||
const resp = await fetch(form.action, {
|
||||
method: 'POST', body: fd,
|
||||
@@ -402,15 +352,19 @@
|
||||
const data = await resp.json();
|
||||
if (data.error) throw new Error(data.error);
|
||||
|
||||
currentPromptId = data.prompt_id;
|
||||
progressLbl.textContent = 'Queued...';
|
||||
progressBar.style.width = '100%';
|
||||
progressBar.textContent = 'Queued';
|
||||
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
|
||||
currentJobId = data.job_id;
|
||||
progressLbl.textContent = 'Queued…';
|
||||
|
||||
await waitForCompletion(currentPromptId);
|
||||
await finalizeGeneration(document.getElementById('character').value, currentPromptId);
|
||||
currentPromptId = null;
|
||||
const jobResult = await waitForJob(currentJobId);
|
||||
currentJobId = null;
|
||||
|
||||
if (jobResult.result && jobResult.result.image_url) {
|
||||
resultImg.src = jobResult.result.image_url;
|
||||
resultImg.parentElement.classList.remove('d-none');
|
||||
if (placeholder) placeholder.classList.add('d-none');
|
||||
resultFooter.classList.remove('d-none');
|
||||
}
|
||||
progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated');
|
||||
}
|
||||
|
||||
async function runLoop(endless) {
|
||||
|
||||
Reference in New Issue
Block a user