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

@@ -209,23 +209,37 @@
const clientId = 'style_detail_' + Math.random().toString(36).substring(2, 15);
// ComfyUI WebSocket
const socket = new WebSocket(`ws://127.0.0.1:8188/ws?clientId=${clientId}`);
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 currentAction = null;
socket.addEventListener('message', (event) => {
if (!currentPromptId) return;
const msg = JSON.parse(event.data);
if (msg.type === 'status') {
const queueRemaining = msg.data.status.exec_info.queue_remaining;
if (queueRemaining > 0) {
progressLabel.textContent = `Queue position: ${queueRemaining}`;
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);
@@ -233,9 +247,16 @@
progressBar.textContent = `${percent}%`;
}
else if (msg.type === 'executing') {
if (msg.data.node === null && msg.data.prompt_id === currentPromptId) {
// Execution finished
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;
}
}
});
@@ -249,7 +270,7 @@
};
resolveCompletion = checkResolve;
// Fallback polling
// Fallback polling in case WebSocket is blocked (403)
const pollInterval = setInterval(async () => {
try {
const resp = await fetch(`/check_status/${promptId}`);
@@ -294,6 +315,10 @@
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;

View File

@@ -9,6 +9,10 @@
<form action="{{ url_for('bulk_create_styles_from_loras') }}" method="post" class="me-2">
<button type="submit" class="btn btn-primary">Bulk Create from LoRAs</button>
</form>
<form action="{{ url_for('bulk_create_styles_from_loras') }}" method="post" class="me-2">
<input type="hidden" name="overwrite" value="true">
<button type="submit" class="btn btn-danger" onclick="return confirm('WARNING: This will re-run LLM generation for ALL style LoRAs, consuming significant API credits and overwriting ALL existing style metadata. Are you absolutely sure?')">Bulk Overwrite from LoRAs</button>
</form>
<a href="{{ url_for('create_style') }}" class="btn btn-success me-2">Create New Style</a>
<form action="{{ url_for('rescan_styles') }}" method="post">
<button type="submit" class="btn btn-outline-primary">Rescan Style Files</button>
@@ -19,11 +23,27 @@
<!-- Batch Progress Bar -->
<div id="batch-progress-container" class="card mb-4 d-none">
<div class="card-body">
<h5 id="batch-status-text">Batch Generating Styles...</h5>
<div class="progress mt-2" role="progressbar" aria-label="Batch Progress" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
<div id="batch-progress-bar" class="progress-bar progress-bar-striped progress-bar-animated bg-success" style="width: 0%">0%</div>
<div class="d-flex justify-content-between align-items-center mb-1">
<h5 id="batch-status-text" class="mb-0">Batch Generating Styles...</h5>
<span id="batch-node-status" class="badge bg-info text-dark">Starting...</span>
</div>
<div class="mb-3">
<small class="text-muted">Overall Batch Progress</small>
<div class="progress" role="progressbar" aria-label="Batch Progress" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="height: 10px;">
<div id="batch-progress-bar" class="progress-bar bg-success" style="width: 0%"></div>
</div>
</div>
<div class="mb-2">
<div class="d-flex justify-content-between">
<small id="current-style-name" class="text-muted mb-1"></small>
<small id="current-step-progress" class="text-muted mb-1"></small>
</div>
<div class="progress" role="progressbar" aria-label="Task Progress" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="height: 20px;">
<div id="task-progress-bar" class="progress-bar progress-bar-striped progress-bar-animated bg-info" style="width: 0%"></div>
</div>
</div>
<p id="current-style-name" class="small text-muted mt-2 mb-0"></p>
</div>
</div>
@@ -64,21 +84,58 @@
const batchBtn = document.getElementById('batch-generate-btn');
const regenAllBtn = document.getElementById('regenerate-all-btn');
const progressBar = document.getElementById('batch-progress-bar');
const taskProgressBar = document.getElementById('task-progress-bar');
const container = document.getElementById('batch-progress-container');
const statusText = document.getElementById('batch-status-text');
const nodeStatus = document.getElementById('batch-node-status');
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(`ws://127.0.0.1:8188/ws?clientId=${clientId}`);
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 === 'executing') {
if (msg.data.node === null && msg.data.prompt_id === currentPromptId) {
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');
}
}
}
});
@@ -118,12 +175,16 @@
let completed = 0;
for (const style of missing) {
completed++;
const percent = Math.round((completed / missing.length) * 100);
progressBar.style.width = `${percent}%`;
progressBar.textContent = `${percent}%`;
statusText.textContent = `Batch Generating Styles: ${completed} / ${missing.length}`;
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');
try {
const genResp = await fetch(`/style/${style.slug}/generate`, {
@@ -158,10 +219,17 @@
} catch (err) {
console.error(`Failed for ${style.name}:`, err);
}
completed++;
}
progressBar.style.width = '100%';
progressBar.textContent = '100%';
statusText.textContent = "Batch Style Generation Complete!";
styleNameText.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);