pixelkiln 0.35.0 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +7 -0
- package/README.md +1 -1
- package/dist/cli.js +800 -589
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.js +42 -5
- package/dist/index.cjs +551 -323
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +616 -71
- package/dist/index.d.ts +616 -71
- package/dist/index.js +538 -320
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +8 -1
- package/docs/CLI.md +62 -2
- package/docs/LIBRARY.md +32 -0
- package/docs/MANIFEST.md +50 -2
- package/docs/QUALITY.md +16 -0
- package/docs/RECIPES.md +6 -2
- package/package.json +3 -1
- package/recipes/comfyui/pixel-art-xl-environment/1.0.0/pixelkiln.recipe.json +1 -1
- package/recipes/comfyui/pixel-art-xl-img2img/1.0.0/pixelkiln.recipe.json +1 -1
- package/schema/manifest.schema.json +8 -0
- package/schema/recipe.schema.json +4 -0
package/dist/client/gallery.js
CHANGED
|
@@ -97,6 +97,11 @@ const ACTIVE_PHASES = new Set(['queued', 'submitting', 'polling', 'fetching', 'r
|
|
|
97
97
|
const PHASE_TONE = { queued: 'cool', submitting: 'cool', polling: 'cool', fetching: 'cool', review: 'warn', done: 'ok', failed: 'bad' };
|
|
98
98
|
// The job still working on this record, if any: what the drawer and its card
|
|
99
99
|
// show while a regeneration, restore, or pull is in flight.
|
|
100
|
+
/** The style's palette rule as the manifest has it now, or null. */
|
|
101
|
+
const paletteRuleOf = (item) => {
|
|
102
|
+
const style = snap.styles.find((s) => s.id === item.styleId && s.project === item.project);
|
|
103
|
+
return style && style.enforcePalette && style.palette.length >= 2 ? style.palette : null;
|
|
104
|
+
};
|
|
100
105
|
const activeJobFor = (item) => GEN.jobs.find((job) => ACTIVE_PHASES.has(job.phase) && job.project === item.project && job.keys.includes(item.key)) || null;
|
|
101
106
|
const JOB_VERB = { generate: 'regenerating', resume: 'resuming', refresh: 'pulling upstream changes for', restore: 'restoring', revert: 'bringing back a previous generation of' };
|
|
102
107
|
const PHASE_TEXT = { queued: 'queued', submitting: 'submitting to the provider', polling: 'waiting for the provider to finish', fetching: 'downloading the result', review: 'candidates are ready to review' };
|
|
@@ -140,6 +145,7 @@ async function pollJobs() {
|
|
|
140
145
|
: job.mode === 'restore' ? 'Restored. The recorded bytes are back on disk.'
|
|
141
146
|
: job.mode === 'revert' ? 'Brought back generation #' + job.revert + '. The one it replaced is now #1 in the list below.'
|
|
142
147
|
: job.mode === 'refresh' ? (job.counts.downloaded ? 'Pulled the upstream change; the lockfile records the new bytes.' : 'Nothing changed upstream; the file was left alone.')
|
|
148
|
+
: job.mode === 'resume' && job.counts.downloaded ? 'Done. The file on disk and the record now match the manifest.'
|
|
143
149
|
: 'Done.' };
|
|
144
150
|
}
|
|
145
151
|
}
|
|
@@ -334,7 +340,14 @@ function generateDialog(items, { project = null, force = false, resume = false,
|
|
|
334
340
|
sum.append(el('div', null, 'Puts the recorded bytes back on disk from the local cache or the provider object. Nothing is generated or submitted.'));
|
|
335
341
|
if (force && replacing.length) sum.append(el('div', 'warn', 'The file on disk was changed after PixelKiln wrote it. Restoring discards that change. To keep it instead, cancel and use Edit by hand or Edit in browser first: the edit is copied beside the generated file, and the record stays intact.'));
|
|
336
342
|
} else if (resume) {
|
|
337
|
-
|
|
343
|
+
const drifted = items.filter((i) => i.state === 'recoverable' && i.status === 'downloaded');
|
|
344
|
+
if (drifted.length === items.length) {
|
|
345
|
+
sum.append(el('div', null, drifted.every((i) => paletteRuleOf(i))
|
|
346
|
+
? 'Snaps each file to the style’s palette from the provider’s bytes in the local cache (or the file itself). The record keeps both hashes, so this can be undone the same way. Nothing is submitted.'
|
|
347
|
+
: 'Puts the provider’s original bytes back from the local cache, or re-downloads them, and clears the palette record. Nothing is submitted.'));
|
|
348
|
+
} else {
|
|
349
|
+
sum.append(el('div', null, 'Polls, reviews, and downloads existing provider work. Nothing is submitted.'));
|
|
350
|
+
}
|
|
338
351
|
} else {
|
|
339
352
|
for (const [provider, g] of byProvider) {
|
|
340
353
|
const left = remainingBudget(provider);
|
|
@@ -1114,7 +1127,11 @@ function generateActions(item) {
|
|
|
1114
1127
|
add('Regenerate' + cost, null, { force: true });
|
|
1115
1128
|
} else if (item.state === 'orphaned') add('Restore · no cost', 'primary', { restore: true });
|
|
1116
1129
|
else if (item.state === 'untracked') add('Generate' + cost + ' · replaces the file', null, { force: true });
|
|
1117
|
-
if (item.state === '
|
|
1130
|
+
if (item.state === 'recoverable' && item.status === 'downloaded') {
|
|
1131
|
+
// The files are fine; the manifest's palette rule changed under them.
|
|
1132
|
+
// A resume runs the same fetch that re-applies it, from the raw bytes.
|
|
1133
|
+
add(paletteRuleOf(item) ? 'Snap to the palette · no cost' : 'Put the provider’s bytes back · no cost', 'primary', { resume: true });
|
|
1134
|
+
} else if (item.state === 'in-flight' || item.state === 'recoverable') {
|
|
1118
1135
|
// A job already parked this asset in review: go straight to its sheet.
|
|
1119
1136
|
const holder = GEN.jobs.find((job) => job.phase === 'review' && job.project === item.project && job.review.includes(item.key));
|
|
1120
1137
|
if (holder) {
|
|
@@ -1386,7 +1403,8 @@ function inheritorsOf(style, field) {
|
|
|
1386
1403
|
// noBackground reaches the request only for pixflux and non-PixelLab
|
|
1387
1404
|
// providers; elsewhere a change is recorded but alters nothing.
|
|
1388
1405
|
const fieldReaches = (field, style) =>
|
|
1389
|
-
field
|
|
1406
|
+
field === 'enforcePalette' ? false
|
|
1407
|
+
: field !== 'noBackground' || style.generator === 'pixflux' || style.provider !== 'pixellab';
|
|
1390
1408
|
function blastRadius(style, fields) {
|
|
1391
1409
|
const byId = new Map(snap.styles.filter((x) => x.project === style.project).map((x) => [x.id, x]));
|
|
1392
1410
|
const styleIds = new Set();
|
|
@@ -1424,6 +1442,9 @@ function styleForm(style) {
|
|
|
1424
1442
|
const noBg = el('select');
|
|
1425
1443
|
noBg.append(new Option(style.extends ? 'inherit from ' + style.extends : 'default (on)', 'default'), new Option('on — strip the generated background', 'on'), new Option('off — keep the background (scenes, banners)', 'off'));
|
|
1426
1444
|
noBg.value = own('noBackground') ? (style.noBackground ? 'on' : 'off') : 'default';
|
|
1445
|
+
const enforce = el('input'); enforce.type = 'checkbox'; enforce.checked = style.enforcePalette;
|
|
1446
|
+
const enforceLabel = el('label', 'chip');
|
|
1447
|
+
enforceLabel.append(enforce, document.createTextNode(' snap downloaded art to this palette (no dithering)'));
|
|
1427
1448
|
const swatches = el('div', 'pal');
|
|
1428
1449
|
const drawSwatches = () => {
|
|
1429
1450
|
swatches.textContent = '';
|
|
@@ -1433,8 +1454,9 @@ function styleForm(style) {
|
|
|
1433
1454
|
form.append(
|
|
1434
1455
|
field('prompt prefix', prefix, 'Prepended to every asset prompt in this style. ' + provenance('promptPrefix')),
|
|
1435
1456
|
field('prompt suffix', suffix, 'Appended to every asset prompt in this style. ' + provenance('promptSuffix')),
|
|
1436
|
-
field('palette', palette, '
|
|
1457
|
+
field('palette', palette, 'Sent to providers that take a palette; with snapping on, guaranteed on every file after download. ' + provenance('palette')),
|
|
1437
1458
|
swatches,
|
|
1459
|
+
field('after download', enforceLabel, 'Turning this on or off changes no request: the next fetch re-applies it to the files already on disk, spending nothing. ' + provenance('enforcePalette')),
|
|
1438
1460
|
);
|
|
1439
1461
|
const row = el('div', 'row');
|
|
1440
1462
|
row.append(
|
|
@@ -1448,6 +1470,7 @@ function styleForm(style) {
|
|
|
1448
1470
|
if (prefix.value !== style.promptPrefix) out.push('promptPrefix');
|
|
1449
1471
|
if (suffix.value !== style.promptSuffix) out.push('promptSuffix');
|
|
1450
1472
|
if (parsePalette(palette.value).join(',') !== style.palette.map((c) => c.toLowerCase()).join(',')) out.push('palette');
|
|
1473
|
+
if (enforce.checked !== style.enforcePalette) out.push('enforcePalette');
|
|
1451
1474
|
if (view.value.trim() !== (style.view || '')) out.push('view');
|
|
1452
1475
|
const bgNow = own('noBackground') ? (style.noBackground ? 'on' : 'off') : 'default';
|
|
1453
1476
|
if (noBg.value !== bgNow) out.push('noBackground');
|
|
@@ -1460,7 +1483,9 @@ function styleForm(style) {
|
|
|
1460
1483
|
const r = blastRadius(style, fields);
|
|
1461
1484
|
if (!r.assets) {
|
|
1462
1485
|
blast.className = 'blast';
|
|
1463
|
-
blast.textContent =
|
|
1486
|
+
blast.textContent = fields.every((f) => f === 'enforcePalette')
|
|
1487
|
+
? 'No request changes. Generated files in ' + style.id + ' become recoverable; the next fetch or restore re-applies the palette rule to them for free.'
|
|
1488
|
+
: 'Recorded in the manifest only: nothing in ' + style.id + ' sends this field, so no request changes.';
|
|
1464
1489
|
return;
|
|
1465
1490
|
}
|
|
1466
1491
|
blast.className = 'blast hot';
|
|
@@ -1474,6 +1499,7 @@ function styleForm(style) {
|
|
|
1474
1499
|
};
|
|
1475
1500
|
for (const input of [prefix, suffix, palette, view]) input.addEventListener('input', updateBlast);
|
|
1476
1501
|
noBg.addEventListener('change', updateBlast);
|
|
1502
|
+
enforce.addEventListener('change', updateBlast);
|
|
1477
1503
|
updateBlast();
|
|
1478
1504
|
form.append(blast);
|
|
1479
1505
|
const actions = el('div', 'actions');
|
|
@@ -1488,10 +1514,12 @@ function styleForm(style) {
|
|
|
1488
1514
|
if (!fields.length) { ui.editing = null; render(); return; }
|
|
1489
1515
|
const colors = parsePalette(palette.value);
|
|
1490
1516
|
if (fields.includes('palette') && !paletteValid(colors)) { msg.className = 'msg bad'; msg.textContent = 'palette must be six-digit hex colours'; return; }
|
|
1517
|
+
if (enforce.checked && colors.length < 2) { msg.className = 'msg bad'; msg.textContent = 'snapping needs a palette of at least two colours'; return; }
|
|
1491
1518
|
const patch = {};
|
|
1492
1519
|
if (fields.includes('promptPrefix')) patch.promptPrefix = prefix.value;
|
|
1493
1520
|
if (fields.includes('promptSuffix')) patch.promptSuffix = suffix.value;
|
|
1494
1521
|
if (fields.includes('palette')) patch.palette = colors;
|
|
1522
|
+
if (fields.includes('enforcePalette')) patch.enforcePalette = enforce.checked;
|
|
1495
1523
|
if (fields.includes('view')) patch.view = view.value.trim();
|
|
1496
1524
|
if (fields.includes('noBackground')) patch.noBackground = noBg.value === 'default' ? null : noBg.value === 'on';
|
|
1497
1525
|
save.disabled = true; msg.className = 'msg'; msg.textContent = 'saving…';
|
|
@@ -1830,6 +1858,15 @@ function renderDrawer() {
|
|
|
1830
1858
|
if (item.currentPrompt) row(dl, 'prompt now', el('div', 'prompt state-warn', item.currentPrompt));
|
|
1831
1859
|
row(dl, 'size', item.width + ' × ' + item.height + ' px');
|
|
1832
1860
|
if (item.status) row(dl, 'cost', fmtCost(item.costUnit, item.cost));
|
|
1861
|
+
if (item.postprocess && item.postprocess.palette) {
|
|
1862
|
+
// What fetch did to the provider's bytes before writing them.
|
|
1863
|
+
const p = item.postprocess.palette;
|
|
1864
|
+
const wrap = el('span');
|
|
1865
|
+
const sw = el('span', 'swatches');
|
|
1866
|
+
for (const c of p.colors) { const i = el('i'); i.style.background = c; i.title = c; sw.append(i); }
|
|
1867
|
+
wrap.append(document.createTextNode('snapped to ' + p.colors.length + ' colours, no dithering '), sw);
|
|
1868
|
+
row(dl, 'palette', wrap);
|
|
1869
|
+
}
|
|
1833
1870
|
if (item.estimatedCost !== null && (item.estimatedCost !== item.cost || !item.status)) {
|
|
1834
1871
|
row(dl, 'estimate now', fmtCost(item.costUnit, item.estimatedCost) +
|
|
1835
1872
|
(item.candidates && item.candidates > 1 ? ' for ' + item.candidates + ' candidates' : ''));
|