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.cjs CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ AssetBatchSchema: () => AssetBatchSchema,
33
34
  AssetSchema: () => AssetSchema,
34
35
  BudgetError: () => BudgetError,
35
36
  CHARACTER_DIRECTIONS_4: () => CHARACTER_DIRECTIONS_4,
@@ -427,6 +428,12 @@ var CharacterProportionsSchema = import_zod.z.union([
427
428
  }).strict()
428
429
  ]);
429
430
  var CharacterAnimationModeSchema = import_zod.z.enum(["template", "v3", "pro"]);
431
+ var AssetBatchSchema = import_zod.z.object({
432
+ /** The batch leader: another `1dir` asset in the same style. */
433
+ of: import_zod.z.string().min(1),
434
+ /** 1-based slot in the batch (the leader is implicitly slot 0). Unique and contiguous among siblings. */
435
+ index: import_zod.z.number().int().min(1)
436
+ }).strict();
430
437
  var CharacterStateSchema = import_zod.z.object({
431
438
  /** The base character, or another state, in the same style. */
432
439
  of: import_zod.z.string().min(1),
@@ -1000,6 +1007,12 @@ var AssetSchema = import_zod.z.object({
1000
1007
  * `prompt` instead.
1001
1008
  */
1002
1009
  animation: CharacterAnimationSchema.optional(),
1010
+ /**
1011
+ * `1dir` styles only: this asset rides along on another `1dir` asset's
1012
+ * batch submission instead of generating on its own. See
1013
+ * `AssetBatchSchema`.
1014
+ */
1015
+ batch: AssetBatchSchema.optional(),
1003
1016
  /** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
1004
1017
  proportions: CharacterProportionsSchema.optional(),
1005
1018
  /**
@@ -1076,7 +1089,7 @@ var AssetSchema = import_zod.z.object({
1076
1089
  path: ["revision"]
1077
1090
  });
1078
1091
  }
1079
- const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror"].filter(Boolean);
1092
+ const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror", asset.batch && "batch"].filter(Boolean);
1080
1093
  if (shapes.length > 1) {
1081
1094
  context.addIssue({
1082
1095
  code: import_zod.z.ZodIssueCode.custom,
@@ -1227,6 +1240,12 @@ var LockEntrySchema = import_zod.z.object({
1227
1240
  sourceAssetId: import_zod.z.string().min(1),
1228
1241
  sourceSha256: import_zod.z.string().regex(/^[0-9a-f]{64}$/)
1229
1242
  }).strict().nullable().default(null),
1243
+ /** For a `1dir` batch member: which leader and slot it rode along on. */
1244
+ batch: import_zod.z.object({
1245
+ role: import_zod.z.enum(["leader", "member"]),
1246
+ leaderAssetId: import_zod.z.string().min(1).optional(),
1247
+ index: import_zod.z.number().int().min(1).optional()
1248
+ }).strict().nullable().default(null),
1230
1249
  /**
1231
1250
  * Output hashes owned by the previous generation while its replacement is
1232
1251
  * pending. They authorize replacing only unchanged PixelKiln-owned files.
@@ -2277,14 +2296,15 @@ var PixelLabClient = class {
2277
2296
  /**
2278
2297
  * `/reduce-colors`, PixelLab's "Cleanup" tier: quantize one image onto a
2279
2298
  * smaller palette, synchronously — no `background_job_id`, the result
2280
- * comes back in this same response, like `createImagePixflux`. Verified
2281
- * against the live OpenAPI schema, not exercised against a live account:
2282
- * the schema's own response example is `usage: {type: "usd", usd: 0.02}`,
2283
- * which — going by this codebase's own repeated experience with PixelLab's
2284
- * documented-vs-billed cost mismatches (`isometricTile`, `objectPro`) —
2285
- * should not be trusted over a real call. `numColors` and `paletteImage`
2286
- * are mutually exclusive upstream; the manifest schema already enforces
2287
- * that before this is ever called.
2299
+ * comes back in this same response, like `createImagePixflux`. The
2300
+ * schema's own response example is dollar-denominated (`usage: {type:
2301
+ * "usd", usd: 0.02}`), which — matching this codebase's repeated
2302
+ * experience with PixelLab's documented-vs-billed cost mismatches
2303
+ * (`isometricTile`, `objectPro`) — did not hold: confirmed live against a
2304
+ * Tier 2 account at exactly 0.1 generations for a 32x32 source
2305
+ * (docs/REVISIONS.md). `numColors` and `paletteImage` are mutually
2306
+ * exclusive upstream; the manifest schema already enforces that before
2307
+ * this is ever called.
2288
2308
  */
2289
2309
  async reduceColors(args) {
2290
2310
  const body = { images: [args.image] };
@@ -2307,8 +2327,9 @@ var PixelLabClient = class {
2307
2327
  /**
2308
2328
  * `/correct-pixelart`, PixelLab's "Cleanup" tier: sharpen edges and drop
2309
2329
  * stray pixels without resizing, synchronously — same shape as
2310
- * `reduceColors` above, no background job. Cost is likewise unverified
2311
- * against a live account (schema example: `usage: {type: "usd", usd: 0.02}`).
2330
+ * `reduceColors` above, no background job. Cost is likewise confirmed
2331
+ * live at 0.1 generations for a 32x32 source, not the schema's own
2332
+ * dollar-denominated example (`usage: {type: "usd", usd: 0.02}`).
2312
2333
  */
2313
2334
  async correctPixelart(args) {
2314
2335
  const body = { images: [args.image] };
@@ -3064,6 +3085,11 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
3064
3085
  // A mirror's bytes come from its source's recorded outputs; the plan
3065
3086
  // compares those directly, so only the choice of source is identity.
3066
3087
  mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
3088
+ // The whole group's ordered descriptions, not just this asset's own —
3089
+ // any sibling's prompt changing, or a member being added or removed,
3090
+ // must mark every member stale together, since they all ride on one
3091
+ // submitted job with no way to amend it after the fact.
3092
+ batch: spec.batch ? { role: spec.batch.role, itemDescriptions: spec.batch.itemDescriptions, index: spec.batch.index } : void 0,
3067
3093
  revision: spec.revision ? {
3068
3094
  mode: spec.revision.mode,
3069
3095
  from: spec.revision.sourceAssetId,
@@ -3245,6 +3271,9 @@ var PixelLabProvider = class _PixelLabProvider {
3245
3271
  return dir;
3246
3272
  }
3247
3273
  estimate(spec) {
3274
+ if (spec.batch?.role === "member") {
3275
+ return { unit: "generations", amount: 0, candidates: 1 };
3276
+ }
3248
3277
  if (spec.revision?.mode === "reduce-colors" || spec.revision?.mode === "correct-pixelart") {
3249
3278
  return { unit: "generations", amount: 0.1, candidates: 1 };
3250
3279
  }
@@ -4043,11 +4072,21 @@ var PixelLabProvider = class _PixelLabProvider {
4043
4072
  return { jobId: res2.background_job_id };
4044
4073
  }
4045
4074
  if (spec.generator === "1dir") {
4075
+ if (spec.batch?.role === "member") {
4076
+ throw new Error(`${spec.styleId}/${spec.assetId}: a batch member cannot submit on its own`);
4077
+ }
4046
4078
  const res2 = await this.client.create1Direction({
4047
4079
  description: spec.prompt,
4048
4080
  size: spec.size,
4049
4081
  view: spec.view === "sidescroller" ? "sidescroller" : "top-down",
4050
- styleImages
4082
+ styleImages,
4083
+ // The leader's own subject is item_descriptions[0], so `description`
4084
+ // matches it exactly. Confirmed live: item_descriptions[0] owns
4085
+ // candidate slot 0 (a 3-item batch returned exactly the declared
4086
+ // items at frames 0/1/2, in order; docs/GENERATORS.md#1dir), so
4087
+ // sending the same text as `description` too is redundant but
4088
+ // harmless, not a hedge against ambiguity.
4089
+ itemDescriptions: spec.batch?.role === "leader" ? spec.batch.itemDescriptions : void 0
4051
4090
  });
4052
4091
  return { jobId: res2.object_id, metadata: { backgroundJobId: res2.background_job_id } };
4053
4092
  }
@@ -7457,6 +7496,14 @@ async function resolveSpecs(loaded, filter) {
7457
7496
  };
7458
7497
  styleSpecs.set(assetId, resolved);
7459
7498
  }
7499
+ const batchMembersByLeader = /* @__PURE__ */ new Map();
7500
+ for (const id of styleSpecs.keys()) {
7501
+ const batch = manifest.assets[id].batch;
7502
+ if (!batch) continue;
7503
+ const members = batchMembersByLeader.get(batch.of) ?? [];
7504
+ members.push({ id, index: batch.index });
7505
+ batchMembersByLeader.set(batch.of, members);
7506
+ }
7460
7507
  const finalized = /* @__PURE__ */ new Set();
7461
7508
  const finalize = async (assetId) => {
7462
7509
  const resolved = styleSpecs.get(assetId);
@@ -7615,6 +7662,56 @@ async function resolveSpecs(loaded, filter) {
7615
7662
  ...asset.revision.enhancePrompt == null ? {} : { enhancePrompt: asset.revision.enhancePrompt }
7616
7663
  };
7617
7664
  }
7665
+ if (asset.batch) {
7666
+ if (asset.batch.of === assetId) throw new Error(`assets.${assetId}: a batch cannot name itself as its own leader`);
7667
+ const leaderSpec = await finalize(asset.batch.of);
7668
+ if (!leaderSpec.batch || leaderSpec.batch.role !== "leader") {
7669
+ throw new Error(`assets.${assetId}: ${asset.batch.of} is not a 1dir batch leader`);
7670
+ }
7671
+ if (resolved.size !== leaderSpec.size) {
7672
+ throw new Error(
7673
+ `assets.${assetId}: batch members share the leader's canvas size; ${asset.batch.of} is ${leaderSpec.size}px, this is ${resolved.size}px`
7674
+ );
7675
+ }
7676
+ resolved.batch = {
7677
+ role: "member",
7678
+ itemDescriptions: leaderSpec.batch.itemDescriptions,
7679
+ index: asset.batch.index,
7680
+ leaderAssetId: asset.batch.of,
7681
+ leaderSpec
7682
+ };
7683
+ } else {
7684
+ const members = batchMembersByLeader.get(assetId);
7685
+ if (members) {
7686
+ if (style.generator !== "1dir") {
7687
+ throw new Error(`assets.${assetId}: only a 1dir asset can lead a batch`);
7688
+ }
7689
+ const sorted = [...members].sort((a, b) => a.index - b.index);
7690
+ const seen = /* @__PURE__ */ new Set();
7691
+ for (const { index } of sorted) {
7692
+ if (seen.has(index)) throw new Error(`assets.${assetId}: two batch members both claim index ${index}`);
7693
+ seen.add(index);
7694
+ }
7695
+ const expectedIndices = sorted.map((_, i) => i + 1).join(",");
7696
+ if (sorted.map((m) => m.index).join(",") !== expectedIndices) {
7697
+ throw new Error(
7698
+ `assets.${assetId}: batch member indices must be 1..${sorted.length} with no gaps; got ${sorted.map((m) => m.index).join(",")}`
7699
+ );
7700
+ }
7701
+ const total = 1 + sorted.length;
7702
+ const limit = candidateCount(resolved.size);
7703
+ if (total > limit) {
7704
+ throw new Error(
7705
+ `assets.${assetId}: a ${resolved.size}px batch holds at most ${limit} items (1 leader + ${limit - 1} members); this one declares ${total}`
7706
+ );
7707
+ }
7708
+ resolved.batch = {
7709
+ role: "leader",
7710
+ itemDescriptions: [resolved.prompt, ...sorted.map(({ id }) => styleSpecs.get(id).prompt)],
7711
+ memberAssetIds: sorted.map((m) => m.id)
7712
+ };
7713
+ }
7714
+ }
7618
7715
  resolved.specHash = specHash(
7619
7716
  resolved,
7620
7717
  styleImageHashes,
@@ -12000,6 +12097,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
12000
12097
  for (const styleId of new Set(items.map((i) => i.spec.styleId))) {
12001
12098
  styleImages.set(styleId, await resolveStyleImages(loaded, styleId));
12002
12099
  }
12100
+ const byKey = new Map(items.map((item) => [item.key, item]));
12003
12101
  let submitted = 0;
12004
12102
  let failed = 0;
12005
12103
  let spent = 0;
@@ -12029,8 +12127,62 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
12029
12127
  await pruneInFlight();
12030
12128
  }
12031
12129
  }
12130
+ function writeBatchMembers(leaderSpec, leaderKey, error) {
12131
+ if (leaderSpec.batch?.role !== "leader") return;
12132
+ const leaderEntry = lock.entries[leaderKey];
12133
+ for (const memberAssetId of leaderSpec.batch.memberAssetIds ?? []) {
12134
+ const memberKey = lockKey(leaderSpec.styleId, memberAssetId);
12135
+ const memberItem = byKey.get(memberKey);
12136
+ if (!memberItem) {
12137
+ throw new Error(
12138
+ `${leaderKey}: batch member "${memberAssetId}" must be submitted in the same run as its leader; run without --only, or include every sibling`
12139
+ );
12140
+ }
12141
+ const memberSpec = memberItem.spec;
12142
+ const memberEstimate = estimates.get(memberKey);
12143
+ const previousMemberEntry = lock.entries[memberKey];
12144
+ const memberSubmittedAt = leaderEntry.submittedAt ?? (/* @__PURE__ */ new Date()).toISOString();
12145
+ upsert(lock, memberKey, {
12146
+ styleId: memberSpec.styleId,
12147
+ assetId: memberSpec.assetId,
12148
+ specHash: memberSpec.specHash,
12149
+ generator: memberSpec.generator,
12150
+ prompt: memberSpec.prompt,
12151
+ width: memberSpec.width,
12152
+ height: memberSpec.height,
12153
+ batch: { role: "member", leaderAssetId: leaderSpec.assetId, index: memberSpec.batch.index },
12154
+ status: error ? "failed" : "processing",
12155
+ error,
12156
+ jobId: error ? null : leaderEntry.jobId,
12157
+ submissionComplete: error ? void 0 : true,
12158
+ reviewObjectId: error ? null : leaderEntry.reviewObjectId,
12159
+ objectId: null,
12160
+ candidateIndex: null,
12161
+ outputs: [],
12162
+ supersededOutputs: previousMemberEntry?.outputs.length ? previousMemberEntry.outputs : previousMemberEntry?.supersededOutputs ?? [],
12163
+ providerMetadata: leaderEntry.providerMetadata,
12164
+ sourceUrl: null,
12165
+ sourceUrls: [],
12166
+ submittedAt: memberSubmittedAt,
12167
+ history: historyAfterReplacing(previousMemberEntry, historyLimit(loaded.manifest), memberSubmittedAt),
12168
+ cost: memberEstimate.amount,
12169
+ costUnit: memberEstimate.unit,
12170
+ provider: provider.id,
12171
+ downloadedAt: null
12172
+ });
12173
+ if (error) failed++;
12174
+ else {
12175
+ submitted++;
12176
+ spent += memberEstimate.amount;
12177
+ log(` ${memberKey} \u2192 ${leaderEntry.jobId} (rides on ${leaderKey}'s batch)`);
12178
+ }
12179
+ }
12180
+ }
12032
12181
  for (const { spec, key } of items) {
12033
12182
  const estimate = estimates.get(key);
12183
+ if (spec.batch?.role === "member") {
12184
+ continue;
12185
+ }
12034
12186
  if (spec.mirror) {
12035
12187
  try {
12036
12188
  await requireRevisionReady(spec, lock);
@@ -12063,6 +12215,14 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
12063
12215
  const since = Date.now() - lastSubmitAt;
12064
12216
  if (since < spacing) await sleep(spacing - since);
12065
12217
  await requireRevisionReady(spec, lock);
12218
+ if (spec.batch?.role === "leader") {
12219
+ const missing = (spec.batch.memberAssetIds ?? []).filter((id) => !byKey.has(lockKey(spec.styleId, id)));
12220
+ if (missing.length) {
12221
+ throw new Error(
12222
+ `${key}: batch member(s) ${missing.join(", ")} must be submitted in the same run as this leader; run without --only, or include every sibling`
12223
+ );
12224
+ }
12225
+ }
12066
12226
  const previousEntry = lock.entries[key];
12067
12227
  const resumesCheckpoint = Boolean(
12068
12228
  previousEntry?.specHash === spec.specHash && previousEntry.provider === provider.id && previousEntry.generator === spec.generator && previousEntry.submissionComplete === false && previousEntry.jobId
@@ -12168,6 +12328,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
12168
12328
  inFlight.set(jobId, spec);
12169
12329
  submitted++;
12170
12330
  spent += estimate.amount;
12331
+ writeBatchMembers(spec, key, null);
12171
12332
  log(
12172
12333
  ` ${key} \u2192 ${jobId} (${spec.width}x${spec.height}` + (estimate.candidates > 1 ? `, ${estimate.candidates} ${spec.tileFeature ? "outputs" : "candidates"}` : "") + `, ${estimate.amount})`
12173
12334
  );
@@ -12179,6 +12340,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
12179
12340
  inFlight.set(entry.jobId, spec);
12180
12341
  submitted++;
12181
12342
  spent += estimate.amount;
12343
+ writeBatchMembers(spec, key, null);
12182
12344
  log(` ${key} \u2192 ${entry.jobId} (recovered from completed checkpoint)`);
12183
12345
  } else {
12184
12346
  failed++;
@@ -12187,6 +12349,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
12187
12349
  error: message6,
12188
12350
  cost: entry.jobId ? estimate.amount : 0
12189
12351
  });
12352
+ writeBatchMembers(spec, key, message6);
12190
12353
  log(` FAILED ${key}: ${message6}`);
12191
12354
  }
12192
12355
  }
@@ -12778,6 +12941,9 @@ function renderSheet(groups, options = {}) {
12778
12941
  .cand.active { border-color:var(--dim); box-shadow:0 0 0 2px color-mix(in srgb, var(--dim) 20%, transparent); }
12779
12942
  .cand.sel { border-color:var(--ok); box-shadow:0 0 0 3px color-mix(in srgb, var(--ok) 22%, transparent); }
12780
12943
  .cand.set-frame { cursor:pointer; }
12944
+ .cand.recommended:not(.sel) { border-color:var(--accent); }
12945
+ .cand.recommended .badge { position:absolute; top:-1px; left:-1px; font-size:9.5px; font-weight:650;
12946
+ text-transform:uppercase; letter-spacing:.03em; color:var(--bg); background:var(--accent); padding:1px 5px; }
12781
12947
  .cand img { image-rendering:pixelated; display:block;
12782
12948
  background-image:
12783
12949
  linear-gradient(45deg,#0000 25%,#7f7f7f22 25%,#7f7f7f22 75%,#0000 75%),
@@ -12935,14 +13101,17 @@ GROUPS.forEach((g, gi) => {
12935
13101
  }).observe(loop);
12936
13102
  }
12937
13103
  }
13104
+ const recommended = g.recommendedIndex ?? 0;
12938
13105
  g.frameUrls.forEach((url, i) => {
12939
13106
  const c = document.createElement('button');
12940
13107
  c.type = 'button';
12941
- c.className = 'cand' + (g.mode === 'frame-set' ? ' set-frame' : '');
13108
+ c.className = 'cand' + (g.mode === 'frame-set' ? ' set-frame' : '') +
13109
+ (g.recommendedIndex != null && i === recommended ? ' recommended' : '');
12942
13110
  c.tabIndex = -1;
12943
- c.setAttribute('aria-label', g.mode === 'frame-set'
13111
+ c.setAttribute('aria-label', (g.mode === 'frame-set'
12944
13112
  ? 'Accept ordered frame set from frame ' + (i + 1)
12945
- : 'Choose candidate ' + (i + 1) + ' of ' + g.frameUrls.length);
13113
+ : 'Choose candidate ' + (i + 1) + ' of ' + g.frameUrls.length) +
13114
+ (g.recommendedIndex != null && i === recommended ? ' (this asset\u2019s declared slot)' : ''));
12946
13115
 
12947
13116
  const preview = document.createElement('img');
12948
13117
  preview.className = 'preview';
@@ -12955,6 +13124,12 @@ GROUPS.forEach((g, gi) => {
12955
13124
  index.textContent = (g.frameLabels?.[i] || String(i + 1)) + ' \xB7 ' +
12956
13125
  g.width + '\xD7' + g.height + ' \xB7 ' + scaleLabel;
12957
13126
  c.append(preview, index);
13127
+ if (g.recommendedIndex != null && i === recommended) {
13128
+ const badge = document.createElement('span');
13129
+ badge.className = 'badge';
13130
+ badge.textContent = 'declared';
13131
+ c.append(badge);
13132
+ }
12958
13133
  if (displayScale > 1 && largestSide <= 96) {
12959
13134
  const actual = document.createElement('img');
12960
13135
  actual.className = 'actual';
@@ -12979,6 +13154,7 @@ GROUPS.forEach((g, gi) => {
12979
13154
  });
12980
13155
  frames.dataset.active = '0';
12981
13156
  frames.querySelector('.cand')?.classList.add('active');
13157
+ if (recommended > 0) activate(recommended, frames);
12982
13158
  root.appendChild(el);
12983
13159
  });
12984
13160
 
@@ -13234,7 +13410,11 @@ async function prepareReview(provider, lock, opts = {}) {
13234
13410
  height: spec.revision.sourceHeight
13235
13411
  }
13236
13412
  } : {},
13237
- ...current && currentRoute ? { current: { url: currentRoute, width: current.width, height: current.height } } : {}
13413
+ ...current && currentRoute ? { current: { url: currentRoute, width: current.width, height: current.height } } : {},
13414
+ // A batch member's declared slot in the shared candidate set — the
13415
+ // leader's own slot 0 is already the sheet's ordinary default, so
13416
+ // only a member (whose slot is 1+) needs to say so explicitly.
13417
+ ...entry.batch?.index != null ? { recommendedIndex: entry.batch.index } : {}
13238
13418
  });
13239
13419
  } catch (err) {
13240
13420
  log(` could not load candidates for ${key}: ${err instanceof Error ? err.message : String(err)}`);
@@ -16282,6 +16462,7 @@ async function workspaceStatus(ws, dir) {
16282
16462
  }
16283
16463
  // Annotate the CommonJS export names for ESM import in node:
16284
16464
  0 && (module.exports = {
16465
+ AssetBatchSchema,
16285
16466
  AssetSchema,
16286
16467
  BudgetError,
16287
16468
  CHARACTER_DIRECTIONS_4,