pixelkiln 0.58.0 → 0.59.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/dist/cli.js CHANGED
@@ -703,6 +703,11 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
703
703
  // A mirror's bytes come from its source's recorded outputs; the plan
704
704
  // compares those directly, so only the choice of source is identity.
705
705
  mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
706
+ // The whole group's ordered descriptions, not just this asset's own —
707
+ // any sibling's prompt changing, or a member being added or removed,
708
+ // must mark every member stale together, since they all ride on one
709
+ // submitted job with no way to amend it after the fact.
710
+ batch: spec.batch ? { role: spec.batch.role, itemDescriptions: spec.batch.itemDescriptions, index: spec.batch.index } : void 0,
706
711
  revision: spec.revision ? {
707
712
  mode: spec.revision.mode,
708
713
  from: spec.revision.sourceAssetId,
@@ -857,6 +862,12 @@ var CharacterProportionsSchema = z.union([
857
862
  }).strict()
858
863
  ]);
859
864
  var CharacterAnimationModeSchema = z.enum(["template", "v3", "pro"]);
865
+ var AssetBatchSchema = z.object({
866
+ /** The batch leader: another `1dir` asset in the same style. */
867
+ of: z.string().min(1),
868
+ /** 1-based slot in the batch (the leader is implicitly slot 0). Unique and contiguous among siblings. */
869
+ index: z.number().int().min(1)
870
+ }).strict();
860
871
  var CharacterStateSchema = z.object({
861
872
  /** The base character, or another state, in the same style. */
862
873
  of: z.string().min(1),
@@ -1430,6 +1441,12 @@ var AssetSchema = z.object({
1430
1441
  * `prompt` instead.
1431
1442
  */
1432
1443
  animation: CharacterAnimationSchema.optional(),
1444
+ /**
1445
+ * `1dir` styles only: this asset rides along on another `1dir` asset's
1446
+ * batch submission instead of generating on its own. See
1447
+ * `AssetBatchSchema`.
1448
+ */
1449
+ batch: AssetBatchSchema.optional(),
1433
1450
  /** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
1434
1451
  proportions: CharacterProportionsSchema.optional(),
1435
1452
  /**
@@ -1506,7 +1523,7 @@ var AssetSchema = z.object({
1506
1523
  path: ["revision"]
1507
1524
  });
1508
1525
  }
1509
- const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror"].filter(Boolean);
1526
+ const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror", asset.batch && "batch"].filter(Boolean);
1510
1527
  if (shapes.length > 1) {
1511
1528
  context.addIssue({
1512
1529
  code: z.ZodIssueCode.custom,
@@ -1657,6 +1674,12 @@ var LockEntrySchema = z.object({
1657
1674
  sourceAssetId: z.string().min(1),
1658
1675
  sourceSha256: z.string().regex(/^[0-9a-f]{64}$/)
1659
1676
  }).strict().nullable().default(null),
1677
+ /** For a `1dir` batch member: which leader and slot it rode along on. */
1678
+ batch: z.object({
1679
+ role: z.enum(["leader", "member"]),
1680
+ leaderAssetId: z.string().min(1).optional(),
1681
+ index: z.number().int().min(1).optional()
1682
+ }).strict().nullable().default(null),
1660
1683
  /**
1661
1684
  * Output hashes owned by the previous generation while its replacement is
1662
1685
  * pending. They authorize replacing only unchanged PixelKiln-owned files.
@@ -6219,14 +6242,15 @@ var PixelLabClient = class {
6219
6242
  /**
6220
6243
  * `/reduce-colors`, PixelLab's "Cleanup" tier: quantize one image onto a
6221
6244
  * smaller palette, synchronously — no `background_job_id`, the result
6222
- * comes back in this same response, like `createImagePixflux`. Verified
6223
- * against the live OpenAPI schema, not exercised against a live account:
6224
- * the schema's own response example is `usage: {type: "usd", usd: 0.02}`,
6225
- * which — going by this codebase's own repeated experience with PixelLab's
6226
- * documented-vs-billed cost mismatches (`isometricTile`, `objectPro`) —
6227
- * should not be trusted over a real call. `numColors` and `paletteImage`
6228
- * are mutually exclusive upstream; the manifest schema already enforces
6229
- * that before this is ever called.
6245
+ * comes back in this same response, like `createImagePixflux`. The
6246
+ * schema's own response example is dollar-denominated (`usage: {type:
6247
+ * "usd", usd: 0.02}`), which — matching this codebase's repeated
6248
+ * experience with PixelLab's documented-vs-billed cost mismatches
6249
+ * (`isometricTile`, `objectPro`) — did not hold: confirmed live against a
6250
+ * Tier 2 account at exactly 0.1 generations for a 32x32 source
6251
+ * (docs/REVISIONS.md). `numColors` and `paletteImage` are mutually
6252
+ * exclusive upstream; the manifest schema already enforces that before
6253
+ * this is ever called.
6230
6254
  */
6231
6255
  async reduceColors(args) {
6232
6256
  const body = { images: [args.image] };
@@ -6249,8 +6273,9 @@ var PixelLabClient = class {
6249
6273
  /**
6250
6274
  * `/correct-pixelart`, PixelLab's "Cleanup" tier: sharpen edges and drop
6251
6275
  * stray pixels without resizing, synchronously — same shape as
6252
- * `reduceColors` above, no background job. Cost is likewise unverified
6253
- * against a live account (schema example: `usage: {type: "usd", usd: 0.02}`).
6276
+ * `reduceColors` above, no background job. Cost is likewise confirmed
6277
+ * live at 0.1 generations for a 32x32 source, not the schema's own
6278
+ * dollar-denominated example (`usage: {type: "usd", usd: 0.02}`).
6254
6279
  */
6255
6280
  async correctPixelart(args) {
6256
6281
  const body = { images: [args.image] };
@@ -6518,6 +6543,9 @@ var PixelLabProvider = class _PixelLabProvider {
6518
6543
  return dir;
6519
6544
  }
6520
6545
  estimate(spec) {
6546
+ if (spec.batch?.role === "member") {
6547
+ return { unit: "generations", amount: 0, candidates: 1 };
6548
+ }
6521
6549
  if (spec.revision?.mode === "reduce-colors" || spec.revision?.mode === "correct-pixelart") {
6522
6550
  return { unit: "generations", amount: 0.1, candidates: 1 };
6523
6551
  }
@@ -7316,11 +7344,21 @@ var PixelLabProvider = class _PixelLabProvider {
7316
7344
  return { jobId: res2.background_job_id };
7317
7345
  }
7318
7346
  if (spec.generator === "1dir") {
7347
+ if (spec.batch?.role === "member") {
7348
+ throw new Error(`${spec.styleId}/${spec.assetId}: a batch member cannot submit on its own`);
7349
+ }
7319
7350
  const res2 = await this.client.create1Direction({
7320
7351
  description: spec.prompt,
7321
7352
  size: spec.size,
7322
7353
  view: spec.view === "sidescroller" ? "sidescroller" : "top-down",
7323
- styleImages
7354
+ styleImages,
7355
+ // The leader's own subject is item_descriptions[0], so `description`
7356
+ // matches it exactly. Confirmed live: item_descriptions[0] owns
7357
+ // candidate slot 0 (a 3-item batch returned exactly the declared
7358
+ // items at frames 0/1/2, in order; docs/GENERATORS.md#1dir), so
7359
+ // sending the same text as `description` too is redundant but
7360
+ // harmless, not a hedge against ambiguity.
7361
+ itemDescriptions: spec.batch?.role === "leader" ? spec.batch.itemDescriptions : void 0
7324
7362
  });
7325
7363
  return { jobId: res2.object_id, metadata: { backgroundJobId: res2.background_job_id } };
7326
7364
  }
@@ -9339,6 +9377,14 @@ async function resolveSpecs(loaded, filter) {
9339
9377
  };
9340
9378
  styleSpecs.set(assetId, resolved);
9341
9379
  }
9380
+ const batchMembersByLeader = /* @__PURE__ */ new Map();
9381
+ for (const id of styleSpecs.keys()) {
9382
+ const batch = manifest.assets[id].batch;
9383
+ if (!batch) continue;
9384
+ const members = batchMembersByLeader.get(batch.of) ?? [];
9385
+ members.push({ id, index: batch.index });
9386
+ batchMembersByLeader.set(batch.of, members);
9387
+ }
9342
9388
  const finalized = /* @__PURE__ */ new Set();
9343
9389
  const finalize = async (assetId) => {
9344
9390
  const resolved = styleSpecs.get(assetId);
@@ -9497,6 +9543,56 @@ async function resolveSpecs(loaded, filter) {
9497
9543
  ...asset.revision.enhancePrompt == null ? {} : { enhancePrompt: asset.revision.enhancePrompt }
9498
9544
  };
9499
9545
  }
9546
+ if (asset.batch) {
9547
+ if (asset.batch.of === assetId) throw new Error(`assets.${assetId}: a batch cannot name itself as its own leader`);
9548
+ const leaderSpec = await finalize(asset.batch.of);
9549
+ if (!leaderSpec.batch || leaderSpec.batch.role !== "leader") {
9550
+ throw new Error(`assets.${assetId}: ${asset.batch.of} is not a 1dir batch leader`);
9551
+ }
9552
+ if (resolved.size !== leaderSpec.size) {
9553
+ throw new Error(
9554
+ `assets.${assetId}: batch members share the leader's canvas size; ${asset.batch.of} is ${leaderSpec.size}px, this is ${resolved.size}px`
9555
+ );
9556
+ }
9557
+ resolved.batch = {
9558
+ role: "member",
9559
+ itemDescriptions: leaderSpec.batch.itemDescriptions,
9560
+ index: asset.batch.index,
9561
+ leaderAssetId: asset.batch.of,
9562
+ leaderSpec
9563
+ };
9564
+ } else {
9565
+ const members = batchMembersByLeader.get(assetId);
9566
+ if (members) {
9567
+ if (style.generator !== "1dir") {
9568
+ throw new Error(`assets.${assetId}: only a 1dir asset can lead a batch`);
9569
+ }
9570
+ const sorted = [...members].sort((a, b) => a.index - b.index);
9571
+ const seen = /* @__PURE__ */ new Set();
9572
+ for (const { index } of sorted) {
9573
+ if (seen.has(index)) throw new Error(`assets.${assetId}: two batch members both claim index ${index}`);
9574
+ seen.add(index);
9575
+ }
9576
+ const expectedIndices = sorted.map((_, i) => i + 1).join(",");
9577
+ if (sorted.map((m) => m.index).join(",") !== expectedIndices) {
9578
+ throw new Error(
9579
+ `assets.${assetId}: batch member indices must be 1..${sorted.length} with no gaps; got ${sorted.map((m) => m.index).join(",")}`
9580
+ );
9581
+ }
9582
+ const total = 1 + sorted.length;
9583
+ const limit = candidateCount(resolved.size);
9584
+ if (total > limit) {
9585
+ throw new Error(
9586
+ `assets.${assetId}: a ${resolved.size}px batch holds at most ${limit} items (1 leader + ${limit - 1} members); this one declares ${total}`
9587
+ );
9588
+ }
9589
+ resolved.batch = {
9590
+ role: "leader",
9591
+ itemDescriptions: [resolved.prompt, ...sorted.map(({ id }) => styleSpecs.get(id).prompt)],
9592
+ memberAssetIds: sorted.map((m) => m.id)
9593
+ };
9594
+ }
9595
+ }
9500
9596
  resolved.specHash = specHash(
9501
9597
  resolved,
9502
9598
  styleImageHashes,
@@ -13764,6 +13860,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
13764
13860
  for (const styleId of new Set(items.map((i) => i.spec.styleId))) {
13765
13861
  styleImages.set(styleId, await resolveStyleImages(loaded, styleId));
13766
13862
  }
13863
+ const byKey = new Map(items.map((item) => [item.key, item]));
13767
13864
  let submitted = 0;
13768
13865
  let failed = 0;
13769
13866
  let spent = 0;
@@ -13793,8 +13890,62 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
13793
13890
  await pruneInFlight();
13794
13891
  }
13795
13892
  }
13893
+ function writeBatchMembers(leaderSpec, leaderKey, error) {
13894
+ if (leaderSpec.batch?.role !== "leader") return;
13895
+ const leaderEntry = lock.entries[leaderKey];
13896
+ for (const memberAssetId of leaderSpec.batch.memberAssetIds ?? []) {
13897
+ const memberKey = lockKey(leaderSpec.styleId, memberAssetId);
13898
+ const memberItem = byKey.get(memberKey);
13899
+ if (!memberItem) {
13900
+ throw new Error(
13901
+ `${leaderKey}: batch member "${memberAssetId}" must be submitted in the same run as its leader; run without --only, or include every sibling`
13902
+ );
13903
+ }
13904
+ const memberSpec = memberItem.spec;
13905
+ const memberEstimate = estimates.get(memberKey);
13906
+ const previousMemberEntry = lock.entries[memberKey];
13907
+ const memberSubmittedAt = leaderEntry.submittedAt ?? (/* @__PURE__ */ new Date()).toISOString();
13908
+ upsert(lock, memberKey, {
13909
+ styleId: memberSpec.styleId,
13910
+ assetId: memberSpec.assetId,
13911
+ specHash: memberSpec.specHash,
13912
+ generator: memberSpec.generator,
13913
+ prompt: memberSpec.prompt,
13914
+ width: memberSpec.width,
13915
+ height: memberSpec.height,
13916
+ batch: { role: "member", leaderAssetId: leaderSpec.assetId, index: memberSpec.batch.index },
13917
+ status: error ? "failed" : "processing",
13918
+ error,
13919
+ jobId: error ? null : leaderEntry.jobId,
13920
+ submissionComplete: error ? void 0 : true,
13921
+ reviewObjectId: error ? null : leaderEntry.reviewObjectId,
13922
+ objectId: null,
13923
+ candidateIndex: null,
13924
+ outputs: [],
13925
+ supersededOutputs: previousMemberEntry?.outputs.length ? previousMemberEntry.outputs : previousMemberEntry?.supersededOutputs ?? [],
13926
+ providerMetadata: leaderEntry.providerMetadata,
13927
+ sourceUrl: null,
13928
+ sourceUrls: [],
13929
+ submittedAt: memberSubmittedAt,
13930
+ history: historyAfterReplacing(previousMemberEntry, historyLimit(loaded.manifest), memberSubmittedAt),
13931
+ cost: memberEstimate.amount,
13932
+ costUnit: memberEstimate.unit,
13933
+ provider: provider.id,
13934
+ downloadedAt: null
13935
+ });
13936
+ if (error) failed++;
13937
+ else {
13938
+ submitted++;
13939
+ spent += memberEstimate.amount;
13940
+ log2(` ${memberKey} \u2192 ${leaderEntry.jobId} (rides on ${leaderKey}'s batch)`);
13941
+ }
13942
+ }
13943
+ }
13796
13944
  for (const { spec, key } of items) {
13797
13945
  const estimate = estimates.get(key);
13946
+ if (spec.batch?.role === "member") {
13947
+ continue;
13948
+ }
13798
13949
  if (spec.mirror) {
13799
13950
  try {
13800
13951
  await requireRevisionReady(spec, lock);
@@ -13827,6 +13978,14 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
13827
13978
  const since = Date.now() - lastSubmitAt;
13828
13979
  if (since < spacing) await sleep2(spacing - since);
13829
13980
  await requireRevisionReady(spec, lock);
13981
+ if (spec.batch?.role === "leader") {
13982
+ const missing = (spec.batch.memberAssetIds ?? []).filter((id) => !byKey.has(lockKey(spec.styleId, id)));
13983
+ if (missing.length) {
13984
+ throw new Error(
13985
+ `${key}: batch member(s) ${missing.join(", ")} must be submitted in the same run as this leader; run without --only, or include every sibling`
13986
+ );
13987
+ }
13988
+ }
13830
13989
  const previousEntry = lock.entries[key];
13831
13990
  const resumesCheckpoint = Boolean(
13832
13991
  previousEntry?.specHash === spec.specHash && previousEntry.provider === provider.id && previousEntry.generator === spec.generator && previousEntry.submissionComplete === false && previousEntry.jobId
@@ -13932,6 +14091,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
13932
14091
  inFlight.set(jobId, spec);
13933
14092
  submitted++;
13934
14093
  spent += estimate.amount;
14094
+ writeBatchMembers(spec, key, null);
13935
14095
  log2(
13936
14096
  ` ${key} \u2192 ${jobId} (${spec.width}x${spec.height}` + (estimate.candidates > 1 ? `, ${estimate.candidates} ${spec.tileFeature ? "outputs" : "candidates"}` : "") + `, ${estimate.amount})`
13937
14097
  );
@@ -13943,6 +14103,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
13943
14103
  inFlight.set(entry.jobId, spec);
13944
14104
  submitted++;
13945
14105
  spent += estimate.amount;
14106
+ writeBatchMembers(spec, key, null);
13946
14107
  log2(` ${key} \u2192 ${entry.jobId} (recovered from completed checkpoint)`);
13947
14108
  } else {
13948
14109
  failed++;
@@ -13951,6 +14112,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
13951
14112
  error: message6,
13952
14113
  cost: entry.jobId ? estimate.amount : 0
13953
14114
  });
14115
+ writeBatchMembers(spec, key, message6);
13954
14116
  log2(` FAILED ${key}: ${message6}`);
13955
14117
  }
13956
14118
  }
@@ -14151,6 +14313,9 @@ function renderSheet(groups, options = {}) {
14151
14313
  .cand.active { border-color:var(--dim); box-shadow:0 0 0 2px color-mix(in srgb, var(--dim) 20%, transparent); }
14152
14314
  .cand.sel { border-color:var(--ok); box-shadow:0 0 0 3px color-mix(in srgb, var(--ok) 22%, transparent); }
14153
14315
  .cand.set-frame { cursor:pointer; }
14316
+ .cand.recommended:not(.sel) { border-color:var(--accent); }
14317
+ .cand.recommended .badge { position:absolute; top:-1px; left:-1px; font-size:9.5px; font-weight:650;
14318
+ text-transform:uppercase; letter-spacing:.03em; color:var(--bg); background:var(--accent); padding:1px 5px; }
14154
14319
  .cand img { image-rendering:pixelated; display:block;
14155
14320
  background-image:
14156
14321
  linear-gradient(45deg,#0000 25%,#7f7f7f22 25%,#7f7f7f22 75%,#0000 75%),
@@ -14308,14 +14473,17 @@ GROUPS.forEach((g, gi) => {
14308
14473
  }).observe(loop);
14309
14474
  }
14310
14475
  }
14476
+ const recommended = g.recommendedIndex ?? 0;
14311
14477
  g.frameUrls.forEach((url, i) => {
14312
14478
  const c = document.createElement('button');
14313
14479
  c.type = 'button';
14314
- c.className = 'cand' + (g.mode === 'frame-set' ? ' set-frame' : '');
14480
+ c.className = 'cand' + (g.mode === 'frame-set' ? ' set-frame' : '') +
14481
+ (g.recommendedIndex != null && i === recommended ? ' recommended' : '');
14315
14482
  c.tabIndex = -1;
14316
- c.setAttribute('aria-label', g.mode === 'frame-set'
14483
+ c.setAttribute('aria-label', (g.mode === 'frame-set'
14317
14484
  ? 'Accept ordered frame set from frame ' + (i + 1)
14318
- : 'Choose candidate ' + (i + 1) + ' of ' + g.frameUrls.length);
14485
+ : 'Choose candidate ' + (i + 1) + ' of ' + g.frameUrls.length) +
14486
+ (g.recommendedIndex != null && i === recommended ? ' (this asset\u2019s declared slot)' : ''));
14319
14487
 
14320
14488
  const preview = document.createElement('img');
14321
14489
  preview.className = 'preview';
@@ -14328,6 +14496,12 @@ GROUPS.forEach((g, gi) => {
14328
14496
  index.textContent = (g.frameLabels?.[i] || String(i + 1)) + ' \xB7 ' +
14329
14497
  g.width + '\xD7' + g.height + ' \xB7 ' + scaleLabel;
14330
14498
  c.append(preview, index);
14499
+ if (g.recommendedIndex != null && i === recommended) {
14500
+ const badge = document.createElement('span');
14501
+ badge.className = 'badge';
14502
+ badge.textContent = 'declared';
14503
+ c.append(badge);
14504
+ }
14331
14505
  if (displayScale > 1 && largestSide <= 96) {
14332
14506
  const actual = document.createElement('img');
14333
14507
  actual.className = 'actual';
@@ -14352,6 +14526,7 @@ GROUPS.forEach((g, gi) => {
14352
14526
  });
14353
14527
  frames.dataset.active = '0';
14354
14528
  frames.querySelector('.cand')?.classList.add('active');
14529
+ if (recommended > 0) activate(recommended, frames);
14355
14530
  root.appendChild(el);
14356
14531
  });
14357
14532
 
@@ -14526,7 +14701,11 @@ async function prepareReview(provider, lock, opts = {}) {
14526
14701
  height: spec.revision.sourceHeight
14527
14702
  }
14528
14703
  } : {},
14529
- ...current && currentRoute ? { current: { url: currentRoute, width: current.width, height: current.height } } : {}
14704
+ ...current && currentRoute ? { current: { url: currentRoute, width: current.width, height: current.height } } : {},
14705
+ // A batch member's declared slot in the shared candidate set — the
14706
+ // leader's own slot 0 is already the sheet's ordinary default, so
14707
+ // only a member (whose slot is 1+) needs to say so explicitly.
14708
+ ...entry.batch?.index != null ? { recommendedIndex: entry.batch.index } : {}
14530
14709
  });
14531
14710
  } catch (err) {
14532
14711
  log2(` could not load candidates for ${key}: ${err instanceof Error ? err.message : String(err)}`);