pixelkiln 0.52.0 → 0.53.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.
@@ -111,6 +111,20 @@ const paletteRuleOf = (item) => {
111
111
  const style = snap.styles.find((s) => s.id === item.styleId && s.project === item.project);
112
112
  return style && style.enforcePalette && style.palette.length >= 2 ? style.palette : null;
113
113
  };
114
+ /**
115
+ * Whether this item has usable pixels to revise: committed `source` art
116
+ * always qualifies, otherwise it needs a current, downloaded generation, or
117
+ * (for a quality-gated style) a current, human-approved refined output. This
118
+ * mirrors docs/REVISIONS.md's dependency gate closely enough to gate the
119
+ * button; the server's own plan is still the final word once the revision
120
+ * asset exists, and reports `blocked` with the real reason if this guess
121
+ * were ever wrong.
122
+ */
123
+ const isRevisable = (item) => {
124
+ if (item.source) return true;
125
+ if (item.quality) return item.quality.review?.status === 'approved' && !!item.quality.check?.current;
126
+ return item.status === 'downloaded' && item.state === 'ok';
127
+ };
114
128
  const activeJobFor = (item) => GEN.jobs.find((job) => ACTIVE_PHASES.has(job.phase) && job.project === item.project && job.keys.includes(item.key)) || null;
115
129
  const JOB_VERB = { generate: 'regenerating', resume: 'resuming', refresh: 'pulling upstream changes for', restore: 'restoring', revert: 'bringing back a previous generation of' };
116
130
  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' };
@@ -1570,7 +1584,7 @@ function addAssetForm(style) {
1570
1584
  const form = el('form', 'edit');
1571
1585
  form.append(el('h3', null, 'New asset in ' + style.id));
1572
1586
  const id = el('input'); id.type = 'text'; id.placeholder = 'asset-id'; id.required = true; id.autocomplete = 'off';
1573
- id.pattern = '[^/\\]+';
1587
+ id.pattern = '[^\\/\\\\]+';
1574
1588
  const prompt = el('textarea'); prompt.placeholder = 'What to generate. The style adds its prefix and suffix.'; prompt.required = true;
1575
1589
  const width = numberInput(null, 'style default'), height = numberInput(null, 'style default');
1576
1590
  const category = el('input'); category.type = 'text'; category.placeholder = 'optional subfolder';
@@ -1613,6 +1627,54 @@ function addAssetForm(style) {
1613
1627
  return form;
1614
1628
  }
1615
1629
 
1630
+ /**
1631
+ * A new `image-to-image` revision of `item`, restricted to `item`'s own
1632
+ * style (a revision's `from` resolves per style, and this button starts
1633
+ * from one specific generation). `inpaint` needs a mask upload the gallery
1634
+ * does not offer yet, so this form only ever creates `image-to-image`.
1635
+ */
1636
+ function newRevisionForm(item) {
1637
+ const pr = projectOf(item);
1638
+ const form = el('form', 'edit');
1639
+ form.append(el('h3', null, 'New revision of ' + item.assetId));
1640
+ const id = el('input'); id.type = 'text'; id.placeholder = 'asset-id'; id.required = true; id.autocomplete = 'off';
1641
+ id.pattern = '[^\\/\\\\]+';
1642
+ const prompt = el('textarea'); prompt.placeholder = 'Edit instruction, e.g. "add snow on the roof".'; prompt.required = true;
1643
+ const strength = el('input'); strength.type = 'number'; strength.min = '0'; strength.max = '1'; strength.step = '0.05'; strength.placeholder = 'provider default';
1644
+ const row1 = el('div', 'row');
1645
+ row1.append(field('new asset id', id), field('strength', strength, 'PixelLab rejects an explicit strength for image-to-image; leave this blank there.'));
1646
+ form.append(row1, field('edit instruction', prompt, 'The style still adds its prefix and suffix.'));
1647
+ const actions = el('div', 'actions');
1648
+ const save = el('button', 'primary', 'Create revision'); save.type = 'submit';
1649
+ const cancel = el('button', null, 'Cancel'); cancel.type = 'button'; cancel.onclick = () => { ui.editing = null; renderDrawer(); };
1650
+ const msg = el('span', 'msg');
1651
+ actions.append(save, cancel, msg);
1652
+ form.append(actions);
1653
+ form.onsubmit = async (e) => {
1654
+ e.preventDefault();
1655
+ save.disabled = true; msg.className = 'msg'; msg.textContent = 'saving…';
1656
+ const revision = { mode: 'image-to-image', from: item.assetId };
1657
+ if (strength.value.trim() !== '') revision.strength = Number(strength.value);
1658
+ const asset = { prompt: prompt.value, styles: [item.styleId], revision };
1659
+ try {
1660
+ const body = { action: 'add-asset', assetId: id.value.trim(), expectedSha256: pr.manifestSha256, asset };
1661
+ if (item.project) body.project = item.project;
1662
+ snap = await postEdit(body);
1663
+ const newId = (item.project ? item.project + ':' : '') + item.styleId + '/' + id.value.trim();
1664
+ ui.editing = null;
1665
+ ui.notice = { id: newId, text: 'Added to the manifest. Nothing is generated until you run pixelkiln gen.' };
1666
+ render();
1667
+ if (snap.items.some((i) => i.id === newId)) openItem(newId);
1668
+ } catch (err) {
1669
+ save.disabled = false;
1670
+ msg.className = 'msg bad';
1671
+ msg.textContent = err.message + (err.status === 409 ? ' Press Refresh.' : '');
1672
+ }
1673
+ };
1674
+ setTimeout(() => id.focus(), 0);
1675
+ return form;
1676
+ }
1677
+
1616
1678
  function editForm(item) {
1617
1679
  const pr = projectOf(item);
1618
1680
  const a = item.asset;
@@ -1980,9 +2042,18 @@ function renderDrawer() {
1980
2042
  body.append(s);
1981
2043
  }
1982
2044
  const children = snap.items.filter((i) => i.revisionParentKey === item.key && i.project === item.project);
1983
- if (children.length) {
2045
+ const reviseKey = 'revise:' + item.id;
2046
+ const canRevise = canEdit && isRevisable(item);
2047
+ if (children.length || canRevise) {
1984
2048
  const { s, dl } = section('Revisions from this asset');
1985
2049
  for (const c of children) row(dl, c.revision ? c.revision.mode : 'child', keyLink(item, c.key));
2050
+ if (canRevise) {
2051
+ const revise = el('button', ui.editing === reviseKey ? null : 'add', ui.editing === reviseKey ? 'Cancel' : '+ New revision');
2052
+ revise.type = 'button';
2053
+ revise.onclick = () => { ui.editing = ui.editing === reviseKey ? null : reviseKey; ui.notice = null; renderDrawer(); };
2054
+ s.append(revise);
2055
+ if (ui.editing === reviseKey) s.append(newRevisionForm(item));
2056
+ }
1986
2057
  body.append(s);
1987
2058
  }
1988
2059
 
package/dist/index.cjs CHANGED
@@ -12341,7 +12341,19 @@ var NewAssetSchema = import_zod7.z.object({
12341
12341
  size: import_zod7.z.number().int().optional(),
12342
12342
  tags: import_zod7.z.array(import_zod7.z.string()).optional(),
12343
12343
  /** Restrict to these styles; empty or omitted means every style. */
12344
- styles: import_zod7.z.array(import_zod7.z.string().min(1)).optional()
12344
+ styles: import_zod7.z.array(import_zod7.z.string().min(1)).optional(),
12345
+ /**
12346
+ * A controlled regeneration of another asset in the same manifest,
12347
+ * instead of a fresh generation. `inpaint` needs a mask upload the
12348
+ * gallery does not offer yet, so only `image-to-image` can be created
12349
+ * here; hand-edit the manifest for the other modes.
12350
+ */
12351
+ revision: import_zod7.z.object({
12352
+ mode: import_zod7.z.literal("image-to-image"),
12353
+ /** Asset id of the parent this revises; must already exist. */
12354
+ from: import_zod7.z.string().min(1),
12355
+ strength: import_zod7.z.number().min(0).max(1).optional()
12356
+ }).strict().optional()
12345
12357
  }).strict();
12346
12358
  var CANDIDATE_OPTION = {
12347
12359
  retrodiffusion: "numImages",
@@ -12507,6 +12519,9 @@ function applyEdit(raw, edit) {
12507
12519
  }
12508
12520
  asset2.styles = styles;
12509
12521
  }
12522
+ if (edit.asset.revision && !Object.hasOwn(raw.assets, edit.asset.revision.from)) {
12523
+ throw new ManifestEditError(`revision parent "${edit.asset.revision.from}" is not declared by the manifest`);
12524
+ }
12510
12525
  raw.assets[edit.assetId] = asset2;
12511
12526
  return;
12512
12527
  }