pixelkiln 0.62.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/README.md +2 -2
- package/dist/cli.js +384 -48
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +240 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +150 -48
- package/dist/index.d.ts +150 -48
- package/dist/index.js +240 -29
- package/dist/index.js.map +1 -1
- package/docs/CHARACTERS.md +39 -0
- package/docs/CLI.md +6 -3
- package/docs/MANIFEST.md +1 -1
- package/docs/RECOVERY.md +36 -3
- package/package.json +1 -1
- package/schema/manifest.schema.json +17 -0
- package/skills/pixelkiln/SKILL.md +5 -2
package/dist/index.cjs
CHANGED
|
@@ -511,6 +511,14 @@ var CharacterAnimationSchema = import_zod.z.object({
|
|
|
511
511
|
template: import_zod.z.string().min(1).optional(),
|
|
512
512
|
/** Direction to animate. */
|
|
513
513
|
direction: CharacterDirectionSchema.default("south"),
|
|
514
|
+
/**
|
|
515
|
+
* Shorthand for one loop in several directions, instead of `direction`.
|
|
516
|
+
* Expanded when the manifest loads into one loop per named direction
|
|
517
|
+
* (`<id>.<direction>`) plus a mirror for each direction whose flip is
|
|
518
|
+
* not named, exactly as if each had been written out. See
|
|
519
|
+
* `src/loop-directions.ts`.
|
|
520
|
+
*/
|
|
521
|
+
directions: import_zod.z.array(CharacterDirectionSchema).min(1).optional(),
|
|
514
522
|
/** Frames to generate without a template (v3). Even, 4 to 16. */
|
|
515
523
|
frames: import_zod.z.number().int().min(4).max(16).optional(),
|
|
516
524
|
/** Playback rate recorded with the frames; PixelLab does not store one. */
|
|
@@ -539,6 +547,13 @@ var CharacterAnimationSchema = import_zod.z.object({
|
|
|
539
547
|
/** v3 text loops only: let PixelLab expand the action into a fuller motion description first. */
|
|
540
548
|
enhancePrompt: import_zod.z.boolean().optional()
|
|
541
549
|
}).strict().superRefine((animation, context) => {
|
|
550
|
+
if (animation.directions !== void 0) {
|
|
551
|
+
context.addIssue({
|
|
552
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
553
|
+
message: "directions is expanded when the manifest loads; validate the loaded manifest, not the raw file",
|
|
554
|
+
path: ["directions"]
|
|
555
|
+
});
|
|
556
|
+
}
|
|
542
557
|
if (animation.frames !== void 0 && animation.frames % 2 !== 0) {
|
|
543
558
|
context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "frames must be even", path: ["frames"] });
|
|
544
559
|
}
|
|
@@ -5037,8 +5052,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5037
5052
|
const sources = [];
|
|
5038
5053
|
const terrainTiles = [];
|
|
5039
5054
|
for (const [index, tile] of tileset.tiles.entries()) {
|
|
5040
|
-
const
|
|
5041
|
-
const role = `tile-${String(index).padStart(2, "0")}-${
|
|
5055
|
+
const slug2 = tile.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "tile";
|
|
5056
|
+
const role = `tile-${String(index).padStart(2, "0")}-${slug2}`;
|
|
5042
5057
|
const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${tilesetId}-${index}.png`);
|
|
5043
5058
|
(0, import_node_fs2.writeFileSync)(file, Buffer.from(tile.image.base64, "base64"));
|
|
5044
5059
|
sources.push({ url: `file://${file}`, role });
|
|
@@ -5157,6 +5172,9 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5157
5172
|
async delete(assetId) {
|
|
5158
5173
|
await this.client.deleteObject(assetId);
|
|
5159
5174
|
}
|
|
5175
|
+
async deleteCharacter(id) {
|
|
5176
|
+
await this.client.deleteCharacter(id);
|
|
5177
|
+
}
|
|
5160
5178
|
async *listCharacters() {
|
|
5161
5179
|
for await (const character of this.client.iterateCharacters(100)) {
|
|
5162
5180
|
yield remoteCharacter(character);
|
|
@@ -7180,6 +7198,8 @@ var FakeProvider = class {
|
|
|
7180
7198
|
this.tags.set(objectId, [...tags]);
|
|
7181
7199
|
const asset = this.assets.get(objectId);
|
|
7182
7200
|
if (asset) asset.tags = [...tags];
|
|
7201
|
+
const character = this.characters.get(objectId.split("#")[0]);
|
|
7202
|
+
if (character) character.tags = [...tags];
|
|
7183
7203
|
}
|
|
7184
7204
|
/**
|
|
7185
7205
|
* Assigned in the constructor rather than declared as a method, so a test can
|
|
@@ -7214,6 +7234,10 @@ var FakeProvider = class {
|
|
|
7214
7234
|
this.deleted.push(assetId);
|
|
7215
7235
|
this.assets.delete(assetId);
|
|
7216
7236
|
}
|
|
7237
|
+
async deleteCharacter(id) {
|
|
7238
|
+
this.deleted.push(id);
|
|
7239
|
+
this.characters.delete(id);
|
|
7240
|
+
}
|
|
7217
7241
|
/** Seed a pre-existing remote asset, for adopt / salvage tests. */
|
|
7218
7242
|
seed(asset) {
|
|
7219
7243
|
const full = {
|
|
@@ -7310,6 +7334,91 @@ var import_promises3 = require("fs/promises");
|
|
|
7310
7334
|
var import_node_fs5 = require("fs");
|
|
7311
7335
|
var import_node_path5 = __toESM(require("path"), 1);
|
|
7312
7336
|
|
|
7337
|
+
// src/loop-directions.ts
|
|
7338
|
+
var SINGLE_OUTPUT_FIELDS = ["file", "cell", "remoteId", "source", "sourceByStyle", "outputRole"];
|
|
7339
|
+
var MIRROR_FIELDS = ["category", "tags", "styles"];
|
|
7340
|
+
function loopDirectionId(shorthandId, direction) {
|
|
7341
|
+
return `${shorthandId}.${direction}`;
|
|
7342
|
+
}
|
|
7343
|
+
function expandLoopDirections(raw) {
|
|
7344
|
+
const families = {};
|
|
7345
|
+
const issues = [];
|
|
7346
|
+
if (!isObject2(raw) || !isObject2(raw.assets)) return { raw, families, issues };
|
|
7347
|
+
const assets = raw.assets;
|
|
7348
|
+
if (!Object.values(assets).some((asset) => isObject2(asset) && isObject2(asset.animation) && "directions" in asset.animation)) {
|
|
7349
|
+
return { raw, families, issues };
|
|
7350
|
+
}
|
|
7351
|
+
const expanded = {};
|
|
7352
|
+
const claimed = new Set(Object.keys(assets));
|
|
7353
|
+
for (const [id, asset] of Object.entries(assets)) {
|
|
7354
|
+
if (!isObject2(asset) || !isObject2(asset.animation) || !("directions" in asset.animation)) {
|
|
7355
|
+
expanded[id] = asset;
|
|
7356
|
+
continue;
|
|
7357
|
+
}
|
|
7358
|
+
const where = `assets.${id}.animation.directions`;
|
|
7359
|
+
const { directions: rawDirections, ...animation } = asset.animation;
|
|
7360
|
+
const parsed = CharacterDirectionSchema.array().min(1).safeParse(rawDirections);
|
|
7361
|
+
if (!parsed.success) {
|
|
7362
|
+
issues.push(`${where}: expected a non-empty list of directions (${CharacterDirectionSchema.options.join(", ")})`);
|
|
7363
|
+
continue;
|
|
7364
|
+
}
|
|
7365
|
+
const directions = parsed.data;
|
|
7366
|
+
const duplicate = directions.find((direction, index) => directions.indexOf(direction) !== index);
|
|
7367
|
+
if (duplicate) {
|
|
7368
|
+
issues.push(`${where}: "${duplicate}" is listed twice`);
|
|
7369
|
+
continue;
|
|
7370
|
+
}
|
|
7371
|
+
if ("direction" in animation) {
|
|
7372
|
+
issues.push(`assets.${id}.animation: direction and directions are mutually exclusive; directions names every loop to make`);
|
|
7373
|
+
continue;
|
|
7374
|
+
}
|
|
7375
|
+
const single = SINGLE_OUTPUT_FIELDS.filter((field) => field in asset);
|
|
7376
|
+
if (single.length) {
|
|
7377
|
+
issues.push(
|
|
7378
|
+
`assets.${id}: ${single.join(", ")} name${single.length === 1 ? "s" : ""} one output, but directions makes ${directions.length} loops; declare the loops explicitly to set ${single.length === 1 ? "it" : "them"} per direction`
|
|
7379
|
+
);
|
|
7380
|
+
continue;
|
|
7381
|
+
}
|
|
7382
|
+
const loops = directions.map((direction) => ({ id: loopDirectionId(id, direction), direction }));
|
|
7383
|
+
const named = new Set(directions);
|
|
7384
|
+
const mirrors = [];
|
|
7385
|
+
for (const { id: loopId, direction } of loops) {
|
|
7386
|
+
const flipped = MIRRORED_DIRECTION[direction];
|
|
7387
|
+
if (flipped === direction || named.has(flipped)) continue;
|
|
7388
|
+
named.add(flipped);
|
|
7389
|
+
mirrors.push({ id: loopDirectionId(id, flipped), of: loopId });
|
|
7390
|
+
}
|
|
7391
|
+
const ids = [...loops.map((loop) => loop.id), ...mirrors.map((mirror) => mirror.id)];
|
|
7392
|
+
const collision = ids.find((expandedId) => claimed.has(expandedId));
|
|
7393
|
+
if (collision) {
|
|
7394
|
+
issues.push(`${where}: expands to "${collision}", which is already declared`);
|
|
7395
|
+
continue;
|
|
7396
|
+
}
|
|
7397
|
+
for (const expandedId of ids) claimed.add(expandedId);
|
|
7398
|
+
for (const loop of loops) {
|
|
7399
|
+
expanded[loop.id] = { ...asset, animation: { ...animation, direction: loop.direction } };
|
|
7400
|
+
}
|
|
7401
|
+
for (const mirror of mirrors) {
|
|
7402
|
+
const inherited = {};
|
|
7403
|
+
for (const field of MIRROR_FIELDS) if (field in asset) inherited[field] = asset[field];
|
|
7404
|
+
expanded[mirror.id] = { mirror: mirror.of, ...inherited };
|
|
7405
|
+
}
|
|
7406
|
+
families[id] = ids;
|
|
7407
|
+
}
|
|
7408
|
+
return { raw: { ...raw, assets: expanded }, families, issues };
|
|
7409
|
+
}
|
|
7410
|
+
function expandAssetFilter(ids, families) {
|
|
7411
|
+
if (!families) return [...ids];
|
|
7412
|
+
return [...new Set(ids.flatMap((id) => families[id] ?? [id]))];
|
|
7413
|
+
}
|
|
7414
|
+
function loopShorthandOf(assetId, families) {
|
|
7415
|
+
if (!families) return void 0;
|
|
7416
|
+
return Object.keys(families).find((id) => families[id].includes(assetId));
|
|
7417
|
+
}
|
|
7418
|
+
function isObject2(value) {
|
|
7419
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7420
|
+
}
|
|
7421
|
+
|
|
7313
7422
|
// src/outputs.ts
|
|
7314
7423
|
var import_node_fs4 = require("fs");
|
|
7315
7424
|
var import_node_path4 = __toESM(require("path"), 1);
|
|
@@ -7628,7 +7737,12 @@ function frameSetFps(entry) {
|
|
|
7628
7737
|
async function loadManifest(manifestPath) {
|
|
7629
7738
|
const abs = import_node_path5.default.resolve(manifestPath);
|
|
7630
7739
|
if (!(0, import_node_fs5.existsSync)(abs)) throw new ProjectError(`No manifest at ${abs}`);
|
|
7631
|
-
const
|
|
7740
|
+
const expansion = expandLoopDirections(JSON.parse(await (0, import_promises3.readFile)(abs, "utf8")));
|
|
7741
|
+
if (expansion.issues.length) {
|
|
7742
|
+
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
7743
|
+
${expansion.issues.map((i) => ` ${i}`).join("\n")}`);
|
|
7744
|
+
}
|
|
7745
|
+
const input = ManifestInputSchema.safeParse(expansion.raw);
|
|
7632
7746
|
if (!input.success) {
|
|
7633
7747
|
const issues = formatManifestIssues(input.error.issues);
|
|
7634
7748
|
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
@@ -7713,7 +7827,8 @@ ${issues}`);
|
|
|
7713
7827
|
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
7714
7828
|
${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
7715
7829
|
}
|
|
7716
|
-
|
|
7830
|
+
const loopFamilies = Object.keys(expansion.families).length ? { loopFamilies: expansion.families } : {};
|
|
7831
|
+
return { manifest: parsed.data, root: import_node_path5.default.dirname(abs), path: abs, ...loopFamilies };
|
|
7717
7832
|
}
|
|
7718
7833
|
function parentAssetId(asset) {
|
|
7719
7834
|
return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of;
|
|
@@ -7797,12 +7912,13 @@ async function resolveSpecs(loaded, filter) {
|
|
|
7797
7912
|
);
|
|
7798
7913
|
}
|
|
7799
7914
|
}
|
|
7800
|
-
|
|
7915
|
+
const assetFilter = expandAssetFilter(filter?.assets ?? [], loaded.loopFamilies);
|
|
7916
|
+
for (const unknownAsset of assetFilter) {
|
|
7801
7917
|
if (!manifest.assets[unknownAsset]) {
|
|
7802
7918
|
throw new UsageError(`Unknown asset "${unknownAsset}".`);
|
|
7803
7919
|
}
|
|
7804
7920
|
}
|
|
7805
|
-
const requestedAssetIds = new Set(
|
|
7921
|
+
const requestedAssetIds = new Set(assetFilter.length ? assetFilter : Object.keys(manifest.assets));
|
|
7806
7922
|
const resolutionAssetIds = new Set(requestedAssetIds);
|
|
7807
7923
|
for (const assetId of [...requestedAssetIds]) {
|
|
7808
7924
|
let current = parentAssetId(manifest.assets[assetId]);
|
|
@@ -14526,11 +14642,21 @@ function assetStyles(raw, asset) {
|
|
|
14526
14642
|
const own = Array.isArray(asset.styles) ? asset.styles : [];
|
|
14527
14643
|
return own.length ? own : Object.keys(raw.styles ?? {});
|
|
14528
14644
|
}
|
|
14645
|
+
function declaredAssets(raw) {
|
|
14646
|
+
const expanded = expandLoopDirections(raw).raw;
|
|
14647
|
+
return new Set(Object.keys(expanded.assets ?? {}));
|
|
14648
|
+
}
|
|
14649
|
+
function undeclaredAsset(raw, assetId) {
|
|
14650
|
+
const shorthand = loopShorthandOf(assetId, expandLoopDirections(raw).families);
|
|
14651
|
+
return new ManifestEditError(
|
|
14652
|
+
shorthand ? `asset "${assetId}" is expanded from "${shorthand}"'s animation.directions; edit "${shorthand}" (it applies to every direction), or write this direction out as its own asset to change it alone` : `asset "${assetId}" is not declared by the manifest`
|
|
14653
|
+
);
|
|
14654
|
+
}
|
|
14529
14655
|
function applyEdit(raw, edit) {
|
|
14530
14656
|
raw.assets ??= {};
|
|
14531
14657
|
if (edit.action === "set-source" || edit.action === "clear-source") {
|
|
14532
14658
|
const asset2 = Object.hasOwn(raw.assets, edit.assetId) ? raw.assets[edit.assetId] : void 0;
|
|
14533
|
-
if (!asset2) throw
|
|
14659
|
+
if (!asset2) throw undeclaredAsset(raw, edit.assetId);
|
|
14534
14660
|
if (!raw.styles || !Object.hasOwn(raw.styles, edit.styleId)) {
|
|
14535
14661
|
throw new ManifestEditError(`unknown style "${edit.styleId}"`);
|
|
14536
14662
|
}
|
|
@@ -14582,7 +14708,7 @@ function applyEdit(raw, edit) {
|
|
|
14582
14708
|
return;
|
|
14583
14709
|
}
|
|
14584
14710
|
if (edit.action === "add-asset") {
|
|
14585
|
-
if (
|
|
14711
|
+
if (declaredAssets(raw).has(edit.assetId)) {
|
|
14586
14712
|
throw new ManifestEditError(`asset "${edit.assetId}" already exists`);
|
|
14587
14713
|
}
|
|
14588
14714
|
const { styles, tags, ...rest } = edit.asset;
|
|
@@ -14596,20 +14722,20 @@ function applyEdit(raw, edit) {
|
|
|
14596
14722
|
}
|
|
14597
14723
|
asset2.styles = styles;
|
|
14598
14724
|
}
|
|
14599
|
-
if (edit.asset.revision && !
|
|
14725
|
+
if (edit.asset.revision && !declaredAssets(raw).has(edit.asset.revision.from)) {
|
|
14600
14726
|
throw new ManifestEditError(`revision parent "${edit.asset.revision.from}" is not declared by the manifest`);
|
|
14601
14727
|
}
|
|
14602
|
-
if (edit.asset.state && !
|
|
14728
|
+
if (edit.asset.state && !declaredAssets(raw).has(edit.asset.state.of)) {
|
|
14603
14729
|
throw new ManifestEditError(`state parent "${edit.asset.state.of}" is not declared by the manifest`);
|
|
14604
14730
|
}
|
|
14605
|
-
if (edit.asset.animation && !
|
|
14731
|
+
if (edit.asset.animation && !declaredAssets(raw).has(edit.asset.animation.of)) {
|
|
14606
14732
|
throw new ManifestEditError(`animation parent "${edit.asset.animation.of}" is not declared by the manifest`);
|
|
14607
14733
|
}
|
|
14608
14734
|
raw.assets[edit.assetId] = asset2;
|
|
14609
14735
|
return;
|
|
14610
14736
|
}
|
|
14611
14737
|
const asset = Object.hasOwn(raw.assets, edit.assetId) ? raw.assets[edit.assetId] : void 0;
|
|
14612
|
-
if (!asset) throw
|
|
14738
|
+
if (!asset) throw undeclaredAsset(raw, edit.assetId);
|
|
14613
14739
|
const { patch } = edit;
|
|
14614
14740
|
if (patch.prompt !== void 0) asset.prompt = patch.prompt;
|
|
14615
14741
|
if (patch.promptForStyle) {
|
|
@@ -15223,6 +15349,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
15223
15349
|
}
|
|
15224
15350
|
async function buildGallerySnapshot(opts) {
|
|
15225
15351
|
const { loaded, specs, lock } = opts;
|
|
15352
|
+
const filter = opts.filter?.assets?.length ? { ...opts.filter, assets: expandAssetFilter(opts.filter.assets, loaded.loopFamilies) } : opts.filter;
|
|
15226
15353
|
const root = loaded.root;
|
|
15227
15354
|
const media = /* @__PURE__ */ new Map();
|
|
15228
15355
|
const plan = await buildPlan(specs, lock);
|
|
@@ -15367,7 +15494,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15367
15494
|
for (const [key, entry] of Object.entries(lock.entries)) {
|
|
15368
15495
|
if (shown.has(key)) continue;
|
|
15369
15496
|
if (isDeclared(loaded, entry.styleId, entry.assetId)) continue;
|
|
15370
|
-
if (!matchesFilter(
|
|
15497
|
+
if (!matchesFilter(filter, entry.styleId, entry.assetId)) continue;
|
|
15371
15498
|
const outputs = await Promise.all(
|
|
15372
15499
|
entry.outputs.map(
|
|
15373
15500
|
(output) => describeOutput(media, root, resolveOutputPath(output.path, root), output)
|
|
@@ -15533,7 +15660,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15533
15660
|
try {
|
|
15534
15661
|
const loaded = await loadManifest(manifestPath);
|
|
15535
15662
|
const styleFilter = filter.styles.filter((id) => loaded.manifest.styles[id]);
|
|
15536
|
-
const assetFilter = filter.assets.filter((id) => loaded.manifest.assets[id]);
|
|
15663
|
+
const assetFilter = expandAssetFilter(filter.assets, loaded.loopFamilies).filter((id) => loaded.manifest.assets[id]);
|
|
15537
15664
|
const excluded = filter.styles.length > 0 && styleFilter.length === 0 || filter.assets.length > 0 && assetFilter.length === 0;
|
|
15538
15665
|
const lock = await loadLock(lockPath);
|
|
15539
15666
|
let projectItems = [];
|
|
@@ -16635,6 +16762,9 @@ async function loadClaims(lockPaths, opts = {}) {
|
|
|
16635
16762
|
add(entry.objectId);
|
|
16636
16763
|
add(entry.reviewObjectId);
|
|
16637
16764
|
add(entry.jobId);
|
|
16765
|
+
for (const id of [entry.objectId, entry.jobId]) {
|
|
16766
|
+
if (id?.includes("#")) add(id.split("#")[0]);
|
|
16767
|
+
}
|
|
16638
16768
|
}
|
|
16639
16769
|
}
|
|
16640
16770
|
return claimed;
|
|
@@ -16658,6 +16788,54 @@ async function findOrphans(provider, claimed, opts = {}) {
|
|
|
16658
16788
|
})).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
16659
16789
|
return { orphans, total: all.length };
|
|
16660
16790
|
}
|
|
16791
|
+
function isState(character) {
|
|
16792
|
+
return Boolean(character.stateName && character.stateName !== "Idle");
|
|
16793
|
+
}
|
|
16794
|
+
function characterSalvageAssets(characters, ctx) {
|
|
16795
|
+
const ordered = [...characters].sort((a, b) => Number(isState(a)) - Number(isState(b)) || a.createdAt.localeCompare(b.createdAt));
|
|
16796
|
+
const base = ordered[0];
|
|
16797
|
+
const common = { styles: [ctx.styleId], tags: ["salvaged"] };
|
|
16798
|
+
const assets = {};
|
|
16799
|
+
const skipped = [];
|
|
16800
|
+
const baseId = idFromPrompt(base.name || base.prompt, ctx.taken);
|
|
16801
|
+
const size = base.width === base.height && base.width >= 32 && base.width <= 256 && base.width !== ctx.styleSize ? { size: base.width } : {};
|
|
16802
|
+
assets[baseId] = { prompt: base.prompt || base.name || baseId, remoteId: base.id, ...size, ...common };
|
|
16803
|
+
const assetIdOf = /* @__PURE__ */ new Map([[base.id, baseId]]);
|
|
16804
|
+
for (const state of ordered.slice(1)) {
|
|
16805
|
+
const stateId = uniqueId(`${baseId}_${slug(state.stateName ?? "state")}`, ctx.taken);
|
|
16806
|
+
assetIdOf.set(state.id, stateId);
|
|
16807
|
+
assets[stateId] = { prompt: state.prompt || state.stateName || stateId, state: { of: baseId }, remoteId: state.id, ...common };
|
|
16808
|
+
}
|
|
16809
|
+
for (const character of ordered) {
|
|
16810
|
+
const parentId = assetIdOf.get(character.id);
|
|
16811
|
+
for (const animation of character.animations) {
|
|
16812
|
+
if (!animation.frames.length) continue;
|
|
16813
|
+
const label = animation.name || animation.type;
|
|
16814
|
+
if (!animation.groupId) {
|
|
16815
|
+
skipped.push(`${parentId}: "${label}" ${animation.direction} loop has no animation group id to adopt it by`);
|
|
16816
|
+
continue;
|
|
16817
|
+
}
|
|
16818
|
+
const loopId = uniqueId(`${parentId}.${slug(label)}.${animation.direction}`, ctx.taken);
|
|
16819
|
+
assets[loopId] = {
|
|
16820
|
+
prompt: label,
|
|
16821
|
+
animation: { of: parentId, direction: animation.direction },
|
|
16822
|
+
remoteId: `${character.id}#${animation.groupId}`,
|
|
16823
|
+
...common
|
|
16824
|
+
};
|
|
16825
|
+
}
|
|
16826
|
+
}
|
|
16827
|
+
return { assets, skipped };
|
|
16828
|
+
}
|
|
16829
|
+
function slug(text) {
|
|
16830
|
+
return text.toLowerCase().replace(/^pixelkiln:.*\//, "").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "loop";
|
|
16831
|
+
}
|
|
16832
|
+
function uniqueId(base, taken) {
|
|
16833
|
+
let id = base;
|
|
16834
|
+
let n = 2;
|
|
16835
|
+
while (taken.has(id)) id = `${base}_${n++}`;
|
|
16836
|
+
taken.add(id);
|
|
16837
|
+
return id;
|
|
16838
|
+
}
|
|
16661
16839
|
var MIN_STYLE_FRAGMENT_LENGTH = 12;
|
|
16662
16840
|
function matchStyleByPattern(prompt, manifest) {
|
|
16663
16841
|
const p = prompt.toLowerCase();
|
|
@@ -16787,18 +16965,22 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
16787
16965
|
let tagged = 0;
|
|
16788
16966
|
let failed = 0;
|
|
16789
16967
|
for (const { id, action } of decisions) {
|
|
16790
|
-
const
|
|
16791
|
-
|
|
16792
|
-
)
|
|
16793
|
-
|
|
16794
|
-
|
|
16795
|
-
|
|
16796
|
-
|
|
16797
|
-
|
|
16798
|
-
tagged
|
|
16799
|
-
|
|
16800
|
-
|
|
16801
|
-
|
|
16968
|
+
const group = opts.characters?.get(id);
|
|
16969
|
+
const targets = group ?? [{ id, tags: existing.get(id) ?? [] }];
|
|
16970
|
+
for (const target of targets) {
|
|
16971
|
+
const current = target.tags.filter(
|
|
16972
|
+
(t) => t !== "pixelkiln:keep" && t !== "pixelkiln:discard" && t !== "pixelkiln:imported"
|
|
16973
|
+
);
|
|
16974
|
+
const decisionTag = `pixelkiln:${action === "import" ? "imported" : action}`;
|
|
16975
|
+
const next = [...current.slice(0, 19), decisionTag];
|
|
16976
|
+
if (!provider.setTags) return { tagged, failed };
|
|
16977
|
+
try {
|
|
16978
|
+
await provider.setTags(target.id, next, group ? "character" : void 0);
|
|
16979
|
+
tagged++;
|
|
16980
|
+
} catch (err) {
|
|
16981
|
+
failed++;
|
|
16982
|
+
log(` tag failed ${target.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
16983
|
+
}
|
|
16802
16984
|
}
|
|
16803
16985
|
}
|
|
16804
16986
|
return { tagged, failed };
|
|
@@ -16811,7 +16993,7 @@ var import_node_path34 = __toESM(require("path"), 1);
|
|
|
16811
16993
|
// src/pick/salvage-sheet.ts
|
|
16812
16994
|
var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
16813
16995
|
function renderSalvageSheet(orphans, ctx) {
|
|
16814
|
-
const safe = orphans.map((o) => ({ ...o, prompt: escapeHtml3(o.prompt) }));
|
|
16996
|
+
const safe = orphans.map((o) => ({ ...o, prompt: escapeHtml3(o.prompt), ...o.note ? { note: escapeHtml3(o.note) } : {} }));
|
|
16815
16997
|
const data = JSON.stringify(safe).replace(/<\//g, "<\\/");
|
|
16816
16998
|
const styleId = ctx ? escapeHtml3(ctx.styleId) : null;
|
|
16817
16999
|
const importDir = ctx ? escapeHtml3(ctx.importDir) : null;
|
|
@@ -16897,6 +17079,7 @@ ITEMS.forEach((o, i) => {
|
|
|
16897
17079
|
el.innerHTML =
|
|
16898
17080
|
'<div class="thumb"><img src="' + o.previewUrl + '" loading="lazy"></div>' +
|
|
16899
17081
|
'<div class="meta">' + o.width + '\xD7' + o.height + ' \xB7 ' + o.createdAt.slice(0,10) + '</div>' +
|
|
17082
|
+
(o.note ? '<div class="meta">' + o.note + '</div>' : '') +
|
|
16900
17083
|
'<div class="prompt">' + o.prompt + '</div>' +
|
|
16901
17084
|
'<div class="acts">' +
|
|
16902
17085
|
'<button data-a="import">import</button>' +
|
|
@@ -16975,6 +17158,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
16975
17158
|
});
|
|
16976
17159
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
16977
17160
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
17161
|
+
const characterMembers = new Map(
|
|
17162
|
+
orphans.flatMap((o) => o.kind === "character" && o.members ? [[o.id, o.members]] : [])
|
|
17163
|
+
);
|
|
16978
17164
|
return serveReviewPage({
|
|
16979
17165
|
html,
|
|
16980
17166
|
port: opts.port,
|
|
@@ -16991,10 +17177,34 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
16991
17177
|
const result = { imported: 0, kept: 0, discarded: 0, failed: 0 };
|
|
16992
17178
|
const taken = new Set(Object.keys(ctx.manifest.assets));
|
|
16993
17179
|
const importedAssetIds = [];
|
|
17180
|
+
const characterAssetIds = [];
|
|
16994
17181
|
for (const decision of decisions) {
|
|
16995
17182
|
const orphan = byId.get(decision.id);
|
|
16996
17183
|
if (!orphan) continue;
|
|
16997
|
-
if (decision.action === "import") {
|
|
17184
|
+
if (decision.action === "import" && orphan.kind === "character") {
|
|
17185
|
+
try {
|
|
17186
|
+
if (!provider.getCharacter) throw new Error(`provider ${provider.id} cannot read characters`);
|
|
17187
|
+
const details = [];
|
|
17188
|
+
for (const member of orphan.members ?? [{ id: orphan.id }]) details.push(await provider.getCharacter(member.id));
|
|
17189
|
+
const style = ctx.manifest.styles[ctx.styleId];
|
|
17190
|
+
const { assets, skipped } = characterSalvageAssets(details, {
|
|
17191
|
+
styleId: ctx.styleId,
|
|
17192
|
+
styleSize: style?.size,
|
|
17193
|
+
taken
|
|
17194
|
+
});
|
|
17195
|
+
for (const [assetId, asset] of Object.entries(assets)) {
|
|
17196
|
+
ctx.manifest.assets[assetId] = asset;
|
|
17197
|
+
importedAssetIds.push(assetId);
|
|
17198
|
+
characterAssetIds.push(assetId);
|
|
17199
|
+
}
|
|
17200
|
+
for (const line of skipped) log(` not imported: ${line}`);
|
|
17201
|
+
result.imported++;
|
|
17202
|
+
log(` imported character ${orphan.id} as ${Object.keys(assets).length} asset(s)`);
|
|
17203
|
+
} catch (err) {
|
|
17204
|
+
result.failed++;
|
|
17205
|
+
log(` import failed ${orphan.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
17206
|
+
}
|
|
17207
|
+
} else if (decision.action === "import") {
|
|
16998
17208
|
try {
|
|
16999
17209
|
const buf = await provider.download(orphan.previewUrl);
|
|
17000
17210
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
@@ -17058,11 +17268,12 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
17058
17268
|
result.discarded++;
|
|
17059
17269
|
}
|
|
17060
17270
|
}
|
|
17061
|
-
await applyTags(provider, decisions, existingTags, { onProgress: log });
|
|
17271
|
+
await applyTags(provider, decisions, existingTags, { onProgress: log, characters: characterMembers });
|
|
17062
17272
|
const raw = JSON.parse(await (0, import_promises27.readFile)(ctx.manifestPath, "utf8"));
|
|
17063
17273
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
17064
17274
|
await (0, import_promises27.writeFile)(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
17065
17275
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
17276
|
+
if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
|
|
17066
17277
|
return result;
|
|
17067
17278
|
}
|
|
17068
17279
|
});
|