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/index.js CHANGED
@@ -123,6 +123,12 @@ var CharacterProportionsSchema = z.union([
123
123
  }).strict()
124
124
  ]);
125
125
  var CharacterAnimationModeSchema = z.enum(["template", "v3", "pro"]);
126
+ var AssetBatchSchema = z.object({
127
+ /** The batch leader: another `1dir` asset in the same style. */
128
+ of: z.string().min(1),
129
+ /** 1-based slot in the batch (the leader is implicitly slot 0). Unique and contiguous among siblings. */
130
+ index: z.number().int().min(1)
131
+ }).strict();
126
132
  var CharacterStateSchema = z.object({
127
133
  /** The base character, or another state, in the same style. */
128
134
  of: z.string().min(1),
@@ -696,6 +702,12 @@ var AssetSchema = z.object({
696
702
  * `prompt` instead.
697
703
  */
698
704
  animation: CharacterAnimationSchema.optional(),
705
+ /**
706
+ * `1dir` styles only: this asset rides along on another `1dir` asset's
707
+ * batch submission instead of generating on its own. See
708
+ * `AssetBatchSchema`.
709
+ */
710
+ batch: AssetBatchSchema.optional(),
699
711
  /** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
700
712
  proportions: CharacterProportionsSchema.optional(),
701
713
  /**
@@ -772,7 +784,7 @@ var AssetSchema = z.object({
772
784
  path: ["revision"]
773
785
  });
774
786
  }
775
- const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror"].filter(Boolean);
787
+ const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror", asset.batch && "batch"].filter(Boolean);
776
788
  if (shapes.length > 1) {
777
789
  context.addIssue({
778
790
  code: z.ZodIssueCode.custom,
@@ -923,6 +935,12 @@ var LockEntrySchema = z.object({
923
935
  sourceAssetId: z.string().min(1),
924
936
  sourceSha256: z.string().regex(/^[0-9a-f]{64}$/)
925
937
  }).strict().nullable().default(null),
938
+ /** For a `1dir` batch member: which leader and slot it rode along on. */
939
+ batch: z.object({
940
+ role: z.enum(["leader", "member"]),
941
+ leaderAssetId: z.string().min(1).optional(),
942
+ index: z.number().int().min(1).optional()
943
+ }).strict().nullable().default(null),
926
944
  /**
927
945
  * Output hashes owned by the previous generation while its replacement is
928
946
  * pending. They authorize replacing only unchanged PixelKiln-owned files.
@@ -1973,14 +1991,15 @@ var PixelLabClient = class {
1973
1991
  /**
1974
1992
  * `/reduce-colors`, PixelLab's "Cleanup" tier: quantize one image onto a
1975
1993
  * smaller palette, synchronously — no `background_job_id`, the result
1976
- * comes back in this same response, like `createImagePixflux`. Verified
1977
- * against the live OpenAPI schema, not exercised against a live account:
1978
- * the schema's own response example is `usage: {type: "usd", usd: 0.02}`,
1979
- * which — going by this codebase's own repeated experience with PixelLab's
1980
- * documented-vs-billed cost mismatches (`isometricTile`, `objectPro`) —
1981
- * should not be trusted over a real call. `numColors` and `paletteImage`
1982
- * are mutually exclusive upstream; the manifest schema already enforces
1983
- * that before this is ever called.
1994
+ * comes back in this same response, like `createImagePixflux`. The
1995
+ * schema's own response example is dollar-denominated (`usage: {type:
1996
+ * "usd", usd: 0.02}`), which — matching this codebase's repeated
1997
+ * experience with PixelLab's documented-vs-billed cost mismatches
1998
+ * (`isometricTile`, `objectPro`) — did not hold: confirmed live against a
1999
+ * Tier 2 account at exactly 0.1 generations for a 32x32 source
2000
+ * (docs/REVISIONS.md). `numColors` and `paletteImage` are mutually
2001
+ * exclusive upstream; the manifest schema already enforces that before
2002
+ * this is ever called.
1984
2003
  */
1985
2004
  async reduceColors(args) {
1986
2005
  const body = { images: [args.image] };
@@ -2003,8 +2022,9 @@ var PixelLabClient = class {
2003
2022
  /**
2004
2023
  * `/correct-pixelart`, PixelLab's "Cleanup" tier: sharpen edges and drop
2005
2024
  * stray pixels without resizing, synchronously — same shape as
2006
- * `reduceColors` above, no background job. Cost is likewise unverified
2007
- * against a live account (schema example: `usage: {type: "usd", usd: 0.02}`).
2025
+ * `reduceColors` above, no background job. Cost is likewise confirmed
2026
+ * live at 0.1 generations for a 32x32 source, not the schema's own
2027
+ * dollar-denominated example (`usage: {type: "usd", usd: 0.02}`).
2008
2028
  */
2009
2029
  async correctPixelart(args) {
2010
2030
  const body = { images: [args.image] };
@@ -2760,6 +2780,11 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
2760
2780
  // A mirror's bytes come from its source's recorded outputs; the plan
2761
2781
  // compares those directly, so only the choice of source is identity.
2762
2782
  mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
2783
+ // The whole group's ordered descriptions, not just this asset's own —
2784
+ // any sibling's prompt changing, or a member being added or removed,
2785
+ // must mark every member stale together, since they all ride on one
2786
+ // submitted job with no way to amend it after the fact.
2787
+ batch: spec.batch ? { role: spec.batch.role, itemDescriptions: spec.batch.itemDescriptions, index: spec.batch.index } : void 0,
2763
2788
  revision: spec.revision ? {
2764
2789
  mode: spec.revision.mode,
2765
2790
  from: spec.revision.sourceAssetId,
@@ -2941,6 +2966,9 @@ var PixelLabProvider = class _PixelLabProvider {
2941
2966
  return dir;
2942
2967
  }
2943
2968
  estimate(spec) {
2969
+ if (spec.batch?.role === "member") {
2970
+ return { unit: "generations", amount: 0, candidates: 1 };
2971
+ }
2944
2972
  if (spec.revision?.mode === "reduce-colors" || spec.revision?.mode === "correct-pixelart") {
2945
2973
  return { unit: "generations", amount: 0.1, candidates: 1 };
2946
2974
  }
@@ -3739,11 +3767,21 @@ var PixelLabProvider = class _PixelLabProvider {
3739
3767
  return { jobId: res2.background_job_id };
3740
3768
  }
3741
3769
  if (spec.generator === "1dir") {
3770
+ if (spec.batch?.role === "member") {
3771
+ throw new Error(`${spec.styleId}/${spec.assetId}: a batch member cannot submit on its own`);
3772
+ }
3742
3773
  const res2 = await this.client.create1Direction({
3743
3774
  description: spec.prompt,
3744
3775
  size: spec.size,
3745
3776
  view: spec.view === "sidescroller" ? "sidescroller" : "top-down",
3746
- styleImages
3777
+ styleImages,
3778
+ // The leader's own subject is item_descriptions[0], so `description`
3779
+ // matches it exactly. Confirmed live: item_descriptions[0] owns
3780
+ // candidate slot 0 (a 3-item batch returned exactly the declared
3781
+ // items at frames 0/1/2, in order; docs/GENERATORS.md#1dir), so
3782
+ // sending the same text as `description` too is redundant but
3783
+ // harmless, not a hedge against ambiguity.
3784
+ itemDescriptions: spec.batch?.role === "leader" ? spec.batch.itemDescriptions : void 0
3747
3785
  });
3748
3786
  return { jobId: res2.object_id, metadata: { backgroundJobId: res2.background_job_id } };
3749
3787
  }
@@ -7153,6 +7191,14 @@ async function resolveSpecs(loaded, filter) {
7153
7191
  };
7154
7192
  styleSpecs.set(assetId, resolved);
7155
7193
  }
7194
+ const batchMembersByLeader = /* @__PURE__ */ new Map();
7195
+ for (const id of styleSpecs.keys()) {
7196
+ const batch = manifest.assets[id].batch;
7197
+ if (!batch) continue;
7198
+ const members = batchMembersByLeader.get(batch.of) ?? [];
7199
+ members.push({ id, index: batch.index });
7200
+ batchMembersByLeader.set(batch.of, members);
7201
+ }
7156
7202
  const finalized = /* @__PURE__ */ new Set();
7157
7203
  const finalize = async (assetId) => {
7158
7204
  const resolved = styleSpecs.get(assetId);
@@ -7311,6 +7357,56 @@ async function resolveSpecs(loaded, filter) {
7311
7357
  ...asset.revision.enhancePrompt == null ? {} : { enhancePrompt: asset.revision.enhancePrompt }
7312
7358
  };
7313
7359
  }
7360
+ if (asset.batch) {
7361
+ if (asset.batch.of === assetId) throw new Error(`assets.${assetId}: a batch cannot name itself as its own leader`);
7362
+ const leaderSpec = await finalize(asset.batch.of);
7363
+ if (!leaderSpec.batch || leaderSpec.batch.role !== "leader") {
7364
+ throw new Error(`assets.${assetId}: ${asset.batch.of} is not a 1dir batch leader`);
7365
+ }
7366
+ if (resolved.size !== leaderSpec.size) {
7367
+ throw new Error(
7368
+ `assets.${assetId}: batch members share the leader's canvas size; ${asset.batch.of} is ${leaderSpec.size}px, this is ${resolved.size}px`
7369
+ );
7370
+ }
7371
+ resolved.batch = {
7372
+ role: "member",
7373
+ itemDescriptions: leaderSpec.batch.itemDescriptions,
7374
+ index: asset.batch.index,
7375
+ leaderAssetId: asset.batch.of,
7376
+ leaderSpec
7377
+ };
7378
+ } else {
7379
+ const members = batchMembersByLeader.get(assetId);
7380
+ if (members) {
7381
+ if (style.generator !== "1dir") {
7382
+ throw new Error(`assets.${assetId}: only a 1dir asset can lead a batch`);
7383
+ }
7384
+ const sorted = [...members].sort((a, b) => a.index - b.index);
7385
+ const seen = /* @__PURE__ */ new Set();
7386
+ for (const { index } of sorted) {
7387
+ if (seen.has(index)) throw new Error(`assets.${assetId}: two batch members both claim index ${index}`);
7388
+ seen.add(index);
7389
+ }
7390
+ const expectedIndices = sorted.map((_, i) => i + 1).join(",");
7391
+ if (sorted.map((m) => m.index).join(",") !== expectedIndices) {
7392
+ throw new Error(
7393
+ `assets.${assetId}: batch member indices must be 1..${sorted.length} with no gaps; got ${sorted.map((m) => m.index).join(",")}`
7394
+ );
7395
+ }
7396
+ const total = 1 + sorted.length;
7397
+ const limit = candidateCount(resolved.size);
7398
+ if (total > limit) {
7399
+ throw new Error(
7400
+ `assets.${assetId}: a ${resolved.size}px batch holds at most ${limit} items (1 leader + ${limit - 1} members); this one declares ${total}`
7401
+ );
7402
+ }
7403
+ resolved.batch = {
7404
+ role: "leader",
7405
+ itemDescriptions: [resolved.prompt, ...sorted.map(({ id }) => styleSpecs.get(id).prompt)],
7406
+ memberAssetIds: sorted.map((m) => m.id)
7407
+ };
7408
+ }
7409
+ }
7314
7410
  resolved.specHash = specHash(
7315
7411
  resolved,
7316
7412
  styleImageHashes,
@@ -11696,6 +11792,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
11696
11792
  for (const styleId of new Set(items.map((i) => i.spec.styleId))) {
11697
11793
  styleImages.set(styleId, await resolveStyleImages(loaded, styleId));
11698
11794
  }
11795
+ const byKey = new Map(items.map((item) => [item.key, item]));
11699
11796
  let submitted = 0;
11700
11797
  let failed = 0;
11701
11798
  let spent = 0;
@@ -11725,8 +11822,62 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
11725
11822
  await pruneInFlight();
11726
11823
  }
11727
11824
  }
11825
+ function writeBatchMembers(leaderSpec, leaderKey, error) {
11826
+ if (leaderSpec.batch?.role !== "leader") return;
11827
+ const leaderEntry = lock.entries[leaderKey];
11828
+ for (const memberAssetId of leaderSpec.batch.memberAssetIds ?? []) {
11829
+ const memberKey = lockKey(leaderSpec.styleId, memberAssetId);
11830
+ const memberItem = byKey.get(memberKey);
11831
+ if (!memberItem) {
11832
+ throw new Error(
11833
+ `${leaderKey}: batch member "${memberAssetId}" must be submitted in the same run as its leader; run without --only, or include every sibling`
11834
+ );
11835
+ }
11836
+ const memberSpec = memberItem.spec;
11837
+ const memberEstimate = estimates.get(memberKey);
11838
+ const previousMemberEntry = lock.entries[memberKey];
11839
+ const memberSubmittedAt = leaderEntry.submittedAt ?? (/* @__PURE__ */ new Date()).toISOString();
11840
+ upsert(lock, memberKey, {
11841
+ styleId: memberSpec.styleId,
11842
+ assetId: memberSpec.assetId,
11843
+ specHash: memberSpec.specHash,
11844
+ generator: memberSpec.generator,
11845
+ prompt: memberSpec.prompt,
11846
+ width: memberSpec.width,
11847
+ height: memberSpec.height,
11848
+ batch: { role: "member", leaderAssetId: leaderSpec.assetId, index: memberSpec.batch.index },
11849
+ status: error ? "failed" : "processing",
11850
+ error,
11851
+ jobId: error ? null : leaderEntry.jobId,
11852
+ submissionComplete: error ? void 0 : true,
11853
+ reviewObjectId: error ? null : leaderEntry.reviewObjectId,
11854
+ objectId: null,
11855
+ candidateIndex: null,
11856
+ outputs: [],
11857
+ supersededOutputs: previousMemberEntry?.outputs.length ? previousMemberEntry.outputs : previousMemberEntry?.supersededOutputs ?? [],
11858
+ providerMetadata: leaderEntry.providerMetadata,
11859
+ sourceUrl: null,
11860
+ sourceUrls: [],
11861
+ submittedAt: memberSubmittedAt,
11862
+ history: historyAfterReplacing(previousMemberEntry, historyLimit(loaded.manifest), memberSubmittedAt),
11863
+ cost: memberEstimate.amount,
11864
+ costUnit: memberEstimate.unit,
11865
+ provider: provider.id,
11866
+ downloadedAt: null
11867
+ });
11868
+ if (error) failed++;
11869
+ else {
11870
+ submitted++;
11871
+ spent += memberEstimate.amount;
11872
+ log(` ${memberKey} \u2192 ${leaderEntry.jobId} (rides on ${leaderKey}'s batch)`);
11873
+ }
11874
+ }
11875
+ }
11728
11876
  for (const { spec, key } of items) {
11729
11877
  const estimate = estimates.get(key);
11878
+ if (spec.batch?.role === "member") {
11879
+ continue;
11880
+ }
11730
11881
  if (spec.mirror) {
11731
11882
  try {
11732
11883
  await requireRevisionReady(spec, lock);
@@ -11759,6 +11910,14 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
11759
11910
  const since = Date.now() - lastSubmitAt;
11760
11911
  if (since < spacing) await sleep(spacing - since);
11761
11912
  await requireRevisionReady(spec, lock);
11913
+ if (spec.batch?.role === "leader") {
11914
+ const missing = (spec.batch.memberAssetIds ?? []).filter((id) => !byKey.has(lockKey(spec.styleId, id)));
11915
+ if (missing.length) {
11916
+ throw new Error(
11917
+ `${key}: batch member(s) ${missing.join(", ")} must be submitted in the same run as this leader; run without --only, or include every sibling`
11918
+ );
11919
+ }
11920
+ }
11762
11921
  const previousEntry = lock.entries[key];
11763
11922
  const resumesCheckpoint = Boolean(
11764
11923
  previousEntry?.specHash === spec.specHash && previousEntry.provider === provider.id && previousEntry.generator === spec.generator && previousEntry.submissionComplete === false && previousEntry.jobId
@@ -11864,6 +12023,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
11864
12023
  inFlight.set(jobId, spec);
11865
12024
  submitted++;
11866
12025
  spent += estimate.amount;
12026
+ writeBatchMembers(spec, key, null);
11867
12027
  log(
11868
12028
  ` ${key} \u2192 ${jobId} (${spec.width}x${spec.height}` + (estimate.candidates > 1 ? `, ${estimate.candidates} ${spec.tileFeature ? "outputs" : "candidates"}` : "") + `, ${estimate.amount})`
11869
12029
  );
@@ -11875,6 +12035,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
11875
12035
  inFlight.set(entry.jobId, spec);
11876
12036
  submitted++;
11877
12037
  spent += estimate.amount;
12038
+ writeBatchMembers(spec, key, null);
11878
12039
  log(` ${key} \u2192 ${entry.jobId} (recovered from completed checkpoint)`);
11879
12040
  } else {
11880
12041
  failed++;
@@ -11883,6 +12044,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
11883
12044
  error: message6,
11884
12045
  cost: entry.jobId ? estimate.amount : 0
11885
12046
  });
12047
+ writeBatchMembers(spec, key, message6);
11886
12048
  log(` FAILED ${key}: ${message6}`);
11887
12049
  }
11888
12050
  }
@@ -12474,6 +12636,9 @@ function renderSheet(groups, options = {}) {
12474
12636
  .cand.active { border-color:var(--dim); box-shadow:0 0 0 2px color-mix(in srgb, var(--dim) 20%, transparent); }
12475
12637
  .cand.sel { border-color:var(--ok); box-shadow:0 0 0 3px color-mix(in srgb, var(--ok) 22%, transparent); }
12476
12638
  .cand.set-frame { cursor:pointer; }
12639
+ .cand.recommended:not(.sel) { border-color:var(--accent); }
12640
+ .cand.recommended .badge { position:absolute; top:-1px; left:-1px; font-size:9.5px; font-weight:650;
12641
+ text-transform:uppercase; letter-spacing:.03em; color:var(--bg); background:var(--accent); padding:1px 5px; }
12477
12642
  .cand img { image-rendering:pixelated; display:block;
12478
12643
  background-image:
12479
12644
  linear-gradient(45deg,#0000 25%,#7f7f7f22 25%,#7f7f7f22 75%,#0000 75%),
@@ -12631,14 +12796,17 @@ GROUPS.forEach((g, gi) => {
12631
12796
  }).observe(loop);
12632
12797
  }
12633
12798
  }
12799
+ const recommended = g.recommendedIndex ?? 0;
12634
12800
  g.frameUrls.forEach((url, i) => {
12635
12801
  const c = document.createElement('button');
12636
12802
  c.type = 'button';
12637
- c.className = 'cand' + (g.mode === 'frame-set' ? ' set-frame' : '');
12803
+ c.className = 'cand' + (g.mode === 'frame-set' ? ' set-frame' : '') +
12804
+ (g.recommendedIndex != null && i === recommended ? ' recommended' : '');
12638
12805
  c.tabIndex = -1;
12639
- c.setAttribute('aria-label', g.mode === 'frame-set'
12806
+ c.setAttribute('aria-label', (g.mode === 'frame-set'
12640
12807
  ? 'Accept ordered frame set from frame ' + (i + 1)
12641
- : 'Choose candidate ' + (i + 1) + ' of ' + g.frameUrls.length);
12808
+ : 'Choose candidate ' + (i + 1) + ' of ' + g.frameUrls.length) +
12809
+ (g.recommendedIndex != null && i === recommended ? ' (this asset\u2019s declared slot)' : ''));
12642
12810
 
12643
12811
  const preview = document.createElement('img');
12644
12812
  preview.className = 'preview';
@@ -12651,6 +12819,12 @@ GROUPS.forEach((g, gi) => {
12651
12819
  index.textContent = (g.frameLabels?.[i] || String(i + 1)) + ' \xB7 ' +
12652
12820
  g.width + '\xD7' + g.height + ' \xB7 ' + scaleLabel;
12653
12821
  c.append(preview, index);
12822
+ if (g.recommendedIndex != null && i === recommended) {
12823
+ const badge = document.createElement('span');
12824
+ badge.className = 'badge';
12825
+ badge.textContent = 'declared';
12826
+ c.append(badge);
12827
+ }
12654
12828
  if (displayScale > 1 && largestSide <= 96) {
12655
12829
  const actual = document.createElement('img');
12656
12830
  actual.className = 'actual';
@@ -12675,6 +12849,7 @@ GROUPS.forEach((g, gi) => {
12675
12849
  });
12676
12850
  frames.dataset.active = '0';
12677
12851
  frames.querySelector('.cand')?.classList.add('active');
12852
+ if (recommended > 0) activate(recommended, frames);
12678
12853
  root.appendChild(el);
12679
12854
  });
12680
12855
 
@@ -12930,7 +13105,11 @@ async function prepareReview(provider, lock, opts = {}) {
12930
13105
  height: spec.revision.sourceHeight
12931
13106
  }
12932
13107
  } : {},
12933
- ...current && currentRoute ? { current: { url: currentRoute, width: current.width, height: current.height } } : {}
13108
+ ...current && currentRoute ? { current: { url: currentRoute, width: current.width, height: current.height } } : {},
13109
+ // A batch member's declared slot in the shared candidate set — the
13110
+ // leader's own slot 0 is already the sheet's ordinary default, so
13111
+ // only a member (whose slot is 1+) needs to say so explicitly.
13112
+ ...entry.batch?.index != null ? { recommendedIndex: entry.batch.index } : {}
12934
13113
  });
12935
13114
  } catch (err) {
12936
13115
  log(` could not load candidates for ${key}: ${err instanceof Error ? err.message : String(err)}`);
@@ -15977,6 +16156,7 @@ async function workspaceStatus(ws, dir) {
15977
16156
  };
15978
16157
  }
15979
16158
  export {
16159
+ AssetBatchSchema,
15980
16160
  AssetSchema,
15981
16161
  BudgetError,
15982
16162
  CHARACTER_DIRECTIONS_4,