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:
@@ -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;
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
|
||||
@@ -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%';
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user