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:
@@ -84,55 +84,20 @@
|
||||
const ckptNameText = document.getElementById('current-ckpt-name');
|
||||
const stepProgressText = document.getElementById('current-step-progress');
|
||||
|
||||
const clientId = 'checkpoints_batch_' + Math.random().toString(36).substring(2, 15);
|
||||
const socket = new WebSocket('{{ COMFYUI_WS_URL }}?clientId=' + clientId);
|
||||
let currentJobId = null;
|
||||
|
||||
const nodeNames = {
|
||||
"3": "Sampling", "11": "Face Detailing", "13": "Hand Detailing",
|
||||
"4": "Loading Models", "16": "Character LoRA", "17": "Outfit LoRA",
|
||||
"18": "Action LoRA", "19": "Style/Detailer LoRA", "8": "Decoding", "9": "Saving"
|
||||
};
|
||||
|
||||
let currentPromptId = null;
|
||||
let resolveGeneration = null;
|
||||
|
||||
socket.addEventListener('message', (event) => {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (msg.type === 'progress') {
|
||||
if (msg.data.prompt_id !== currentPromptId) return;
|
||||
const percent = Math.round((msg.data.value / msg.data.max) * 100);
|
||||
stepProgressText.textContent = `${percent}%`;
|
||||
taskProgressBar.style.width = `${percent}%`;
|
||||
taskProgressBar.textContent = `${percent}%`;
|
||||
taskProgressBar.classList.remove('progress-bar-striped', 'progress-bar-animated');
|
||||
} else if (msg.type === 'executing') {
|
||||
if (msg.data.prompt_id !== currentPromptId) return;
|
||||
const nodeId = msg.data.node;
|
||||
if (nodeId === null) {
|
||||
if (resolveGeneration) resolveGeneration();
|
||||
} else {
|
||||
nodeStatus.textContent = nodeNames[nodeId] || 'Processing...';
|
||||
stepProgressText.textContent = '';
|
||||
if (nodeId !== '3') {
|
||||
taskProgressBar.style.width = '100%';
|
||||
taskProgressBar.textContent = nodeNames[nodeId] || 'Processing...';
|
||||
taskProgressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function waitForCompletion(promptId) {
|
||||
return new Promise((resolve) => {
|
||||
const checkResolve = () => { clearInterval(pollInterval); resolve(); };
|
||||
resolveGeneration = checkResolve;
|
||||
const pollInterval = setInterval(async () => {
|
||||
async function waitForJob(jobId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const poll = setInterval(async () => {
|
||||
try {
|
||||
const resp = await fetch(`/check_status/${promptId}`);
|
||||
const resp = await fetch(`/api/queue/${jobId}/status`);
|
||||
const data = await resp.json();
|
||||
if (data.status === 'finished') checkResolve();
|
||||
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') nodeStatus.textContent = 'Generating…';
|
||||
else nodeStatus.textContent = 'Queued…';
|
||||
} catch (err) {}
|
||||
}, 2000);
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -157,36 +122,32 @@
|
||||
progressBar.textContent = `${percent}%`;
|
||||
statusText.textContent = `Batch Generating: ${completed + 1} / ${missing.length}`;
|
||||
ckptNameText.textContent = `Current: ${ckpt.name}`;
|
||||
nodeStatus.textContent = 'Queuing...';
|
||||
nodeStatus.textContent = 'Queuing…';
|
||||
taskProgressBar.style.width = '100%';
|
||||
taskProgressBar.textContent = 'Queued';
|
||||
taskProgressBar.textContent = '';
|
||||
taskProgressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
|
||||
|
||||
try {
|
||||
const genResp = await fetch(`/checkpoint/${ckpt.slug}/generate`, {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({ 'client_id': clientId, 'character_slug': '__random__' }),
|
||||
body: new URLSearchParams({ 'character_slug': '__random__' }),
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
const genData = await genResp.json();
|
||||
currentPromptId = genData.prompt_id;
|
||||
currentJobId = genData.job_id;
|
||||
|
||||
await waitForCompletion(currentPromptId);
|
||||
const jobResult = await waitForJob(currentJobId);
|
||||
currentJobId = null;
|
||||
|
||||
const finResp = await fetch(`/checkpoint/${ckpt.slug}/finalize_generation/${currentPromptId}`, {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({ 'action': 'replace' })
|
||||
});
|
||||
const finData = await finResp.json();
|
||||
|
||||
if (finData.success) {
|
||||
if (jobResult.result && jobResult.result.image_url) {
|
||||
const img = document.getElementById(`img-${ckpt.slug}`);
|
||||
const noImgSpan = document.getElementById(`no-img-${ckpt.slug}`);
|
||||
if (img) { img.src = finData.image_url; img.classList.remove('d-none'); }
|
||||
if (img) { img.src = jobResult.result.image_url; img.classList.remove('d-none'); }
|
||||
if (noImgSpan) noImgSpan.classList.add('d-none');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed for ${ckpt.name}:`, err);
|
||||
currentJobId = null;
|
||||
}
|
||||
completed++;
|
||||
}
|
||||
@@ -196,7 +157,6 @@
|
||||
statusText.textContent = 'Batch Generation Complete!';
|
||||
ckptNameText.textContent = '';
|
||||
nodeStatus.textContent = 'Done';
|
||||
stepProgressText.textContent = '';
|
||||
taskProgressBar.style.width = '0%';
|
||||
taskProgressBar.textContent = '';
|
||||
batchBtn.disabled = false;
|
||||
|
||||
Reference in New Issue
Block a user