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:
Aodhan Collins
2026-03-03 02:32:50 +00:00
parent ae7ba961c1
commit 3c828a170f
21 changed files with 1451 additions and 2146 deletions

View File

@@ -326,187 +326,72 @@
}
});
// Generate a unique client ID
const clientId = 'action_detail_' + Math.random().toString(36).substring(2, 15);
// ComfyUI WebSocket
const socket = new WebSocket('{{ COMFYUI_WS_URL }}?clientId=' + clientId);
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 Image",
"9": "Saving Image"
};
let currentPromptId = null;
let currentJobId = null;
let currentAction = null;
socket.addEventListener('message', (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'status') {
if (!currentPromptId) {
const queueRemaining = msg.data.status.exec_info.queue_remaining;
if (queueRemaining > 0) {
progressLabel.textContent = `Queue position: ${queueRemaining}`;
}
}
}
else if (msg.type === 'progress') {
if (msg.data.prompt_id !== currentPromptId) return;
const value = msg.data.value;
const max = msg.data.max;
const percent = Math.round((value / max) * 100);
progressBar.style.width = `${percent}%`;
progressBar.textContent = `${percent}%`;
}
else if (msg.type === 'executing') {
if (msg.data.prompt_id !== currentPromptId) return;
const nodeId = msg.data.node;
if (nodeId === null) {
// Execution finished via WebSocket
console.log('Finished via WebSocket');
if (resolveCompletion) resolveCompletion();
} else {
const nodeName = nodeNames[nodeId] || `Processing...`;
progressLabel.textContent = nodeName;
}
}
});
let resolveCompletion = null;
async function waitForCompletion(promptId) {
return new Promise((resolve) => {
const checkResolve = () => {
clearInterval(pollInterval);
resolve();
};
resolveCompletion = checkResolve;
// Fallback polling in case WebSocket is blocked (403)
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') {
console.log('Finished via Polling');
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') {
progressLabel.textContent = 'Generating…';
} else {
progressLabel.textContent = 'Queued…';
}
} catch (err) { console.error('Polling error:', err); }
}, 2000);
} catch (err) { console.error('Poll error:', err); }
}, 1500);
});
}
form.addEventListener('submit', async (e) => {
// Only intercept generate actions
const submitter = e.submitter;
if (!submitter || submitter.value !== 'preview') {
return;
}
if (!submitter || submitter.value !== 'preview') return;
e.preventDefault();
currentAction = submitter.value;
const formData = new FormData(form);
formData.append('action', currentAction);
formData.append('client_id', clientId);
// UI Reset
progressContainer.classList.remove('d-none');
progressBar.style.width = '0%';
progressBar.textContent = '0%';
progressLabel.textContent = 'Starting...';
progressBar.style.width = '100%';
progressBar.textContent = '';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
progressLabel.textContent = 'Queuing…';
try {
const response = await fetch(form.getAttribute('action'), {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
const data = await response.json();
if (data.error) {
alert('Error: ' + data.error);
progressContainer.classList.add('d-none');
return;
}
currentPromptId = data.prompt_id;
progressLabel.textContent = 'Queued...';
progressBar.style.width = '100%';
progressBar.textContent = 'Queued';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
// Wait for completion (WebSocket or Polling)
await waitForCompletion(currentPromptId);
// Finalize
finalizeGeneration(currentPromptId, currentAction);
currentPromptId = null;
} catch (err) {
console.error(err);
alert('Request failed');
progressContainer.classList.add('d-none');
}
});
async function finalizeGeneration(promptId, action) {
progressLabel.textContent = 'Saving image...';
const url = `/action/{{ action.slug }}/finalize_generation/${promptId}`;
const formData = new FormData();
formData.append('action', 'preview'); // Always save as preview
try {
const response = await fetch(url, {
method: 'POST',
body: formData
method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await response.json();
if (data.success) {
// Update preview image
previewImg.src = data.image_url;
if (data.error) { alert('Error: ' + data.error); progressContainer.classList.add('d-none'); return; }
currentJobId = data.job_id;
const jobResult = await waitForJob(currentJobId);
currentJobId = null;
if (jobResult.result && jobResult.result.image_url) {
previewImg.src = jobResult.result.image_url;
if (previewCard) previewCard.classList.remove('d-none');
// Enable the replace cover button if it exists
const replaceBtn = document.getElementById('replace-cover-btn');
if (replaceBtn) {
replaceBtn.disabled = false;
// Check if there's a form to update
const form = replaceBtn.closest('form');
if (form) {
form.action = `/action/{{ action.slug }}/replace_cover_from_preview`;
}
}
} else {
alert('Save failed: ' + data.error);
if (replaceBtn) replaceBtn.disabled = false;
}
} catch (err) {
console.error(err);
alert('Finalize request failed');
alert('Generation failed: ' + err.message);
} finally {
progressContainer.classList.add('d-none');
progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated');
}
}
});
// Batch: Generate All Characters
const allCharacters = [
{% for char in characters %}{ slug: "{{ char.slug }}", name: {{ char.name | tojson }} },
{% endfor %}
];
const finalizeBaseUrl = '/action/{{ action.slug }}/finalize_generation';
let stopBatch = false;
const generateAllBtn = document.getElementById('generate-all-btn');
@@ -542,54 +427,43 @@
stopAllBtn.classList.remove('d-none');
batchProgress.classList.remove('d-none');
bootstrap.Tab.getOrCreateInstance(document.getElementById('previews-tab')).show();
for (let i = 0; i < allCharacters.length; i++) {
if (stopBatch) break;
const char = allCharacters[i];
batchBar.style.width = `${Math.round((i / allCharacters.length) * 100)}%`;
batchLabel.textContent = `Generating ${char.name} (${i + 1} / ${allCharacters.length})`;
const genForm = document.getElementById('generate-form');
const fd = new FormData();
genForm.querySelectorAll('input[name="include_field"]:checked').forEach(cb => fd.append('include_field', cb.value));
fd.append('character_slug', char.slug);
fd.append('action', 'preview');
fd.append('client_id', clientId);
try {
progressContainer.classList.remove('d-none');
progressBar.style.width = '0%';
progressBar.textContent = '0%';
progressLabel.textContent = `${char.name}: Starting...`;
progressBar.style.width = '100%';
progressBar.textContent = '';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
progressLabel.textContent = `${char.name}: Queuing…`;
const resp = await fetch(genForm.getAttribute('action'), {
method: 'POST', body: fd, headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await resp.json();
if (data.error) { console.error(`Error for ${char.name}:`, data.error); progressContainer.classList.add('d-none'); continue; }
currentPromptId = data.prompt_id;
await waitForCompletion(currentPromptId);
progressLabel.textContent = 'Saving image...';
const finalFD = new FormData();
finalFD.append('action', 'preview');
const finalResp = await fetch(`${finalizeBaseUrl}/${currentPromptId}`, { method: 'POST', body: finalFD });
const finalData = await finalResp.json();
if (finalData.success) {
addToPreviewGallery(finalData.image_url, char.name);
previewImg.src = finalData.image_url;
currentJobId = data.job_id;
const jobResult = await waitForJob(currentJobId);
currentJobId = null;
if (jobResult.result && jobResult.result.image_url) {
addToPreviewGallery(jobResult.result.image_url, char.name);
previewImg.src = jobResult.result.image_url;
if (previewCard) previewCard.classList.remove('d-none');
}
currentPromptId = null;
} catch (err) {
console.error(`Failed for ${char.name}:`, err);
currentPromptId = null;
currentJobId = null;
} finally {
progressContainer.classList.add('d-none');
progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated');
}
}
batchBar.style.width = '100%';
batchLabel.textContent = stopBatch ? 'Stopped.' : 'Complete!';
generateAllBtn.disabled = false;

View File

@@ -102,71 +102,18 @@
const itemNameText = document.getElementById('current-item-name');
const stepProgressText = document.getElementById('current-step-progress');
const clientId = 'actions_batch_' + Math.random().toString(36).substring(2, 15);
const socket = new WebSocket('{{ COMFYUI_WS_URL }}?clientId=' + clientId);
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 value = msg.data.value;
const max = msg.data.max;
const percent = Math.round((value / 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);
});
}
@@ -183,68 +130,65 @@
batchBtn.disabled = true;
regenAllBtn.disabled = true;
container.classList.remove('d-none');
let completed = 0;
for (const item of missing) {
const percent = Math.round((completed / missing.length) * 100);
progressBar.style.width = `${percent}%`;
progressBar.textContent = `${percent}%`;
statusText.textContent = `Batch Generating Actions: ${completed + 1} / ${missing.length}`;
itemNameText.textContent = `Current: ${item.name}`;
nodeStatus.textContent = "Queuing...";
taskProgressBar.style.width = '100%';
taskProgressBar.textContent = 'Queued';
taskProgressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
// Phase 1: Queue all jobs upfront
progressBar.style.width = '100%';
progressBar.textContent = '';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
nodeStatus.textContent = 'Queuing…';
const jobs = [];
for (const item of missing) {
statusText.textContent = `Queuing ${jobs.length + 1} / ${missing.length}`;
try {
// Random character for action preview
const genResp = await fetch(`/action/${item.slug}/generate`, {
method: 'POST',
body: new URLSearchParams({
'action': 'replace',
'client_id': clientId,
'character_slug': '__random__'
}),
body: new URLSearchParams({ action: 'replace', character_slug: '__random__' }),
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const genData = await genResp.json();
currentPromptId = genData.prompt_id;
if (genData.job_id) jobs.push({ item, jobId: genData.job_id });
} catch (err) {
console.error(`Failed to queue ${item.name}:`, err);
}
}
await waitForCompletion(currentPromptId);
// Phase 2: Poll all concurrently
progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated');
progressBar.style.width = '0%';
progressBar.textContent = '0%';
statusText.textContent = `0 / ${jobs.length} done`;
const finResp = await fetch(`/action/${item.slug}/finalize_generation/${currentPromptId}`, {
method: 'POST',
body: new URLSearchParams({ 'action': 'replace' })
});
const finData = await finResp.json();
if (finData.success) {
let completed = 0;
await Promise.all(jobs.map(async ({ item, jobId }) => {
try {
const jobResult = await waitForJob(jobId);
if (jobResult.result && jobResult.result.image_url) {
const img = document.getElementById(`img-${item.slug}`);
const noImgSpan = document.getElementById(`no-img-${item.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 ${item.name}:`, err);
}
completed++;
}
const pct = Math.round((completed / jobs.length) * 100);
progressBar.style.width = `${pct}%`;
progressBar.textContent = `${pct}%`;
statusText.textContent = `${completed} / ${jobs.length} done`;
}));
progressBar.style.width = '100%';
progressBar.textContent = '100%';
statusText.textContent = "Batch Action Generation Complete!";
itemNameText.textContent = "";
nodeStatus.textContent = "Done";
stepProgressText.textContent = "";
statusText.textContent = 'Batch Action Generation Complete!';
itemNameText.textContent = '';
nodeStatus.textContent = 'Done';
taskProgressBar.style.width = '0%';
taskProgressBar.textContent = '';
batchBtn.disabled = false;
regenAllBtn.disabled = false;
setTimeout(() => { container.classList.add('d-none'); }, 5000);
setTimeout(() => container.classList.add('d-none'), 5000);
}
batchBtn.addEventListener('click', async () => {

View File

@@ -261,52 +261,20 @@
const previewCard = document.getElementById('preview-card');
const previewImg = document.getElementById('preview-img');
const clientId = 'checkpoint_detail_' + 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 Image", "9": "Saving Image"
};
let currentPromptId = null;
let resolveCompletion = null;
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) progressLabel.textContent = `Queue position: ${q}`;
}
} else if (msg.type === 'progress') {
if (msg.data.prompt_id !== currentPromptId) return;
const percent = Math.round((msg.data.value / msg.data.max) * 100);
progressBar.style.width = `${percent}%`;
progressBar.textContent = `${percent}%`;
} else if (msg.type === 'executing') {
if (msg.data.prompt_id !== currentPromptId) return;
const nodeId = msg.data.node;
if (nodeId === null) {
if (resolveCompletion) resolveCompletion();
} else {
progressLabel.textContent = nodeNames[nodeId] || 'Processing...';
}
}
});
async function waitForCompletion(promptId) {
return new Promise((resolve) => {
const checkResolve = () => { clearInterval(pollInterval); resolve(); };
resolveCompletion = 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') progressLabel.textContent = 'Generating…';
else progressLabel.textContent = 'Queued…';
} catch (err) {}
}, 2000);
}, 1500);
});
}
@@ -314,76 +282,36 @@
const submitter = e.submitter;
if (!submitter || submitter.value !== 'preview') return;
e.preventDefault();
const formData = new FormData(form);
formData.append('action', 'preview');
formData.append('client_id', clientId);
progressContainer.classList.remove('d-none');
progressBar.style.width = '0%';
progressBar.textContent = '0%';
progressLabel.textContent = 'Starting...';
progressBar.style.width = '100%'; progressBar.textContent = '';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
progressLabel.textContent = 'Queuing…';
try {
const response = await fetch(form.getAttribute('action'), {
method: 'POST',
body: formData,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await response.json();
if (data.error) {
alert('Error: ' + data.error);
progressContainer.classList.add('d-none');
return;
}
currentPromptId = data.prompt_id;
progressLabel.textContent = 'Queued...';
progressBar.style.width = '100%';
progressBar.textContent = 'Queued';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
await waitForCompletion(currentPromptId);
await finalizeGeneration(currentPromptId);
currentPromptId = null;
} catch (err) {
console.error(err);
alert('Request failed');
progressContainer.classList.add('d-none');
}
});
async function finalizeGeneration(promptId) {
progressLabel.textContent = 'Saving image...';
const url = `/checkpoint/{{ ckpt.slug }}/finalize_generation/${promptId}`;
const formData = new FormData();
formData.append('action', 'preview');
try {
const response = await fetch(url, { method: 'POST', body: formData });
const data = await response.json();
if (data.success) {
previewImg.src = data.image_url;
if (data.error) { alert('Error: ' + data.error); progressContainer.classList.add('d-none'); return; }
currentJobId = data.job_id;
const jobResult = await waitForJob(currentJobId);
currentJobId = null;
if (jobResult.result && jobResult.result.image_url) {
previewImg.src = jobResult.result.image_url;
if (previewCard) previewCard.classList.remove('d-none');
const replaceBtn = document.getElementById('replace-cover-btn');
if (replaceBtn) replaceBtn.disabled = false;
} else {
alert('Save failed: ' + data.error);
}
} catch (err) {
console.error(err);
alert('Finalize request failed');
} finally {
progressContainer.classList.add('d-none');
}
}
} catch (err) { console.error(err); alert('Generation failed: ' + err.message); }
finally { progressContainer.classList.add('d-none'); progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated'); }
});
// Batch: Generate All Characters
const allCharacters = [
{% for char in characters %}{ slug: "{{ char.slug }}", name: {{ char.name | tojson }} },
{% endfor %}
];
const finalizeBaseUrl = '/checkpoint/{{ ckpt.slug }}/finalize_generation';
let stopBatch = false;
const generateAllBtn = document.getElementById('generate-all-btn');
@@ -419,53 +347,36 @@
stopAllBtn.classList.remove('d-none');
batchProgress.classList.remove('d-none');
bootstrap.Tab.getOrCreateInstance(document.getElementById('previews-tab')).show();
for (let i = 0; i < allCharacters.length; i++) {
if (stopBatch) break;
const char = allCharacters[i];
batchBar.style.width = `${Math.round((i / allCharacters.length) * 100)}%`;
batchLabel.textContent = `Generating ${char.name} (${i + 1} / ${allCharacters.length})`;
const genForm = document.getElementById('generate-form');
const fd = new FormData();
fd.append('character_slug', char.slug);
fd.append('action', 'preview');
fd.append('client_id', clientId);
try {
progressContainer.classList.remove('d-none');
progressBar.style.width = '0%';
progressBar.textContent = '0%';
progressLabel.textContent = `${char.name}: Starting...`;
progressBar.style.width = '100%'; progressBar.textContent = '';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
progressLabel.textContent = `${char.name}: Queuing…`;
const resp = await fetch(genForm.getAttribute('action'), {
method: 'POST', body: fd, headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await resp.json();
if (data.error) { console.error(`Error for ${char.name}:`, data.error); progressContainer.classList.add('d-none'); continue; }
currentPromptId = data.prompt_id;
await waitForCompletion(currentPromptId);
progressLabel.textContent = 'Saving image...';
const finalFD = new FormData();
finalFD.append('action', 'preview');
const finalResp = await fetch(`${finalizeBaseUrl}/${currentPromptId}`, { method: 'POST', body: finalFD });
const finalData = await finalResp.json();
if (finalData.success) {
addToPreviewGallery(finalData.image_url, char.name);
previewImg.src = finalData.image_url;
currentJobId = data.job_id;
const jobResult = await waitForJob(currentJobId);
currentJobId = null;
if (jobResult.result && jobResult.result.image_url) {
addToPreviewGallery(jobResult.result.image_url, char.name);
previewImg.src = jobResult.result.image_url;
if (previewCard) previewCard.classList.remove('d-none');
}
currentPromptId = null;
} catch (err) {
console.error(`Failed for ${char.name}:`, err);
currentPromptId = null;
} finally {
progressContainer.classList.add('d-none');
}
} catch (err) { console.error(`Failed for ${char.name}:`, err); currentJobId = null; }
finally { progressContainer.classList.add('d-none'); progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated'); }
}
batchBar.style.width = '100%';
batchLabel.textContent = stopBatch ? 'Stopped.' : 'Complete!';
generateAllBtn.disabled = false;

View File

@@ -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;

View File

@@ -227,83 +227,29 @@
const progressLabel = document.getElementById('progress-label');
const previewCard = document.getElementById('preview-card');
const previewImg = document.getElementById('preview-img');
// Generate a unique client ID
const clientId = 'detail_view_' + Math.random().toString(36).substring(2, 15);
// ComfyUI WebSocket
const socket = new WebSocket('{{ COMFYUI_WS_URL }}?clientId=' + clientId);
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 Image",
"9": "Saving Image"
};
let currentPromptId = null;
let currentJobId = null;
let currentAction = null;
socket.addEventListener('message', (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'status') {
if (!currentPromptId) {
const queueRemaining = msg.data.status.exec_info.queue_remaining;
if (queueRemaining > 0) {
progressLabel.textContent = `Queue position: ${queueRemaining}`;
}
}
}
else if (msg.type === 'progress') {
if (msg.data.prompt_id !== currentPromptId) return;
const value = msg.data.value;
const max = msg.data.max;
const percent = Math.round((value / max) * 100);
progressBar.style.width = `${percent}%`;
progressBar.textContent = `${percent}%`;
}
else if (msg.type === 'executing') {
if (msg.data.prompt_id !== currentPromptId) return;
const nodeId = msg.data.node;
if (nodeId === null) {
// Execution finished via WebSocket
console.log('Finished via WebSocket');
if (resolveCompletion) resolveCompletion();
} else {
const nodeName = nodeNames[nodeId] || `Processing...`;
progressLabel.textContent = nodeName;
}
}
});
let resolveCompletion = null;
async function waitForCompletion(promptId) {
return new Promise((resolve) => {
const checkResolve = () => {
clearInterval(pollInterval);
resolve();
};
resolveCompletion = checkResolve;
// Fallback polling in case WebSocket is blocked (403)
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') {
console.log('Finished via Polling');
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') {
progressLabel.textContent = 'Generating…';
} else {
progressLabel.textContent = 'Queued…';
}
} catch (err) { console.error('Polling error:', err); }
}, 2000);
} catch (err) { console.error('Poll error:', err); }
}, 1500);
});
}
@@ -311,7 +257,7 @@
// Only intercept generate actions
const submitter = e.submitter;
if (!submitter || submitter.value !== 'preview') {
return;
return;
}
e.preventDefault();
@@ -319,21 +265,19 @@
currentAction = submitter.value;
const formData = new FormData(form);
formData.append('action', currentAction);
formData.append('client_id', clientId);
// UI Reset
progressContainer.classList.remove('d-none');
progressBar.style.width = '0%';
progressBar.textContent = '0%';
progressLabel.textContent = 'Starting...';
progressBar.style.width = '100%';
progressBar.textContent = '';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
progressLabel.textContent = 'Queuing…';
try {
const response = await fetch(form.getAttribute('action'), {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await response.json();
@@ -344,64 +288,29 @@
return;
}
currentPromptId = data.prompt_id;
progressLabel.textContent = 'Queued...';
progressBar.style.width = '100%';
progressBar.textContent = 'Queued';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
// Wait for completion (WebSocket or Polling)
await waitForCompletion(currentPromptId);
// Finalize
finalizeGeneration(currentPromptId, currentAction);
currentPromptId = null;
} catch (err) {
console.error(err);
alert('Request failed');
progressContainer.classList.add('d-none');
}
});
async function finalizeGeneration(promptId, action) {
progressLabel.textContent = 'Saving image...';
const url = `/character/{{ character.slug }}/finalize_generation/${promptId}`;
const formData = new FormData();
formData.append('action', 'preview'); // Always save as preview
try {
const response = await fetch(url, {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
// Update preview image
previewImg.src = data.image_url;
currentJobId = data.job_id;
progressLabel.textContent = 'Queued';
// Wait for the background worker to finish
const jobResult = await waitForJob(currentJobId);
currentJobId = null;
// Image is already saved — just display it
if (jobResult.result && jobResult.result.image_url) {
previewImg.src = jobResult.result.image_url;
if (previewCard) previewCard.classList.remove('d-none');
// Enable the replace cover button if it exists
const replaceBtn = document.getElementById('replace-cover-btn');
if (replaceBtn) {
replaceBtn.disabled = false;
// Check if there's a form to update
const form = replaceBtn.closest('form');
if (form) {
form.action = `/character/{{ character.slug }}/replace_cover_from_preview`;
}
}
} else {
alert('Save failed: ' + data.error);
if (replaceBtn) replaceBtn.disabled = false;
}
} catch (err) {
console.error(err);
alert('Finalize request failed');
alert('Generation failed: ' + err.message);
} finally {
progressContainer.classList.add('d-none');
progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated');
}
}
});
});
// Image modal function

View File

@@ -296,171 +296,59 @@
}
});
// Generate a unique client ID
const clientId = 'detailer_detail_' + Math.random().toString(36).substring(2, 15);
// ComfyUI WebSocket
const socket = new WebSocket('{{ COMFYUI_WS_URL }}?clientId=' + clientId);
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 Image",
"9": "Saving Image"
};
let currentPromptId = null;
let currentJobId = null;
let currentAction = null;
socket.addEventListener('message', (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'status') {
if (!currentPromptId) {
const queueRemaining = msg.data.status.exec_info.queue_remaining;
if (queueRemaining > 0) {
progressLabel.textContent = `Queue position: ${queueRemaining}`;
}
}
}
else if (msg.type === 'progress') {
if (msg.data.prompt_id !== currentPromptId) return;
const value = msg.data.value;
const max = msg.data.max;
const percent = Math.round((value / max) * 100);
progressBar.style.width = `${percent}%`;
progressBar.textContent = `${percent}%`;
}
else if (msg.type === 'executing') {
if (msg.data.prompt_id !== currentPromptId) return;
const nodeId = msg.data.node;
if (nodeId === null) {
// Execution finished via WebSocket
console.log('Finished via WebSocket');
if (resolveCompletion) resolveCompletion();
} else {
const nodeName = nodeNames[nodeId] || `Processing...`;
progressLabel.textContent = nodeName;
}
}
});
let resolveCompletion = null;
async function waitForCompletion(promptId) {
return new Promise((resolve) => {
const checkResolve = () => {
clearInterval(pollInterval);
resolve();
};
resolveCompletion = checkResolve;
// Fallback polling in case WebSocket is blocked (403)
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') {
console.log('Finished via Polling');
checkResolve();
}
} catch (err) { console.error('Polling error:', err); }
}, 2000);
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') progressLabel.textContent = 'Generating…';
else progressLabel.textContent = 'Queued…';
} catch (err) { console.error('Poll error:', err); }
}, 1500);
});
}
form.addEventListener('submit', async (e) => {
const submitter = e.submitter;
if (!submitter || submitter.value !== 'preview') return;
e.preventDefault();
currentAction = submitter.value;
const formData = new FormData(form);
formData.append('action', currentAction);
formData.append('client_id', clientId);
progressContainer.classList.remove('d-none');
progressBar.style.width = '0%';
progressBar.textContent = '0%';
progressLabel.textContent = 'Starting...';
progressBar.style.width = '100%'; progressBar.textContent = '';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
progressLabel.textContent = 'Queuing…';
try {
const response = await fetch(form.getAttribute('action'), {
method: 'POST',
body: formData,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await response.json();
if (data.error) {
alert('Error: ' + data.error);
progressContainer.classList.add('d-none');
return;
}
currentPromptId = data.prompt_id;
progressLabel.textContent = 'Queued...';
progressBar.style.width = '100%';
progressBar.textContent = 'Queued';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
await waitForCompletion(currentPromptId);
finalizeGeneration(currentPromptId, currentAction);
currentPromptId = null;
} catch (err) {
console.error(err);
alert('Request failed');
progressContainer.classList.add('d-none');
}
});
async function finalizeGeneration(promptId, action) {
progressLabel.textContent = 'Saving image...';
const url = `/detailer/{{ detailer.slug }}/finalize_generation/${promptId}`;
const formData = new FormData();
formData.append('action', 'preview'); // Always save as preview
try {
const response = await fetch(url, { method: 'POST', body: formData });
const data = await response.json();
if (data.success) {
previewImg.src = data.image_url;
if (data.error) { alert('Error: ' + data.error); progressContainer.classList.add('d-none'); return; }
currentJobId = data.job_id;
const jobResult = await waitForJob(currentJobId);
currentJobId = null;
if (jobResult.result && jobResult.result.image_url) {
previewImg.src = jobResult.result.image_url;
if (previewCard) previewCard.classList.remove('d-none');
const replaceBtn = document.getElementById('replace-cover-btn');
if (replaceBtn) {
replaceBtn.disabled = false;
const form = replaceBtn.closest('form');
if (form) {
form.action = `/detailer/{{ detailer.slug }}/replace_cover_from_preview`;
}
}
} else {
alert('Save failed: ' + data.error);
if (replaceBtn) replaceBtn.disabled = false;
}
} catch (err) {
console.error(err);
alert('Finalize request failed');
} finally {
progressContainer.classList.add('d-none');
}
}
} catch (err) { console.error(err); alert('Generation failed: ' + err.message); }
finally { progressContainer.classList.add('d-none'); progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated'); }
});
// Batch: Generate All Characters
const allCharacters = [
{% for char in characters %}{ slug: "{{ char.slug }}", name: {{ char.name | tojson }} },
{% endfor %}
];
const finalizeBaseUrl = '/detailer/{{ detailer.slug }}/finalize_generation';
let stopBatch = false;
const generateAllBtn = document.getElementById('generate-all-btn');
@@ -496,13 +384,11 @@
stopAllBtn.classList.remove('d-none');
batchProgress.classList.remove('d-none');
bootstrap.Tab.getOrCreateInstance(document.getElementById('previews-tab')).show();
for (let i = 0; i < allCharacters.length; i++) {
if (stopBatch) break;
const char = allCharacters[i];
batchBar.style.width = `${Math.round((i / allCharacters.length) * 100)}%`;
batchLabel.textContent = `Generating ${char.name} (${i + 1} / ${allCharacters.length})`;
const genForm = document.getElementById('generate-form');
const fd = new FormData();
genForm.querySelectorAll('input[name="include_field"]:checked').forEach(cb => fd.append('include_field', cb.value));
@@ -511,42 +397,27 @@
fd.append('extra_positive', document.getElementById('extra_positive').value);
fd.append('extra_negative', document.getElementById('extra_negative').value);
fd.append('action', 'preview');
fd.append('client_id', clientId);
try {
progressContainer.classList.remove('d-none');
progressBar.style.width = '0%';
progressBar.textContent = '0%';
progressLabel.textContent = `${char.name}: Starting...`;
progressBar.style.width = '100%'; progressBar.textContent = '';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
progressLabel.textContent = `${char.name}: Queuing…`;
const resp = await fetch(genForm.getAttribute('action'), {
method: 'POST', body: fd, headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await resp.json();
if (data.error) { console.error(`Error for ${char.name}:`, data.error); progressContainer.classList.add('d-none'); continue; }
currentPromptId = data.prompt_id;
await waitForCompletion(currentPromptId);
progressLabel.textContent = 'Saving image...';
const finalFD = new FormData();
finalFD.append('action', 'preview');
const finalResp = await fetch(`${finalizeBaseUrl}/${currentPromptId}`, { method: 'POST', body: finalFD });
const finalData = await finalResp.json();
if (finalData.success) {
addToPreviewGallery(finalData.image_url, char.name);
previewImg.src = finalData.image_url;
currentJobId = data.job_id;
const jobResult = await waitForJob(currentJobId);
currentJobId = null;
if (jobResult.result && jobResult.result.image_url) {
addToPreviewGallery(jobResult.result.image_url, char.name);
previewImg.src = jobResult.result.image_url;
if (previewCard) previewCard.classList.remove('d-none');
}
currentPromptId = null;
} catch (err) {
console.error(`Failed for ${char.name}:`, err);
currentPromptId = null;
} finally {
progressContainer.classList.add('d-none');
}
} catch (err) { console.error(`Failed for ${char.name}:`, err); currentJobId = null; }
finally { progressContainer.classList.add('d-none'); progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated'); }
}
batchBar.style.width = '100%';
batchLabel.textContent = stopBatch ? 'Stopped.' : 'Complete!';
generateAllBtn.disabled = false;

View File

@@ -104,72 +104,18 @@
const detailerNameText = document.getElementById('current-detailer-name');
const stepProgressText = document.getElementById('current-step-progress');
const clientId = 'detailers_batch_' + Math.random().toString(36).substring(2, 15);
const socket = new WebSocket('{{ COMFYUI_WS_URL }}?clientId=' + clientId);
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 value = msg.data.value;
const max = msg.data.max;
const percent = Math.round((value / 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);
});
}
@@ -186,67 +132,66 @@
batchBtn.disabled = true;
regenAllBtn.disabled = true;
container.classList.remove('d-none');
let completed = 0;
for (const item of missing) {
const percent = Math.round((completed / missing.length) * 100);
progressBar.style.width = `${percent}%`;
progressBar.textContent = `${percent}%`;
statusText.textContent = `Batch Generating Detailers: ${completed + 1} / ${missing.length}`;
detailerNameText.textContent = `Current: ${item.name}`;
nodeStatus.textContent = "Queuing...";
taskProgressBar.style.width = '100%';
taskProgressBar.textContent = 'Queued';
taskProgressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
// Phase 1: Queue all jobs upfront
progressBar.style.width = '100%';
progressBar.textContent = '';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
nodeStatus.textContent = 'Queuing…';
const jobs = [];
for (const item of missing) {
statusText.textContent = `Queuing ${jobs.length + 1} / ${missing.length}`;
try {
const genResp = await fetch(`/detailer/${item.slug}/generate`, {
method: 'POST',
body: new URLSearchParams({
'action': 'replace',
'client_id': clientId,
'character_slug': '__random__'
}),
body: new URLSearchParams({ action: 'replace', character_slug: '__random__' }),
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const genData = await genResp.json();
currentPromptId = genData.prompt_id;
if (genData.job_id) jobs.push({ item, jobId: genData.job_id });
} catch (err) {
console.error(`Failed to queue ${item.name}:`, err);
}
}
await waitForCompletion(currentPromptId);
// Phase 2: Poll all concurrently
progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated');
progressBar.style.width = '0%';
progressBar.textContent = '0%';
statusText.textContent = `0 / ${jobs.length} done`;
const finResp = await fetch(`/detailer/${item.slug}/finalize_generation/${currentPromptId}`, {
method: 'POST',
body: new URLSearchParams({ 'action': 'replace' })
});
const finData = await finResp.json();
if (finData.success) {
let completed = 0;
await Promise.all(jobs.map(async ({ item, jobId }) => {
try {
const jobResult = await waitForJob(jobId);
if (jobResult.result && jobResult.result.image_url) {
const img = document.getElementById(`img-${item.slug}`);
const noImgSpan = document.getElementById(`no-img-${item.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 ${item.name}:`, err);
}
completed++;
}
const pct = Math.round((completed / jobs.length) * 100);
progressBar.style.width = `${pct}%`;
progressBar.textContent = `${pct}%`;
statusText.textContent = `${completed} / ${jobs.length} done`;
}));
progressBar.style.width = '100%';
progressBar.textContent = '100%';
statusText.textContent = "Batch Detailer Generation Complete!";
detailerNameText.textContent = "";
nodeStatus.textContent = "Done";
stepProgressText.textContent = "";
statusText.textContent = 'Batch Detailer Generation Complete!';
detailerNameText.textContent = '';
nodeStatus.textContent = 'Done';
stepProgressText.textContent = '';
taskProgressBar.style.width = '0%';
taskProgressBar.textContent = '';
batchBtn.disabled = false;
regenAllBtn.disabled = false;
setTimeout(() => { container.classList.add('d-none'); }, 5000);
setTimeout(() => container.classList.add('d-none'), 5000);
}
batchBtn.addEventListener('click', async () => {

View File

@@ -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) {

View File

@@ -103,72 +103,18 @@
const charNameText = document.getElementById('current-char-name');
const stepProgressText = document.getElementById('current-step-progress');
const clientId = 'gallery_batch_' + Math.random().toString(36).substring(2, 15);
const socket = new WebSocket('{{ COMFYUI_WS_URL }}?clientId=' + clientId);
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 value = msg.data.value;
const max = msg.data.max;
const percent = Math.round((value / 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 = "";
// Reset task bar for new node if it's not sampling
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);
});
}
@@ -185,63 +131,66 @@
batchBtn.disabled = true;
regenAllBtn.disabled = true;
container.classList.remove('d-none');
let completed = 0;
for (const char of missing) {
const percent = Math.round((completed / missing.length) * 100);
progressBar.style.width = `${percent}%`;
progressBar.textContent = `${percent}%`;
statusText.textContent = `Batch Generating: ${completed + 1} / ${missing.length}`;
charNameText.textContent = `Current: ${char.name}`;
nodeStatus.textContent = "Queuing...";
taskProgressBar.style.width = '100%';
taskProgressBar.textContent = 'Queued';
taskProgressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
// Phase 1: Queue all jobs upfront so the page can be navigated away from
progressBar.style.width = '100%';
progressBar.textContent = '';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
nodeStatus.textContent = 'Queuing…';
const jobs = [];
for (const char of missing) {
statusText.textContent = `Queuing ${jobs.length + 1} / ${missing.length}`;
try {
const genResp = await fetch(`/character/${char.slug}/generate`, {
method: 'POST',
body: new URLSearchParams({ 'action': 'replace', 'client_id': clientId }),
body: new URLSearchParams({ action: 'replace' }),
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const genData = await genResp.json();
currentPromptId = genData.prompt_id;
if (genData.job_id) jobs.push({ item: char, jobId: genData.job_id });
} catch (err) {
console.error(`Failed to queue ${char.name}:`, err);
}
}
await waitForCompletion(currentPromptId);
// Phase 2: Poll all jobs concurrently; update UI as each finishes
progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated');
progressBar.style.width = '0%';
progressBar.textContent = '0%';
statusText.textContent = `0 / ${jobs.length} done`;
const finResp = await fetch(`/character/${char.slug}/finalize_generation/${currentPromptId}`, {
method: 'POST',
body: new URLSearchParams({ 'action': 'replace' })
});
const finData = await finResp.json();
if (finData.success) {
const img = document.getElementById(`img-${char.slug}`);
const noImgSpan = document.getElementById(`no-img-${char.slug}`);
if (img) {
img.src = finData.image_url;
img.classList.remove('d-none');
}
let completed = 0;
await Promise.all(jobs.map(async ({ item, jobId }) => {
try {
const jobResult = await waitForJob(jobId);
if (jobResult.result && jobResult.result.image_url) {
const img = document.getElementById(`img-${item.slug}`);
const noImgSpan = document.getElementById(`no-img-${item.slug}`);
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 ${char.name}:`, err);
console.error(`Failed for ${item.name}:`, err);
}
completed++;
}
const pct = Math.round((completed / jobs.length) * 100);
progressBar.style.width = `${pct}%`;
progressBar.textContent = `${pct}%`;
statusText.textContent = `${completed} / ${jobs.length} done`;
}));
progressBar.style.width = '100%';
progressBar.textContent = '100%';
statusText.textContent = "Batch Complete!";
charNameText.textContent = "";
nodeStatus.textContent = "Done";
stepProgressText.textContent = "";
statusText.textContent = 'Batch Complete!';
charNameText.textContent = '';
nodeStatus.textContent = 'Done';
stepProgressText.textContent = '';
taskProgressBar.style.width = '0%';
taskProgressBar.textContent = '';
batchBtn.disabled = false;
regenAllBtn.disabled = false;
setTimeout(() => { container.classList.add('d-none'); }, 5000);
setTimeout(() => container.classList.add('d-none'), 5000);
}
batchBtn.addEventListener('click', async () => {

View File

@@ -29,6 +29,12 @@
<a href="/gallery" class="btn btn-sm btn-outline-light">Gallery</a>
<a href="/settings" class="btn btn-sm btn-outline-light">Settings</a>
<div class="vr mx-1 d-none d-lg-block"></div>
<!-- Queue indicator -->
<button id="queue-btn" class="btn btn-sm queue-btn" data-bs-toggle="modal" data-bs-target="#queueModal" title="Generation Queue">
<span class="queue-icon"></span>
<span id="queue-count-badge" class="queue-badge d-none">0</span>
</button>
<div class="vr mx-1 d-none d-lg-block"></div>
<!-- Service status indicators -->
<span id="status-comfyui" class="service-status" title="ComfyUI" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="ComfyUI: checking…">
<span class="status-dot status-checking"></span>
@@ -94,6 +100,31 @@
</div>
</div>
<!-- Generation Queue Modal -->
<div class="modal fade" id="queueModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
Generation Queue
<span id="queue-modal-count" class="badge bg-secondary ms-2">0</span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0">
<div id="queue-empty-msg" class="text-center text-muted py-4">
<p class="mb-0">No jobs in queue.</p>
</div>
<ul id="queue-job-list" class="list-group list-group-flush d-none"></ul>
</div>
<div class="modal-footer">
<small class="text-muted me-auto">Jobs are processed sequentially. Close this window to continue browsing.</small>
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
@@ -339,6 +370,148 @@
});
})();
</script>
<script>
// ---- Generation Queue UI ----
(function() {
const badge = document.getElementById('queue-count-badge');
const modalCount = document.getElementById('queue-modal-count');
const jobList = document.getElementById('queue-job-list');
const emptyMsg = document.getElementById('queue-empty-msg');
const STATUS_LABELS = {
pending: { text: 'Pending', cls: 'text-muted' },
processing: { text: 'Generating…', cls: 'text-warning' },
paused: { text: 'Paused', cls: 'text-secondary' },
done: { text: 'Done', cls: 'text-success' },
failed: { text: 'Failed', cls: 'text-danger' },
removed: { text: 'Removed', cls: 'text-muted' },
};
function renderQueue(jobs) {
const activeJobs = jobs.filter(j => !['done', 'failed', 'removed'].includes(j.status));
const count = activeJobs.length;
// Update badge
if (count > 0) {
badge.textContent = count;
badge.classList.remove('d-none');
document.getElementById('queue-btn').classList.add('queue-btn-active');
} else {
badge.classList.add('d-none');
document.getElementById('queue-btn').classList.remove('queue-btn-active');
}
// Update modal count
if (modalCount) modalCount.textContent = jobs.length;
// Render job list
if (!jobList) return;
if (jobs.length === 0) {
jobList.classList.add('d-none');
if (emptyMsg) emptyMsg.classList.remove('d-none');
return;
}
jobList.classList.remove('d-none');
if (emptyMsg) emptyMsg.classList.add('d-none');
jobList.innerHTML = '';
jobs.forEach(job => {
const li = document.createElement('li');
li.className = 'list-group-item d-flex align-items-center gap-2 py-2';
li.id = `queue-job-${job.id}`;
const statusInfo = STATUS_LABELS[job.status] || { text: job.status, cls: 'text-muted' };
// Status indicator
const statusDot = document.createElement('span');
statusDot.className = `queue-status-dot queue-status-${job.status}`;
li.appendChild(statusDot);
// Label
const label = document.createElement('span');
label.className = 'flex-grow-1 small';
label.textContent = job.label;
li.appendChild(label);
// Status text
const statusText = document.createElement('span');
statusText.className = `small ${statusInfo.cls}`;
statusText.textContent = statusInfo.text;
if (job.status === 'failed' && job.error) {
statusText.title = job.error;
statusText.style.cursor = 'help';
}
li.appendChild(statusText);
// Action buttons
const btnGroup = document.createElement('div');
btnGroup.className = 'd-flex gap-1';
if (job.status === 'pending') {
const pauseBtn = document.createElement('button');
pauseBtn.className = 'btn btn-xs btn-outline-secondary';
pauseBtn.textContent = '⏸';
pauseBtn.title = 'Pause';
pauseBtn.onclick = () => queuePause(job.id);
btnGroup.appendChild(pauseBtn);
}
if (job.status === 'paused') {
const resumeBtn = document.createElement('button');
resumeBtn.className = 'btn btn-xs btn-outline-success';
resumeBtn.textContent = '▶';
resumeBtn.title = 'Resume';
resumeBtn.onclick = () => queuePause(job.id);
btnGroup.appendChild(resumeBtn);
}
if (['pending', 'paused', 'failed'].includes(job.status)) {
const removeBtn = document.createElement('button');
removeBtn.className = 'btn btn-xs btn-outline-danger';
removeBtn.textContent = '✕';
removeBtn.title = 'Remove';
removeBtn.onclick = () => queueRemove(job.id);
btnGroup.appendChild(removeBtn);
}
li.appendChild(btnGroup);
jobList.appendChild(li);
});
}
async function fetchQueue() {
try {
const resp = await fetch('/api/queue');
const data = await resp.json();
renderQueue(data.jobs || []);
} catch (e) {}
}
async function queueRemove(jobId) {
try {
await fetch(`/api/queue/${jobId}/remove`, { method: 'POST' });
fetchQueue();
} catch (e) {}
}
async function queuePause(jobId) {
try {
await fetch(`/api/queue/${jobId}/pause`, { method: 'POST' });
fetchQueue();
} catch (e) {}
}
// Poll queue every 2 seconds
document.addEventListener('DOMContentLoaded', () => {
fetchQueue();
setInterval(fetchQueue, 2000);
// Refresh when modal opens
const queueModal = document.getElementById('queueModal');
if (queueModal) {
queueModal.addEventListener('show.bs.modal', fetchQueue);
}
});
})();
</script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -239,45 +239,20 @@
const previewCard = document.getElementById('preview-card');
const previewImg = document.getElementById('preview-img');
const clientId = 'look_detail_' + 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": "Look LoRA", "17": "Outfit LoRA",
"18": "Action LoRA", "19": "Style/Detailer LoRA",
"8": "Decoding Image", "9": "Saving Image"
};
let currentPromptId = null;
let resolveCompletion = null;
socket.addEventListener('message', (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'progress' && msg.data.prompt_id === currentPromptId) {
const percent = Math.round((msg.data.value / msg.data.max) * 100);
progressBar.style.width = `${percent}%`;
progressBar.textContent = `${percent}%`;
} else if (msg.type === 'executing' && msg.data.prompt_id === currentPromptId) {
if (msg.data.node === null) {
if (resolveCompletion) resolveCompletion();
} else {
progressLabel.textContent = nodeNames[msg.data.node] || 'Processing...';
}
}
});
async function waitForCompletion(promptId) {
return new Promise((resolve) => {
const checkResolve = () => { clearInterval(pollInterval); resolve(); };
resolveCompletion = 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();
} catch (err) { console.error('Polling error:', err); }
}, 2000);
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') progressLabel.textContent = 'Generating…';
else progressLabel.textContent = 'Queued…';
} catch (err) { console.error('Poll error:', err); }
}, 1500);
});
}
@@ -285,63 +260,30 @@
const submitter = e.submitter;
if (!submitter || submitter.value !== 'preview') return;
e.preventDefault();
const formData = new FormData(form);
formData.append('action', 'preview');
formData.append('client_id', clientId);
progressContainer.classList.remove('d-none');
progressBar.style.width = '0%';
progressBar.textContent = '0%';
progressLabel.textContent = 'Starting...';
progressBar.style.width = '100%'; progressBar.textContent = '';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
progressLabel.textContent = 'Queuing…';
try {
const response = await fetch(form.getAttribute('action'), {
method: 'POST', body: formData,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await response.json();
if (data.error) { alert('Error: ' + data.error); progressContainer.classList.add('d-none'); return; }
currentPromptId = data.prompt_id;
progressLabel.textContent = 'Queued...';
progressBar.style.width = '100%';
progressBar.textContent = 'Queued';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
await waitForCompletion(currentPromptId);
finalizeGeneration(currentPromptId);
currentPromptId = null;
} catch (err) {
console.error(err);
alert('Request failed');
progressContainer.classList.add('d-none');
}
});
async function finalizeGeneration(promptId) {
progressLabel.textContent = 'Saving image...';
const url = `/look/{{ look.slug }}/finalize_generation/${promptId}`;
try {
const response = await fetch(url, {
method: 'POST', body: new FormData()
});
const data = await response.json();
if (data.success) {
previewImg.src = data.image_url;
currentJobId = data.job_id;
const jobResult = await waitForJob(currentJobId);
currentJobId = null;
if (jobResult.result && jobResult.result.image_url) {
previewImg.src = jobResult.result.image_url;
if (previewCard) previewCard.classList.remove('d-none');
const replaceBtn = document.getElementById('replace-cover-btn');
if (replaceBtn) replaceBtn.disabled = false;
} else {
alert('Save failed: ' + data.error);
}
} catch (err) {
console.error(err);
alert('Finalize request failed');
} finally {
progressContainer.classList.add('d-none');
}
}
} catch (err) { console.error(err); alert('Generation failed: ' + err.message); }
finally { progressContainer.classList.add('d-none'); progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated'); }
});
});
function showImage(src) {

View File

@@ -105,56 +105,20 @@
const itemNameText = document.getElementById('current-item-name');
const stepProgressText = document.getElementById('current-step-progress');
const clientId = 'looks_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": "Look 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);
});
}
@@ -179,40 +143,32 @@
progressBar.textContent = `${percent}%`;
statusText.textContent = `Batch Generating Looks: ${completed + 1} / ${missing.length}`;
itemNameText.textContent = `Current: ${item.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 {
// Looks are self-contained — no character_slug passed
const genResp = await fetch(`/look/${item.slug}/generate`, {
method: 'POST',
body: new URLSearchParams({
'action': 'replace',
'client_id': clientId
}),
body: new URLSearchParams({ 'action': 'replace' }),
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(`/look/${item.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-${item.slug}`);
const noImgSpan = document.getElementById(`no-img-${item.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 ${item.name}:`, err);
currentJobId = null;
}
completed++;
}

View File

@@ -272,82 +272,28 @@
const previewCard = document.getElementById('preview-card');
const previewImg = document.getElementById('preview-img');
// Generate a unique client ID
const clientId = 'outfit_detail_' + Math.random().toString(36).substring(2, 15);
// ComfyUI WebSocket
const socket = new WebSocket('{{ COMFYUI_WS_URL }}?clientId=' + clientId);
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 Image",
"9": "Saving Image"
};
let currentPromptId = null;
let currentJobId = null;
let currentAction = null;
socket.addEventListener('message', (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'status') {
if (!currentPromptId) {
const queueRemaining = msg.data.status.exec_info.queue_remaining;
if (queueRemaining > 0) {
progressLabel.textContent = `Queue position: ${queueRemaining}`;
}
}
}
else if (msg.type === 'progress') {
if (msg.data.prompt_id !== currentPromptId) return;
const value = msg.data.value;
const max = msg.data.max;
const percent = Math.round((value / max) * 100);
progressBar.style.width = `${percent}%`;
progressBar.textContent = `${percent}%`;
}
else if (msg.type === 'executing') {
if (msg.data.prompt_id !== currentPromptId) return;
const nodeId = msg.data.node;
if (nodeId === null) {
// Execution finished via WebSocket
console.log('Finished via WebSocket');
if (resolveCompletion) resolveCompletion();
} else {
const nodeName = nodeNames[nodeId] || `Processing...`;
progressLabel.textContent = nodeName;
}
}
});
let resolveCompletion = null;
async function waitForCompletion(promptId) {
return new Promise((resolve) => {
const checkResolve = () => {
clearInterval(pollInterval);
resolve();
};
resolveCompletion = checkResolve;
// Fallback polling in case WebSocket is blocked (403)
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') {
console.log('Finished via Polling');
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') {
progressLabel.textContent = 'Generating…';
} else {
progressLabel.textContent = 'Queued…';
}
} catch (err) { console.error('Polling error:', err); }
}, 2000);
} catch (err) { console.error('Poll error:', err); }
}, 1500);
});
}
@@ -355,7 +301,7 @@
// Only intercept generate actions
const submitter = e.submitter;
if (!submitter || submitter.value !== 'preview') {
return;
return;
}
e.preventDefault();
@@ -363,21 +309,19 @@
currentAction = submitter.value;
const formData = new FormData(form);
formData.append('action', currentAction);
formData.append('client_id', clientId);
// UI Reset
progressContainer.classList.remove('d-none');
progressBar.style.width = '0%';
progressBar.textContent = '0%';
progressLabel.textContent = 'Starting...';
progressBar.style.width = '100%';
progressBar.textContent = '';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
progressLabel.textContent = 'Queuing…';
try {
const response = await fetch(form.getAttribute('action'), {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await response.json();
@@ -388,71 +332,33 @@
return;
}
currentPromptId = data.prompt_id;
progressLabel.textContent = 'Queued...';
progressBar.style.width = '100%';
progressBar.textContent = 'Queued';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
// Wait for completion (WebSocket or Polling)
await waitForCompletion(currentPromptId);
// Finalize
finalizeGeneration(currentPromptId, currentAction);
currentPromptId = null;
} catch (err) {
console.error(err);
alert('Request failed');
progressContainer.classList.add('d-none');
}
});
async function finalizeGeneration(promptId, action) {
progressLabel.textContent = 'Saving image...';
const url = `/outfit/{{ outfit.slug }}/finalize_generation/${promptId}`;
const formData = new FormData();
formData.append('action', 'preview'); // Always save as preview
try {
const response = await fetch(url, {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
// Update preview image
previewImg.src = data.image_url;
currentJobId = data.job_id;
progressLabel.textContent = 'Queued';
const jobResult = await waitForJob(currentJobId);
currentJobId = null;
if (jobResult.result && jobResult.result.image_url) {
previewImg.src = jobResult.result.image_url;
if (previewCard) previewCard.classList.remove('d-none');
// Enable the replace cover button if it exists
const replaceBtn = document.getElementById('replace-cover-btn');
if (replaceBtn) {
replaceBtn.disabled = false;
// Check if there's a form to update
const form = replaceBtn.closest('form');
if (form) {
form.action = `/outfit/{{ outfit.slug }}/replace_cover_from_preview`;
}
}
} else {
alert('Save failed: ' + data.error);
if (replaceBtn) replaceBtn.disabled = false;
}
} catch (err) {
console.error(err);
alert('Finalize request failed');
alert('Generation failed: ' + err.message);
} finally {
progressContainer.classList.add('d-none');
progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated');
}
}
});
// Batch: Generate All Characters
const allCharacters = [
{% for char in characters %}{ slug: "{{ char.slug }}", name: {{ char.name | tojson }} },
{% endfor %}
];
const finalizeBaseUrl = '/outfit/{{ outfit.slug }}/finalize_generation';
let stopBatch = false;
const generateAllBtn = document.getElementById('generate-all-btn');
const stopAllBtn = document.getElementById('stop-all-btn');
@@ -497,35 +403,31 @@
genForm.querySelectorAll('input[name="include_field"]:checked').forEach(cb => fd.append('include_field', cb.value));
fd.append('character_slug', char.slug);
fd.append('action', 'preview');
fd.append('client_id', clientId);
try {
progressContainer.classList.remove('d-none');
progressBar.style.width = '0%';
progressBar.textContent = '0%';
progressLabel.textContent = `${char.name}: Starting...`;
progressBar.style.width = '100%';
progressBar.textContent = '';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
progressLabel.textContent = `${char.name}: Queuing…`;
const resp = await fetch(genForm.getAttribute('action'), {
method: 'POST', body: fd, headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await resp.json();
if (data.error) { console.error(`Error for ${char.name}:`, data.error); progressContainer.classList.add('d-none'); continue; }
currentPromptId = data.prompt_id;
await waitForCompletion(currentPromptId);
progressLabel.textContent = 'Saving image...';
const finalFD = new FormData();
finalFD.append('action', 'preview');
const finalResp = await fetch(`${finalizeBaseUrl}/${currentPromptId}`, { method: 'POST', body: finalFD });
const finalData = await finalResp.json();
if (finalData.success) {
addToPreviewGallery(finalData.image_url, char.name);
previewImg.src = finalData.image_url;
currentJobId = data.job_id;
const jobResult = await waitForJob(currentJobId);
currentJobId = null;
if (jobResult.result && jobResult.result.image_url) {
addToPreviewGallery(jobResult.result.image_url, char.name);
previewImg.src = jobResult.result.image_url;
if (previewCard) previewCard.classList.remove('d-none');
}
currentPromptId = null;
} catch (err) {
console.error(`Failed for ${char.name}:`, err);
currentPromptId = null;
currentJobId = null;
} finally {
progressContainer.classList.add('d-none');
progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated');
}
}
batchBar.style.width = '100%';

View File

@@ -102,71 +102,18 @@
const itemNameText = document.getElementById('current-item-name');
const stepProgressText = document.getElementById('current-step-progress');
const clientId = 'outfits_batch_' + Math.random().toString(36).substring(2, 15);
const socket = new WebSocket('{{ COMFYUI_WS_URL }}?clientId=' + clientId);
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 value = msg.data.value;
const max = msg.data.max;
const percent = Math.round((value / 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);
});
}
@@ -183,67 +130,66 @@
batchBtn.disabled = true;
regenAllBtn.disabled = true;
container.classList.remove('d-none');
let completed = 0;
for (const item of missing) {
const percent = Math.round((completed / missing.length) * 100);
progressBar.style.width = `${percent}%`;
progressBar.textContent = `${percent}%`;
statusText.textContent = `Batch Generating Outfits: ${completed + 1} / ${missing.length}`;
itemNameText.textContent = `Current: ${item.name}`;
nodeStatus.textContent = "Queuing...";
taskProgressBar.style.width = '100%';
taskProgressBar.textContent = 'Queued';
taskProgressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
// Phase 1: Queue all jobs upfront
progressBar.style.width = '100%';
progressBar.textContent = '';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
nodeStatus.textContent = 'Queuing…';
const jobs = [];
for (const item of missing) {
statusText.textContent = `Queuing ${jobs.length + 1} / ${missing.length}`;
try {
const genResp = await fetch(`/outfit/${item.slug}/generate`, {
method: 'POST',
body: new URLSearchParams({
'action': 'replace',
'client_id': clientId,
'character_slug': '__random__'
}),
body: new URLSearchParams({ action: 'replace', character_slug: '__random__' }),
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const genData = await genResp.json();
currentPromptId = genData.prompt_id;
if (genData.job_id) jobs.push({ item, jobId: genData.job_id });
} catch (err) {
console.error(`Failed to queue ${item.name}:`, err);
}
}
await waitForCompletion(currentPromptId);
// Phase 2: Poll all concurrently
progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated');
progressBar.style.width = '0%';
progressBar.textContent = '0%';
statusText.textContent = `0 / ${jobs.length} done`;
const finResp = await fetch(`/outfit/${item.slug}/finalize_generation/${currentPromptId}`, {
method: 'POST',
body: new URLSearchParams({ 'action': 'replace' })
});
const finData = await finResp.json();
if (finData.success) {
let completed = 0;
await Promise.all(jobs.map(async ({ item, jobId }) => {
try {
const jobResult = await waitForJob(jobId);
if (jobResult.result && jobResult.result.image_url) {
const img = document.getElementById(`img-${item.slug}`);
const noImgSpan = document.getElementById(`no-img-${item.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 ${item.name}:`, err);
}
completed++;
}
const pct = Math.round((completed / jobs.length) * 100);
progressBar.style.width = `${pct}%`;
progressBar.textContent = `${pct}%`;
statusText.textContent = `${completed} / ${jobs.length} done`;
}));
progressBar.style.width = '100%';
progressBar.textContent = '100%';
statusText.textContent = "Batch Outfit Generation Complete!";
itemNameText.textContent = "";
nodeStatus.textContent = "Done";
stepProgressText.textContent = "";
statusText.textContent = 'Batch Outfit Generation Complete!';
itemNameText.textContent = '';
nodeStatus.textContent = 'Done';
stepProgressText.textContent = '';
taskProgressBar.style.width = '0%';
taskProgressBar.textContent = '';
batchBtn.disabled = false;
regenAllBtn.disabled = false;
setTimeout(() => { container.classList.add('d-none'); }, 5000);
setTimeout(() => container.classList.add('d-none'), 5000);
}
batchBtn.addEventListener('click', async () => {

View File

@@ -101,11 +101,10 @@
const SG_CAT = {{ sg_category | tojson }};
const SG_SLUG = {{ sg_entity.slug | tojson }};
const SG_WS = {{ COMFYUI_WS_URL | tojson }};
const SG_CLIENT_ID = 'sg_' + Math.random().toString(36).slice(2, 10);
let sgRunning = false;
let sgShouldStop = false;
let sgQueuedJobs = []; // track all queued job IDs so stop can cancel them
// ---- helpers ----
@@ -241,52 +240,23 @@
sgHighlightBounds();
}
// ---- WebSocket wait ----
// ---- Job queue wait ----
function sgWaitForCompletion(promptId) {
function sgWaitForJob(jobId) {
return new Promise((resolve, reject) => {
let ws;
try {
ws = new WebSocket(`${SG_WS}?clientId=${SG_CLIENT_ID}`);
} catch (e) {
// Fall back to polling if WS unavailable
sgPollUntilDone(promptId).then(resolve).catch(reject);
return;
}
const timeout = setTimeout(() => {
ws.close();
sgPollUntilDone(promptId).then(resolve).catch(reject);
}, 120000);
ws.onmessage = (event) => {
let msg;
try { msg = JSON.parse(event.data); } catch { return; }
if (msg.type === 'executing' && msg.data && msg.data.prompt_id === promptId) {
if (msg.data.node === null) {
clearTimeout(timeout);
ws.close();
resolve();
const poll = setInterval(async () => {
try {
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'));
}
}
};
ws.onerror = () => {
clearTimeout(timeout);
sgPollUntilDone(promptId).then(resolve).catch(reject);
};
} catch (err) { console.error('[Strengths] poll error:', err); }
}, 1500);
});
}
async function sgPollUntilDone(promptId) {
for (let i = 0; i < 120; i++) {
await new Promise(r => setTimeout(r, 2000));
const r = await fetch(`/check_status/${promptId}`);
const d = await r.json();
if (d.status === 'complete' || d.status === 'finished' || d.done) return;
}
}
// ---- main flow ----
async function sgClearImages() {
@@ -310,64 +280,65 @@
const steps = sgBuildSteps(min, max, sgGetInterval());
if (!steps.length) return;
// Clear any previous set before starting a new one
await sgClearImages();
sgRunning = true;
sgShouldStop = false;
sgQueuedJobs = [];
document.getElementById('sg-btn-run').classList.add('d-none');
document.getElementById('sg-btn-stop').classList.remove('d-none');
document.getElementById('sg-progress').classList.remove('d-none');
const charSelect = document.getElementById('character_select');
const charSlug = charSelect ? charSelect.value : '';
// Phase 1: Queue all steps upfront so generation continues even if the page is navigated away
document.getElementById('sg-progress-bar').style.width = '100%';
document.getElementById('sg-progress-bar').classList.add('progress-bar-striped', 'progress-bar-animated');
for (let i = 0; i < steps.length; i++) {
if (sgShouldStop) break;
const sv = steps[i];
const pct = Math.round(((i) / steps.length) * 100);
document.getElementById('sg-progress-bar').style.width = pct + '%';
document.getElementById('sg-progress-label').textContent =
`${i} / ${steps.length} \u2014 weight: ${sv}`;
`Queuing ${i + 1} / ${steps.length} \u2014 weight: ${sv}`;
try {
// Queue one generation
// Pick up the character currently selected on this detail page (if any)
const charSelect = document.getElementById('character_select');
const charSlug = charSelect ? charSelect.value : '';
const formData = new URLSearchParams({
strength_value: sv,
seed: seed,
client_id: SG_CLIENT_ID,
character_slug: charSlug,
});
const formData = new URLSearchParams({ strength_value: sv, seed, character_slug: charSlug });
const queueResp = await fetch(`/strengths/${SG_CAT}/${SG_SLUG}/generate`, {
method: 'POST',
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded' },
body: formData,
});
const queueData = await queueResp.json();
if (!queueData.prompt_id) throw new Error('No prompt_id returned');
await sgWaitForCompletion(queueData.prompt_id);
// Finalize
const finData = new URLSearchParams({ strength_value: sv, seed: seed });
const finResp = await fetch(`/strengths/${SG_CAT}/${SG_SLUG}/finalize/${queueData.prompt_id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: finData,
});
const finJson = await finResp.json();
if (finJson.success && finJson.image_url) {
sgAddImage(finJson.image_url, sv);
}
if (!queueData.job_id) throw new Error('No job_id returned');
sgQueuedJobs.push({ jobId: queueData.job_id, sv });
} catch (err) {
console.error('[Strengths] step error:', sv, err);
console.error('[Strengths] queue error:', sv, err);
}
}
// Phase 2: Poll all jobs concurrently; show results as each finishes
document.getElementById('sg-progress-bar').classList.remove('progress-bar-striped', 'progress-bar-animated');
document.getElementById('sg-progress-bar').style.width = '0%';
let completed = 0;
await Promise.all(sgQueuedJobs.map(async ({ jobId, sv }) => {
try {
const jobResult = await sgWaitForJob(jobId);
if (jobResult.result && jobResult.result.image_url) {
sgAddImage(jobResult.result.image_url, sv);
}
} catch (err) {
console.error('[Strengths] job error:', sv, err);
}
completed++;
const pct = Math.round((completed / sgQueuedJobs.length) * 100);
document.getElementById('sg-progress-bar').style.width = pct + '%';
document.getElementById('sg-progress-label').textContent =
`${completed} / ${sgQueuedJobs.length} done`;
}));
document.getElementById('sg-progress-bar').style.width = '100%';
document.getElementById('sg-progress-label').textContent =
`Done \u2014 ${steps.length} images generated`;
`Done \u2014 ${sgQueuedJobs.length} images generated`;
setTimeout(() => document.getElementById('sg-progress').classList.add('d-none'), 3000);
document.getElementById('sg-btn-stop').classList.add('d-none');
@@ -377,6 +348,10 @@
function sgStop() {
sgShouldStop = true;
// Cancel any pending (not yet processing) queued jobs
sgQueuedJobs.forEach(({ jobId }) => {
fetch(`/api/queue/${jobId}/remove`, { method: 'POST' }).catch(() => {});
});
document.getElementById('sg-btn-stop').classList.add('d-none');
document.getElementById('sg-btn-run').classList.remove('d-none');
}

View File

@@ -292,173 +292,57 @@
}
});
// Generate a unique client ID
const clientId = 'scene_detail_' + Math.random().toString(36).substring(2, 15);
let currentJobId = null;
// ComfyUI WebSocket
const socket = new WebSocket('{{ COMFYUI_WS_URL }}?clientId=' + clientId);
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 Image",
"9": "Saving Image"
};
let currentPromptId = null;
let resolveCompletion = null;
socket.addEventListener('message', (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'status') {
if (!currentPromptId) {
const queueRemaining = msg.data.status.exec_info.queue_remaining;
if (queueRemaining > 0) {
progressLabel.textContent = `Queue position: ${queueRemaining}`;
}
}
}
else if (msg.type === 'progress') {
if (msg.data.prompt_id !== currentPromptId) return;
const value = msg.data.value;
const max = msg.data.max;
const percent = Math.round((value / max) * 100);
progressBar.style.width = `${percent}%`;
progressBar.textContent = `${percent}%`;
}
else if (msg.type === 'executing') {
if (msg.data.prompt_id !== currentPromptId) return;
const nodeId = msg.data.node;
if (nodeId === null) {
// Execution finished via WebSocket
console.log('Finished via WebSocket');
if (resolveCompletion) resolveCompletion();
} else {
const nodeName = nodeNames[nodeId] || `Processing...`;
progressLabel.textContent = nodeName;
}
}
});
async function waitForCompletion(promptId) {
return new Promise((resolve) => {
const checkResolve = () => {
clearInterval(pollInterval);
resolve();
};
resolveCompletion = checkResolve;
// Fallback polling in case WebSocket is blocked (403)
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') {
console.log('Finished via Polling');
checkResolve();
}
} catch (err) { console.error('Polling error:', err); }
}, 2000);
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') progressLabel.textContent = 'Generating…';
else progressLabel.textContent = 'Queued…';
} catch (err) { console.error('Poll error:', err); }
}, 1500);
});
}
form.addEventListener('submit', async (e) => {
const submitter = e.submitter;
if (!submitter || submitter.value !== 'preview') return;
e.preventDefault();
const formData = new FormData(form);
formData.append('action', 'preview');
formData.append('client_id', clientId);
progressContainer.classList.remove('d-none');
progressBar.style.width = '0%';
progressBar.textContent = '0%';
progressLabel.textContent = 'Starting...';
progressBar.style.width = '100%'; progressBar.textContent = '';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
progressLabel.textContent = 'Queuing…';
try {
const response = await fetch(form.getAttribute('action'), {
method: 'POST',
body: formData,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await response.json();
if (data.error) {
alert('Error: ' + data.error);
progressContainer.classList.add('d-none');
return;
}
currentPromptId = data.prompt_id;
progressLabel.textContent = 'Queued...';
progressBar.style.width = '100%';
progressBar.textContent = 'Queued';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
// Wait for completion (WebSocket or Polling)
await waitForCompletion(currentPromptId);
// Finalize
finalizeGeneration(currentPromptId);
currentPromptId = null;
} catch (err) {
console.error(err);
alert('Request failed');
progressContainer.classList.add('d-none');
}
});
async function finalizeGeneration(promptId) {
progressLabel.textContent = 'Saving image...';
const url = `/scene/{{ scene.slug }}/finalize_generation/${promptId}`;
const formData = new FormData();
formData.append('action', 'preview');
try {
const response = await fetch(url, { method: 'POST', body: formData });
const data = await response.json();
if (data.success) {
// Update preview image
previewImg.src = data.image_url;
if (data.error) { alert('Error: ' + data.error); progressContainer.classList.add('d-none'); return; }
currentJobId = data.job_id;
const jobResult = await waitForJob(currentJobId);
currentJobId = null;
if (jobResult.result && jobResult.result.image_url) {
previewImg.src = jobResult.result.image_url;
if (previewCard) previewCard.classList.remove('d-none');
const replaceBtn = document.getElementById('replace-cover-btn');
if (replaceBtn) {
replaceBtn.disabled = false;
const form = replaceBtn.closest('form');
if (form) {
form.action = `/scene/{{ scene.slug }}/replace_cover_from_preview`;
}
}
} else {
alert('Save failed: ' + data.error);
if (replaceBtn) replaceBtn.disabled = false;
}
} catch (err) {
console.error(err);
alert('Finalize request failed');
} finally {
progressContainer.classList.add('d-none');
}
}
} catch (err) { console.error(err); alert('Generation failed: ' + err.message); }
finally { progressContainer.classList.add('d-none'); progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated'); }
});
// Batch: Generate All Characters
const allCharacters = [
{% for char in characters %}{ slug: "{{ char.slug }}", name: {{ char.name | tojson }} },
{% endfor %}
];
const finalizeBaseUrl = '/scene/{{ scene.slug }}/finalize_generation';
let stopBatch = false;
const generateAllBtn = document.getElementById('generate-all-btn');
@@ -494,54 +378,37 @@
stopAllBtn.classList.remove('d-none');
batchProgress.classList.remove('d-none');
bootstrap.Tab.getOrCreateInstance(document.getElementById('previews-tab')).show();
for (let i = 0; i < allCharacters.length; i++) {
if (stopBatch) break;
const char = allCharacters[i];
batchBar.style.width = `${Math.round((i / allCharacters.length) * 100)}%`;
batchLabel.textContent = `Generating ${char.name} (${i + 1} / ${allCharacters.length})`;
const genForm = document.getElementById('generate-form');
const fd = new FormData();
genForm.querySelectorAll('input[name="include_field"]:checked').forEach(cb => fd.append('include_field', cb.value));
fd.append('character_slug', char.slug);
fd.append('action', 'preview');
fd.append('client_id', clientId);
try {
progressContainer.classList.remove('d-none');
progressBar.style.width = '0%';
progressBar.textContent = '0%';
progressLabel.textContent = `${char.name}: Starting...`;
progressBar.style.width = '100%'; progressBar.textContent = '';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
progressLabel.textContent = `${char.name}: Queuing…`;
const resp = await fetch(genForm.getAttribute('action'), {
method: 'POST', body: fd, headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await resp.json();
if (data.error) { console.error(`Error for ${char.name}:`, data.error); progressContainer.classList.add('d-none'); continue; }
currentPromptId = data.prompt_id;
await waitForCompletion(currentPromptId);
progressLabel.textContent = 'Saving image...';
const finalFD = new FormData();
finalFD.append('action', 'preview');
const finalResp = await fetch(`${finalizeBaseUrl}/${currentPromptId}`, { method: 'POST', body: finalFD });
const finalData = await finalResp.json();
if (finalData.success) {
addToPreviewGallery(finalData.image_url, char.name);
previewImg.src = finalData.image_url;
currentJobId = data.job_id;
const jobResult = await waitForJob(currentJobId);
currentJobId = null;
if (jobResult.result && jobResult.result.image_url) {
addToPreviewGallery(jobResult.result.image_url, char.name);
previewImg.src = jobResult.result.image_url;
if (previewCard) previewCard.classList.remove('d-none');
}
currentPromptId = null;
} catch (err) {
console.error(`Failed for ${char.name}:`, err);
currentPromptId = null;
} finally {
progressContainer.classList.add('d-none');
}
} catch (err) { console.error(`Failed for ${char.name}:`, err); currentJobId = null; }
finally { progressContainer.classList.add('d-none'); progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated'); }
}
batchBar.style.width = '100%';
batchLabel.textContent = stopBatch ? 'Stopped.' : 'Complete!';
generateAllBtn.disabled = false;

View File

@@ -102,71 +102,18 @@
const sceneNameText = document.getElementById('current-scene-name');
const stepProgressText = document.getElementById('current-step-progress');
const clientId = 'scenes_batch_' + Math.random().toString(36).substring(2, 15);
const socket = new WebSocket('{{ COMFYUI_WS_URL }}?clientId=' + clientId);
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 value = msg.data.value;
const max = msg.data.max;
const percent = Math.round((value / 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);
});
}
@@ -183,66 +130,65 @@
batchBtn.disabled = true;
regenAllBtn.disabled = true;
container.classList.remove('d-none');
let completed = 0;
for (const scene of missing) {
const percent = Math.round((completed / missing.length) * 100);
progressBar.style.width = `${percent}%`;
progressBar.textContent = `${percent}%`;
statusText.textContent = `Batch Generating Scenes: ${completed + 1} / ${missing.length}`;
sceneNameText.textContent = `Current: ${scene.name}`;
nodeStatus.textContent = "Queuing...";
taskProgressBar.style.width = '100%';
taskProgressBar.textContent = 'Queued';
taskProgressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
// Phase 1: Queue all jobs upfront
progressBar.style.width = '100%';
progressBar.textContent = '';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
nodeStatus.textContent = 'Queuing…';
const jobs = [];
for (const scene of missing) {
statusText.textContent = `Queuing ${jobs.length + 1} / ${missing.length}`;
try {
const genResp = await fetch(`/scene/${scene.slug}/generate`, {
method: 'POST',
body: new URLSearchParams({
'action': 'replace',
'client_id': clientId,
'character_slug': '__random__'
}),
body: new URLSearchParams({ action: 'replace', character_slug: '__random__' }),
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const genData = await genResp.json();
currentPromptId = genData.prompt_id;
if (genData.job_id) jobs.push({ item: scene, jobId: genData.job_id });
} catch (err) {
console.error(`Failed to queue ${scene.name}:`, err);
}
}
await waitForCompletion(currentPromptId);
// Phase 2: Poll all concurrently
progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated');
progressBar.style.width = '0%';
progressBar.textContent = '0%';
statusText.textContent = `0 / ${jobs.length} done`;
const finResp = await fetch(`/scene/${scene.slug}/finalize_generation/${currentPromptId}`, {
method: 'POST',
body: new URLSearchParams({ 'action': 'replace' })
});
const finData = await finResp.json();
if (finData.success) {
const img = document.getElementById(`img-${scene.slug}`);
const noImgSpan = document.getElementById(`no-img-${scene.slug}`);
if (img) {
img.src = finData.image_url;
img.classList.remove('d-none');
}
let completed = 0;
await Promise.all(jobs.map(async ({ item, jobId }) => {
try {
const jobResult = await waitForJob(jobId);
if (jobResult.result && jobResult.result.image_url) {
const img = document.getElementById(`img-${item.slug}`);
const noImgSpan = document.getElementById(`no-img-${item.slug}`);
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 ${scene.name}:`, err);
console.error(`Failed for ${item.name}:`, err);
}
completed++;
}
const pct = Math.round((completed / jobs.length) * 100);
progressBar.style.width = `${pct}%`;
progressBar.textContent = `${pct}%`;
statusText.textContent = `${completed} / ${jobs.length} done`;
}));
progressBar.style.width = '100%';
progressBar.textContent = '100%';
statusText.textContent = "Batch Scene Generation Complete!";
sceneNameText.textContent = "";
nodeStatus.textContent = "Done";
stepProgressText.textContent = "";
statusText.textContent = 'Batch Scene Generation Complete!';
sceneNameText.textContent = '';
nodeStatus.textContent = 'Done';
stepProgressText.textContent = '';
taskProgressBar.style.width = '0%';
taskProgressBar.textContent = '';
batchBtn.disabled = false;
setTimeout(() => { container.classList.add('d-none'); }, 5000);
setTimeout(() => container.classList.add('d-none'), 5000);
}
batchBtn.addEventListener('click', async () => {

View File

@@ -285,170 +285,59 @@
}
});
// Generate a unique client ID
const clientId = 'style_detail_' + Math.random().toString(36).substring(2, 15);
// ComfyUI WebSocket
const socket = new WebSocket('{{ COMFYUI_WS_URL }}?clientId=' + clientId);
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 Image",
"9": "Saving Image"
};
let currentPromptId = null;
let currentJobId = null;
let currentAction = null;
socket.addEventListener('message', (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'status') {
if (!currentPromptId) {
const queueRemaining = msg.data.status.exec_info.queue_remaining;
if (queueRemaining > 0) {
progressLabel.textContent = `Queue position: ${queueRemaining}`;
}
}
}
else if (msg.type === 'progress') {
if (msg.data.prompt_id !== currentPromptId) return;
const value = msg.data.value;
const max = msg.data.max;
const percent = Math.round((value / max) * 100);
progressBar.style.width = `${percent}%`;
progressBar.textContent = `${percent}%`;
}
else if (msg.type === 'executing') {
if (msg.data.prompt_id !== currentPromptId) return;
const nodeId = msg.data.node;
if (nodeId === null) {
// Execution finished via WebSocket
console.log('Finished via WebSocket');
if (resolveCompletion) resolveCompletion();
} else {
const nodeName = nodeNames[nodeId] || `Processing...`;
progressLabel.textContent = nodeName;
}
}
});
let resolveCompletion = null;
async function waitForCompletion(promptId) {
return new Promise((resolve) => {
const checkResolve = () => {
clearInterval(pollInterval);
resolve();
};
resolveCompletion = checkResolve;
// Fallback polling in case WebSocket is blocked (403)
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();
}
} catch (err) { console.error('Polling error:', err); }
}, 2000);
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') progressLabel.textContent = 'Generating…';
else progressLabel.textContent = 'Queued…';
} catch (err) { console.error('Poll error:', err); }
}, 1500);
});
}
form.addEventListener('submit', async (e) => {
const submitter = e.submitter;
if (!submitter || submitter.value !== 'preview') return;
e.preventDefault();
currentAction = submitter.value;
const formData = new FormData(form);
formData.append('action', currentAction);
formData.append('client_id', clientId);
progressContainer.classList.remove('d-none');
progressBar.style.width = '0%';
progressBar.textContent = '0%';
progressLabel.textContent = 'Starting...';
progressBar.style.width = '100%'; progressBar.textContent = '';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
progressLabel.textContent = 'Queuing…';
try {
const response = await fetch(form.getAttribute('action'), {
method: 'POST',
body: formData,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await response.json();
if (data.error) {
alert('Error: ' + data.error);
progressContainer.classList.add('d-none');
return;
}
currentPromptId = data.prompt_id;
progressLabel.textContent = 'Queued...';
progressBar.style.width = '100%';
progressBar.textContent = 'Queued';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
await waitForCompletion(currentPromptId);
finalizeGeneration(currentPromptId, currentAction);
currentPromptId = null;
} catch (err) {
console.error(err);
alert('Request failed');
progressContainer.classList.add('d-none');
}
});
async function finalizeGeneration(promptId, action) {
progressLabel.textContent = 'Saving image...';
const url = `/style/{{ style.slug }}/finalize_generation/${promptId}`;
const formData = new FormData();
formData.append('action', 'preview'); // Always save as preview
try {
const response = await fetch(url, { method: 'POST', body: formData });
const data = await response.json();
if (data.success) {
previewImg.src = data.image_url;
if (data.error) { alert('Error: ' + data.error); progressContainer.classList.add('d-none'); return; }
currentJobId = data.job_id;
const jobResult = await waitForJob(currentJobId);
currentJobId = null;
if (jobResult.result && jobResult.result.image_url) {
previewImg.src = jobResult.result.image_url;
if (previewCard) previewCard.classList.remove('d-none');
const replaceBtn = document.getElementById('replace-cover-btn');
if (replaceBtn) {
replaceBtn.disabled = false;
const form = replaceBtn.closest('form');
if (form) {
form.action = `/style/{{ style.slug }}/replace_cover_from_preview`;
}
}
} else {
alert('Save failed: ' + data.error);
if (replaceBtn) replaceBtn.disabled = false;
}
} catch (err) {
console.error(err);
alert('Finalize request failed');
} finally {
progressContainer.classList.add('d-none');
}
}
} catch (err) { console.error(err); alert('Generation failed: ' + err.message); }
finally { progressContainer.classList.add('d-none'); progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated'); }
});
// Batch: Generate All Characters
const allCharacters = [
{% for char in characters %}{ slug: "{{ char.slug }}", name: {{ char.name | tojson }} },
{% endfor %}
];
const finalizeBaseUrl = '/style/{{ style.slug }}/finalize_generation';
let stopBatch = false;
const generateAllBtn = document.getElementById('generate-all-btn');
@@ -484,54 +373,37 @@
stopAllBtn.classList.remove('d-none');
batchProgress.classList.remove('d-none');
bootstrap.Tab.getOrCreateInstance(document.getElementById('previews-tab')).show();
for (let i = 0; i < allCharacters.length; i++) {
if (stopBatch) break;
const char = allCharacters[i];
batchBar.style.width = `${Math.round((i / allCharacters.length) * 100)}%`;
batchLabel.textContent = `Generating ${char.name} (${i + 1} / ${allCharacters.length})`;
const genForm = document.getElementById('generate-form');
const fd = new FormData();
genForm.querySelectorAll('input[name="include_field"]:checked').forEach(cb => fd.append('include_field', cb.value));
fd.append('character_slug', char.slug);
fd.append('action', 'preview');
fd.append('client_id', clientId);
try {
progressContainer.classList.remove('d-none');
progressBar.style.width = '0%';
progressBar.textContent = '0%';
progressLabel.textContent = `${char.name}: Starting...`;
progressBar.style.width = '100%'; progressBar.textContent = '';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
progressLabel.textContent = `${char.name}: Queuing…`;
const resp = await fetch(genForm.getAttribute('action'), {
method: 'POST', body: fd, headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await resp.json();
if (data.error) { console.error(`Error for ${char.name}:`, data.error); progressContainer.classList.add('d-none'); continue; }
currentPromptId = data.prompt_id;
await waitForCompletion(currentPromptId);
progressLabel.textContent = 'Saving image...';
const finalFD = new FormData();
finalFD.append('action', 'preview');
const finalResp = await fetch(`${finalizeBaseUrl}/${currentPromptId}`, { method: 'POST', body: finalFD });
const finalData = await finalResp.json();
if (finalData.success) {
addToPreviewGallery(finalData.image_url, char.name);
previewImg.src = finalData.image_url;
currentJobId = data.job_id;
const jobResult = await waitForJob(currentJobId);
currentJobId = null;
if (jobResult.result && jobResult.result.image_url) {
addToPreviewGallery(jobResult.result.image_url, char.name);
previewImg.src = jobResult.result.image_url;
if (previewCard) previewCard.classList.remove('d-none');
}
currentPromptId = null;
} catch (err) {
console.error(`Failed for ${char.name}:`, err);
currentPromptId = null;
} finally {
progressContainer.classList.add('d-none');
}
} catch (err) { console.error(`Failed for ${char.name}:`, err); currentJobId = null; }
finally { progressContainer.classList.add('d-none'); progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated'); }
}
batchBar.style.width = '100%';
batchLabel.textContent = stopBatch ? 'Stopped.' : 'Complete!';
generateAllBtn.disabled = false;

View File

@@ -102,71 +102,18 @@
const styleNameText = document.getElementById('current-style-name');
const stepProgressText = document.getElementById('current-step-progress');
const clientId = 'styles_batch_' + Math.random().toString(36).substring(2, 15);
const socket = new WebSocket('{{ COMFYUI_WS_URL }}?clientId=' + clientId);
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 value = msg.data.value;
const max = msg.data.max;
const percent = Math.round((value / 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);
});
}
@@ -183,62 +130,61 @@
batchBtn.disabled = true;
regenAllBtn.disabled = true;
container.classList.remove('d-none');
let completed = 0;
for (const style of missing) {
const percent = Math.round((completed / missing.length) * 100);
progressBar.style.width = `${percent}%`;
progressBar.textContent = `${percent}%`;
statusText.textContent = `Batch Generating Styles: ${completed + 1} / ${missing.length}`;
styleNameText.textContent = `Current: ${style.name}`;
nodeStatus.textContent = "Queuing...";
taskProgressBar.style.width = '100%';
taskProgressBar.textContent = 'Queued';
taskProgressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
// Phase 1: Queue all jobs upfront
progressBar.style.width = '100%';
progressBar.textContent = '';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
nodeStatus.textContent = 'Queuing…';
const jobs = [];
for (const style of missing) {
statusText.textContent = `Queuing ${jobs.length + 1} / ${missing.length}`;
try {
const genResp = await fetch(`/style/${style.slug}/generate`, {
method: 'POST',
body: new URLSearchParams({
'action': 'replace',
'client_id': clientId,
'character_slug': '__random__'
}),
body: new URLSearchParams({ action: 'replace', character_slug: '__random__' }),
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const genData = await genResp.json();
currentPromptId = genData.prompt_id;
if (genData.job_id) jobs.push({ item: style, jobId: genData.job_id });
} catch (err) {
console.error(`Failed to queue ${style.name}:`, err);
}
}
await waitForCompletion(currentPromptId);
// Phase 2: Poll all concurrently
progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated');
progressBar.style.width = '0%';
progressBar.textContent = '0%';
statusText.textContent = `0 / ${jobs.length} done`;
const finResp = await fetch(`/style/${style.slug}/finalize_generation/${currentPromptId}`, {
method: 'POST',
body: new URLSearchParams({ 'action': 'replace' })
});
const finData = await finResp.json();
if (finData.success) {
const img = document.getElementById(`img-${style.slug}`);
const noImgSpan = document.getElementById(`no-img-${style.slug}`);
if (img) {
img.src = finData.image_url;
img.classList.remove('d-none');
}
let completed = 0;
await Promise.all(jobs.map(async ({ item, jobId }) => {
try {
const jobResult = await waitForJob(jobId);
if (jobResult.result && jobResult.result.image_url) {
const img = document.getElementById(`img-${item.slug}`);
const noImgSpan = document.getElementById(`no-img-${item.slug}`);
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 ${style.name}:`, err);
console.error(`Failed for ${item.name}:`, err);
}
completed++;
}
const pct = Math.round((completed / jobs.length) * 100);
progressBar.style.width = `${pct}%`;
progressBar.textContent = `${pct}%`;
statusText.textContent = `${completed} / ${jobs.length} done`;
}));
progressBar.style.width = '100%';
progressBar.textContent = '100%';
statusText.textContent = "Batch Style Generation Complete!";
styleNameText.textContent = "";
nodeStatus.textContent = "Done";
stepProgressText.textContent = "";
statusText.textContent = 'Batch Style Generation Complete!';
styleNameText.textContent = '';
nodeStatus.textContent = 'Done';
stepProgressText.textContent = '';
taskProgressBar.style.width = '0%';
taskProgressBar.textContent = '';
batchBtn.disabled = false;