pixelkiln 0.63.0 → 0.64.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.d.cts CHANGED
@@ -363,6 +363,12 @@ interface Provider {
363
363
  listCharacters?(): AsyncGenerator<RemoteCharacter>;
364
364
  /** One character with its rotations and animations. */
365
365
  getCharacter?(id: string): Promise<RemoteCharacterDetail>;
366
+ /**
367
+ * Irreversible: the character and its animations. A state is its own
368
+ * character record and is deleted on its own. Only reached via `purge`,
369
+ * behind explicit confirmation.
370
+ */
371
+ deleteCharacter?(id: string): Promise<void>;
366
372
  /**
367
373
  * The object's source URLs as the service hands them out now, for
368
374
  * `fetch --refresh` when recorded URLs go stale (PixelLab stamps a
@@ -12502,6 +12508,7 @@ declare class PixelLabProvider implements Provider {
12502
12508
  setTags(objectId: string, tags: string[], generator?: Generator): Promise<void>;
12503
12509
  list(): AsyncGenerator<RemoteAsset>;
12504
12510
  delete(assetId: string): Promise<void>;
12511
+ deleteCharacter(id: string): Promise<void>;
12505
12512
  listCharacters(): AsyncGenerator<RemoteCharacter>;
12506
12513
  getCharacter(id: string): Promise<RemoteCharacterDetail>;
12507
12514
  /**
@@ -12835,6 +12842,7 @@ declare class FakeProvider implements Provider {
12835
12842
  /** Characters answer with whatever the store holds now; other objects keep their URLs. */
12836
12843
  refreshSources(objectId: string, context: RefreshContext): Promise<OutputSource[] | null | undefined>;
12837
12844
  delete(assetId: string): Promise<void>;
12845
+ deleteCharacter(id: string): Promise<void>;
12838
12846
  /** Seed a pre-existing remote asset, for adopt / salvage tests. */
12839
12847
  seed(asset: Partial<RemoteAsset> & {
12840
12848
  id: string;
@@ -18234,6 +18242,18 @@ interface Orphan {
18234
18242
  createdAt: string;
18235
18243
  previewUrl: string;
18236
18244
  tags: string[];
18245
+ /**
18246
+ * `character` for a character group (a base and its states), triaged as
18247
+ * one card; absent for an ordinary account object.
18248
+ */
18249
+ kind?: "character";
18250
+ /** One line the triage sheet shows under the size, e.g. a character's direction/state/loop counts. */
18251
+ note?: string;
18252
+ /** A character group's every character id, base first, with its current tags; a decision applies to all of them. */
18253
+ members?: {
18254
+ id: string;
18255
+ tags: string[];
18256
+ }[];
18237
18257
  }
18238
18258
  declare function findOrphans(provider: Provider, claimed: Set<string>, opts?: {
18239
18259
  onProgress?: (msg: string) => void;
@@ -18318,6 +18338,11 @@ interface SalvageDecision {
18318
18338
  */
18319
18339
  declare function applyTags(provider: Provider, decisions: SalvageDecision[], existing: Map<string, string[]>, opts?: {
18320
18340
  onProgress?: (msg: string) => void;
18341
+ /** Character groups by their card id: the member ids and current tags a decision applies to. */
18342
+ characters?: Map<string, {
18343
+ id: string;
18344
+ tags: string[];
18345
+ }[]>;
18321
18346
  }): Promise<{
18322
18347
  tagged: number;
18323
18348
  failed: number;
@@ -18328,14 +18353,22 @@ interface SalvageResult {
18328
18353
  kept: number;
18329
18354
  discarded: number;
18330
18355
  failed: number;
18356
+ /**
18357
+ * Manifest assets written for imported character groups. They have no
18358
+ * lock entries yet: the caller adopts them (`adoptCharacters`) once the
18359
+ * manifest is saved, which downloads every rotation and frame.
18360
+ */
18361
+ characterAssetIds?: string[];
18331
18362
  }
18332
18363
  /**
18333
18364
  * Serves the triage sheet and applies decisions.
18334
18365
  *
18335
18366
  * `import` is the only action that touches local state: it downloads the image,
18336
18367
  * writes a manifest asset and a lock entry, so a recovered sprite becomes a
18337
- * first-class tracked asset rather than a loose file. `keep` and `discard` only
18338
- * write tags upstream; no object is ever deleted here.
18368
+ * first-class tracked asset rather than a loose file. A character group's
18369
+ * import writes its base, states, and loops as manifest assets only; the
18370
+ * caller adopts them. `keep` and `discard` only write tags upstream (on every
18371
+ * member of a character group); nothing is ever deleted here.
18339
18372
  */
18340
18373
  declare function runSalvage(provider: Provider, orphans: Orphan[], ctx: {
18341
18374
  manifestPath: string;
package/dist/index.d.ts CHANGED
@@ -363,6 +363,12 @@ interface Provider {
363
363
  listCharacters?(): AsyncGenerator<RemoteCharacter>;
364
364
  /** One character with its rotations and animations. */
365
365
  getCharacter?(id: string): Promise<RemoteCharacterDetail>;
366
+ /**
367
+ * Irreversible: the character and its animations. A state is its own
368
+ * character record and is deleted on its own. Only reached via `purge`,
369
+ * behind explicit confirmation.
370
+ */
371
+ deleteCharacter?(id: string): Promise<void>;
366
372
  /**
367
373
  * The object's source URLs as the service hands them out now, for
368
374
  * `fetch --refresh` when recorded URLs go stale (PixelLab stamps a
@@ -12502,6 +12508,7 @@ declare class PixelLabProvider implements Provider {
12502
12508
  setTags(objectId: string, tags: string[], generator?: Generator): Promise<void>;
12503
12509
  list(): AsyncGenerator<RemoteAsset>;
12504
12510
  delete(assetId: string): Promise<void>;
12511
+ deleteCharacter(id: string): Promise<void>;
12505
12512
  listCharacters(): AsyncGenerator<RemoteCharacter>;
12506
12513
  getCharacter(id: string): Promise<RemoteCharacterDetail>;
12507
12514
  /**
@@ -12835,6 +12842,7 @@ declare class FakeProvider implements Provider {
12835
12842
  /** Characters answer with whatever the store holds now; other objects keep their URLs. */
12836
12843
  refreshSources(objectId: string, context: RefreshContext): Promise<OutputSource[] | null | undefined>;
12837
12844
  delete(assetId: string): Promise<void>;
12845
+ deleteCharacter(id: string): Promise<void>;
12838
12846
  /** Seed a pre-existing remote asset, for adopt / salvage tests. */
12839
12847
  seed(asset: Partial<RemoteAsset> & {
12840
12848
  id: string;
@@ -18234,6 +18242,18 @@ interface Orphan {
18234
18242
  createdAt: string;
18235
18243
  previewUrl: string;
18236
18244
  tags: string[];
18245
+ /**
18246
+ * `character` for a character group (a base and its states), triaged as
18247
+ * one card; absent for an ordinary account object.
18248
+ */
18249
+ kind?: "character";
18250
+ /** One line the triage sheet shows under the size, e.g. a character's direction/state/loop counts. */
18251
+ note?: string;
18252
+ /** A character group's every character id, base first, with its current tags; a decision applies to all of them. */
18253
+ members?: {
18254
+ id: string;
18255
+ tags: string[];
18256
+ }[];
18237
18257
  }
18238
18258
  declare function findOrphans(provider: Provider, claimed: Set<string>, opts?: {
18239
18259
  onProgress?: (msg: string) => void;
@@ -18318,6 +18338,11 @@ interface SalvageDecision {
18318
18338
  */
18319
18339
  declare function applyTags(provider: Provider, decisions: SalvageDecision[], existing: Map<string, string[]>, opts?: {
18320
18340
  onProgress?: (msg: string) => void;
18341
+ /** Character groups by their card id: the member ids and current tags a decision applies to. */
18342
+ characters?: Map<string, {
18343
+ id: string;
18344
+ tags: string[];
18345
+ }[]>;
18321
18346
  }): Promise<{
18322
18347
  tagged: number;
18323
18348
  failed: number;
@@ -18328,14 +18353,22 @@ interface SalvageResult {
18328
18353
  kept: number;
18329
18354
  discarded: number;
18330
18355
  failed: number;
18356
+ /**
18357
+ * Manifest assets written for imported character groups. They have no
18358
+ * lock entries yet: the caller adopts them (`adoptCharacters`) once the
18359
+ * manifest is saved, which downloads every rotation and frame.
18360
+ */
18361
+ characterAssetIds?: string[];
18331
18362
  }
18332
18363
  /**
18333
18364
  * Serves the triage sheet and applies decisions.
18334
18365
  *
18335
18366
  * `import` is the only action that touches local state: it downloads the image,
18336
18367
  * writes a manifest asset and a lock entry, so a recovered sprite becomes a
18337
- * first-class tracked asset rather than a loose file. `keep` and `discard` only
18338
- * write tags upstream; no object is ever deleted here.
18368
+ * first-class tracked asset rather than a loose file. A character group's
18369
+ * import writes its base, states, and loops as manifest assets only; the
18370
+ * caller adopts them. `keep` and `discard` only write tags upstream (on every
18371
+ * member of a character group); nothing is ever deleted here.
18339
18372
  */
18340
18373
  declare function runSalvage(provider: Provider, orphans: Orphan[], ctx: {
18341
18374
  manifestPath: string;
package/dist/index.js CHANGED
@@ -4734,8 +4734,8 @@ var PixelLabProvider = class _PixelLabProvider {
4734
4734
  const sources = [];
4735
4735
  const terrainTiles = [];
4736
4736
  for (const [index, tile] of tileset.tiles.entries()) {
4737
- const slug = tile.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "tile";
4738
- const role = `tile-${String(index).padStart(2, "0")}-${slug}`;
4737
+ const slug2 = tile.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "tile";
4738
+ const role = `tile-${String(index).padStart(2, "0")}-${slug2}`;
4739
4739
  const file = path.join(_PixelLabProvider.cacheDir(), `${tilesetId}-${index}.png`);
4740
4740
  writeFileSync(file, Buffer.from(tile.image.base64, "base64"));
4741
4741
  sources.push({ url: `file://${file}`, role });
@@ -4854,6 +4854,9 @@ var PixelLabProvider = class _PixelLabProvider {
4854
4854
  async delete(assetId) {
4855
4855
  await this.client.deleteObject(assetId);
4856
4856
  }
4857
+ async deleteCharacter(id) {
4858
+ await this.client.deleteCharacter(id);
4859
+ }
4857
4860
  async *listCharacters() {
4858
4861
  for await (const character of this.client.iterateCharacters(100)) {
4859
4862
  yield remoteCharacter(character);
@@ -6877,6 +6880,8 @@ var FakeProvider = class {
6877
6880
  this.tags.set(objectId, [...tags]);
6878
6881
  const asset = this.assets.get(objectId);
6879
6882
  if (asset) asset.tags = [...tags];
6883
+ const character = this.characters.get(objectId.split("#")[0]);
6884
+ if (character) character.tags = [...tags];
6880
6885
  }
6881
6886
  /**
6882
6887
  * Assigned in the constructor rather than declared as a method, so a test can
@@ -6911,6 +6916,10 @@ var FakeProvider = class {
6911
6916
  this.deleted.push(assetId);
6912
6917
  this.assets.delete(assetId);
6913
6918
  }
6919
+ async deleteCharacter(id) {
6920
+ this.deleted.push(id);
6921
+ this.characters.delete(id);
6922
+ }
6914
6923
  /** Seed a pre-existing remote asset, for adopt / salvage tests. */
6915
6924
  seed(asset) {
6916
6925
  const full = {
@@ -16435,6 +16444,9 @@ async function loadClaims(lockPaths, opts = {}) {
16435
16444
  add(entry.objectId);
16436
16445
  add(entry.reviewObjectId);
16437
16446
  add(entry.jobId);
16447
+ for (const id of [entry.objectId, entry.jobId]) {
16448
+ if (id?.includes("#")) add(id.split("#")[0]);
16449
+ }
16438
16450
  }
16439
16451
  }
16440
16452
  return claimed;
@@ -16458,6 +16470,54 @@ async function findOrphans(provider, claimed, opts = {}) {
16458
16470
  })).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
16459
16471
  return { orphans, total: all.length };
16460
16472
  }
16473
+ function isState(character) {
16474
+ return Boolean(character.stateName && character.stateName !== "Idle");
16475
+ }
16476
+ function characterSalvageAssets(characters, ctx) {
16477
+ const ordered = [...characters].sort((a, b) => Number(isState(a)) - Number(isState(b)) || a.createdAt.localeCompare(b.createdAt));
16478
+ const base = ordered[0];
16479
+ const common = { styles: [ctx.styleId], tags: ["salvaged"] };
16480
+ const assets = {};
16481
+ const skipped = [];
16482
+ const baseId = idFromPrompt(base.name || base.prompt, ctx.taken);
16483
+ const size = base.width === base.height && base.width >= 32 && base.width <= 256 && base.width !== ctx.styleSize ? { size: base.width } : {};
16484
+ assets[baseId] = { prompt: base.prompt || base.name || baseId, remoteId: base.id, ...size, ...common };
16485
+ const assetIdOf = /* @__PURE__ */ new Map([[base.id, baseId]]);
16486
+ for (const state of ordered.slice(1)) {
16487
+ const stateId = uniqueId(`${baseId}_${slug(state.stateName ?? "state")}`, ctx.taken);
16488
+ assetIdOf.set(state.id, stateId);
16489
+ assets[stateId] = { prompt: state.prompt || state.stateName || stateId, state: { of: baseId }, remoteId: state.id, ...common };
16490
+ }
16491
+ for (const character of ordered) {
16492
+ const parentId = assetIdOf.get(character.id);
16493
+ for (const animation of character.animations) {
16494
+ if (!animation.frames.length) continue;
16495
+ const label = animation.name || animation.type;
16496
+ if (!animation.groupId) {
16497
+ skipped.push(`${parentId}: "${label}" ${animation.direction} loop has no animation group id to adopt it by`);
16498
+ continue;
16499
+ }
16500
+ const loopId = uniqueId(`${parentId}.${slug(label)}.${animation.direction}`, ctx.taken);
16501
+ assets[loopId] = {
16502
+ prompt: label,
16503
+ animation: { of: parentId, direction: animation.direction },
16504
+ remoteId: `${character.id}#${animation.groupId}`,
16505
+ ...common
16506
+ };
16507
+ }
16508
+ }
16509
+ return { assets, skipped };
16510
+ }
16511
+ function slug(text) {
16512
+ return text.toLowerCase().replace(/^pixelkiln:.*\//, "").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "loop";
16513
+ }
16514
+ function uniqueId(base, taken) {
16515
+ let id = base;
16516
+ let n = 2;
16517
+ while (taken.has(id)) id = `${base}_${n++}`;
16518
+ taken.add(id);
16519
+ return id;
16520
+ }
16461
16521
  var MIN_STYLE_FRAGMENT_LENGTH = 12;
16462
16522
  function matchStyleByPattern(prompt, manifest) {
16463
16523
  const p = prompt.toLowerCase();
@@ -16587,18 +16647,22 @@ async function applyTags(provider, decisions, existing, opts = {}) {
16587
16647
  let tagged = 0;
16588
16648
  let failed = 0;
16589
16649
  for (const { id, action } of decisions) {
16590
- const current = (existing.get(id) ?? []).filter(
16591
- (t) => t !== "pixelkiln:keep" && t !== "pixelkiln:discard" && t !== "pixelkiln:imported"
16592
- );
16593
- const decisionTag = `pixelkiln:${action === "import" ? "imported" : action}`;
16594
- const next = [...current.slice(0, 19), decisionTag];
16595
- if (!provider.setTags) return { tagged, failed };
16596
- try {
16597
- await provider.setTags(id, next);
16598
- tagged++;
16599
- } catch (err) {
16600
- failed++;
16601
- log(` tag failed ${id}: ${err instanceof Error ? err.message : String(err)}`);
16650
+ const group = opts.characters?.get(id);
16651
+ const targets = group ?? [{ id, tags: existing.get(id) ?? [] }];
16652
+ for (const target of targets) {
16653
+ const current = target.tags.filter(
16654
+ (t) => t !== "pixelkiln:keep" && t !== "pixelkiln:discard" && t !== "pixelkiln:imported"
16655
+ );
16656
+ const decisionTag = `pixelkiln:${action === "import" ? "imported" : action}`;
16657
+ const next = [...current.slice(0, 19), decisionTag];
16658
+ if (!provider.setTags) return { tagged, failed };
16659
+ try {
16660
+ await provider.setTags(target.id, next, group ? "character" : void 0);
16661
+ tagged++;
16662
+ } catch (err) {
16663
+ failed++;
16664
+ log(` tag failed ${target.id}: ${err instanceof Error ? err.message : String(err)}`);
16665
+ }
16602
16666
  }
16603
16667
  }
16604
16668
  return { tagged, failed };
@@ -16611,7 +16675,7 @@ import path33 from "path";
16611
16675
  // src/pick/salvage-sheet.ts
16612
16676
  var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
16613
16677
  function renderSalvageSheet(orphans, ctx) {
16614
- const safe = orphans.map((o) => ({ ...o, prompt: escapeHtml3(o.prompt) }));
16678
+ const safe = orphans.map((o) => ({ ...o, prompt: escapeHtml3(o.prompt), ...o.note ? { note: escapeHtml3(o.note) } : {} }));
16615
16679
  const data = JSON.stringify(safe).replace(/<\//g, "<\\/");
16616
16680
  const styleId = ctx ? escapeHtml3(ctx.styleId) : null;
16617
16681
  const importDir = ctx ? escapeHtml3(ctx.importDir) : null;
@@ -16697,6 +16761,7 @@ ITEMS.forEach((o, i) => {
16697
16761
  el.innerHTML =
16698
16762
  '<div class="thumb"><img src="' + o.previewUrl + '" loading="lazy"></div>' +
16699
16763
  '<div class="meta">' + o.width + '\xD7' + o.height + ' \xB7 ' + o.createdAt.slice(0,10) + '</div>' +
16764
+ (o.note ? '<div class="meta">' + o.note + '</div>' : '') +
16700
16765
  '<div class="prompt">' + o.prompt + '</div>' +
16701
16766
  '<div class="acts">' +
16702
16767
  '<button data-a="import">import</button>' +
@@ -16775,6 +16840,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
16775
16840
  });
16776
16841
  const byId = new Map(orphans.map((o) => [o.id, o]));
16777
16842
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
16843
+ const characterMembers = new Map(
16844
+ orphans.flatMap((o) => o.kind === "character" && o.members ? [[o.id, o.members]] : [])
16845
+ );
16778
16846
  return serveReviewPage({
16779
16847
  html,
16780
16848
  port: opts.port,
@@ -16791,10 +16859,34 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
16791
16859
  const result = { imported: 0, kept: 0, discarded: 0, failed: 0 };
16792
16860
  const taken = new Set(Object.keys(ctx.manifest.assets));
16793
16861
  const importedAssetIds = [];
16862
+ const characterAssetIds = [];
16794
16863
  for (const decision of decisions) {
16795
16864
  const orphan = byId.get(decision.id);
16796
16865
  if (!orphan) continue;
16797
- if (decision.action === "import") {
16866
+ if (decision.action === "import" && orphan.kind === "character") {
16867
+ try {
16868
+ if (!provider.getCharacter) throw new Error(`provider ${provider.id} cannot read characters`);
16869
+ const details = [];
16870
+ for (const member of orphan.members ?? [{ id: orphan.id }]) details.push(await provider.getCharacter(member.id));
16871
+ const style = ctx.manifest.styles[ctx.styleId];
16872
+ const { assets, skipped } = characterSalvageAssets(details, {
16873
+ styleId: ctx.styleId,
16874
+ styleSize: style?.size,
16875
+ taken
16876
+ });
16877
+ for (const [assetId, asset] of Object.entries(assets)) {
16878
+ ctx.manifest.assets[assetId] = asset;
16879
+ importedAssetIds.push(assetId);
16880
+ characterAssetIds.push(assetId);
16881
+ }
16882
+ for (const line of skipped) log(` not imported: ${line}`);
16883
+ result.imported++;
16884
+ log(` imported character ${orphan.id} as ${Object.keys(assets).length} asset(s)`);
16885
+ } catch (err) {
16886
+ result.failed++;
16887
+ log(` import failed ${orphan.id}: ${err instanceof Error ? err.message : String(err)}`);
16888
+ }
16889
+ } else if (decision.action === "import") {
16798
16890
  try {
16799
16891
  const buf = await provider.download(orphan.previewUrl);
16800
16892
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
@@ -16858,11 +16950,12 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
16858
16950
  result.discarded++;
16859
16951
  }
16860
16952
  }
16861
- await applyTags(provider, decisions, existingTags, { onProgress: log });
16953
+ await applyTags(provider, decisions, existingTags, { onProgress: log, characters: characterMembers });
16862
16954
  const raw = JSON.parse(await readFile26(ctx.manifestPath, "utf8"));
16863
16955
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
16864
16956
  await writeFile13(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
16865
16957
  await saveLock(ctx.lockPath, ctx.lock);
16958
+ if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
16866
16959
  return result;
16867
16960
  }
16868
16961
  });