Add Checkpoints Gallery with per-checkpoint generation settings

- New Checkpoint model (slug, name, checkpoint_path, data JSON, image_path)
- sync_checkpoints() loads metadata from data/checkpoints/*.json and falls
  back to template defaults for models without a JSON file
- _apply_checkpoint_settings() applies per-checkpoint steps, CFG, sampler,
  base positive/negative prompts, and VAE (with dynamic VAELoader node
  injection for non-integrated VAEs) to the ComfyUI workflow
- Bulk Create from Checkpoints: scans Illustrious/Noob model directories,
  reads matching HTML files, uses LLM to populate metadata, falls back to
  template defaults when no HTML is present
- Gallery index with batch cover generation and WebSocket progress bar
- Detail page showing Generation Settings and Base Prompts cards
- Checkpoints nav link added to layout
- New data/prompts/checkpoint_system.txt LLM system prompt
- Updated README with all current galleries and file structure
- Also includes accumulated action/scene JSON updates, new actions, and
  other template/generator improvements from prior sessions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Aodhan Collins
2026-02-26 21:25:23 +00:00
parent 0d7d4d404f
commit 0b8802deb5
334 changed files with 9437 additions and 3772 deletions

View File

@@ -212,23 +212,58 @@
}
});
// Generate a unique client ID
const clientId = 'scene_detail_' + Math.random().toString(36).substring(2, 15);
const socket = new WebSocket(`ws://127.0.0.1:8188/ws?clientId=${clientId}`);
// 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) => {
if (!currentPromptId) return;
const msg = JSON.parse(event.data);
if (msg.type === 'progress') {
const percent = Math.round((msg.data.value / msg.data.max) * 100);
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.node === null && msg.data.prompt_id === currentPromptId) {
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;
}
}
});
@@ -240,12 +275,17 @@
resolve();
};
resolveCompletion = checkResolve;
// Fallback polling in case WebSocket is blocked (403)
const pollInterval = setInterval(async () => {
try {
const resp = await fetch(`/check_status/${promptId}`);
const data = await resp.json();
if (data.status === 'finished') checkResolve();
} catch (err) {}
if (data.status === 'finished') {
console.log('Finished via Polling');
checkResolve();
}
} catch (err) { console.error('Polling error:', err); }
}, 2000);
});
}
@@ -253,6 +293,7 @@
form.addEventListener('submit', async (e) => {
const submitter = e.submitter;
if (!submitter || submitter.value !== 'preview') return;
e.preventDefault();
const formData = new FormData(form);
@@ -262,7 +303,7 @@
progressContainer.classList.remove('d-none');
progressBar.style.width = '0%';
progressBar.textContent = '0%';
progressLabel.textContent = 'Queued...';
progressLabel.textContent = 'Starting...';
try {
const response = await fetch(form.getAttribute('action'), {
@@ -270,12 +311,30 @@
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');
}
});
@@ -289,19 +348,33 @@
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 (previewCard) previewCard.classList.remove('d-none');
document.getElementById('replace-cover-btn').disabled = false;
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);
}
} catch (err) {
console.error(err);
alert('Finalize request failed');
} finally {
progressContainer.classList.add('d-none');
}
}
});
// Image modal function
function showImage(src) {
document.getElementById('modalImage').src = src;
}