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.js
CHANGED
|
@@ -193,6 +193,14 @@ var CharacterAnimationSchema = z.object({
|
|
|
193
193
|
template: z.string().min(1).optional(),
|
|
194
194
|
/** Direction to animate. */
|
|
195
195
|
direction: CharacterDirectionSchema.default("south"),
|
|
196
|
+
/**
|
|
197
|
+
* Shorthand for one loop in several directions, instead of `direction`.
|
|
198
|
+
* Expanded when the manifest loads into one loop per named direction
|
|
199
|
+
* (`<id>.<direction>`) plus a mirror for each direction whose flip is
|
|
200
|
+
* not named, exactly as if each had been written out. See
|
|
201
|
+
* `src/loop-directions.ts`.
|
|
202
|
+
*/
|
|
203
|
+
directions: z.array(CharacterDirectionSchema).min(1).optional(),
|
|
196
204
|
/** Frames to generate without a template (v3). Even, 4 to 16. */
|
|
197
205
|
frames: z.number().int().min(4).max(16).optional(),
|
|
198
206
|
/** Playback rate recorded with the frames; PixelLab does not store one. */
|
|
@@ -221,6 +229,13 @@ var CharacterAnimationSchema = z.object({
|
|
|
221
229
|
/** v3 text loops only: let PixelLab expand the action into a fuller motion description first. */
|
|
222
230
|
enhancePrompt: z.boolean().optional()
|
|
223
231
|
}).strict().superRefine((animation, context) => {
|
|
232
|
+
if (animation.directions !== void 0) {
|
|
233
|
+
context.addIssue({
|
|
234
|
+
code: z.ZodIssueCode.custom,
|
|
235
|
+
message: "directions is expanded when the manifest loads; validate the loaded manifest, not the raw file",
|
|
236
|
+
path: ["directions"]
|
|
237
|
+
});
|
|
238
|
+
}
|
|
224
239
|
if (animation.frames !== void 0 && animation.frames % 2 !== 0) {
|
|
225
240
|
context.addIssue({ code: z.ZodIssueCode.custom, message: "frames must be even", path: ["frames"] });
|
|
226
241
|
}
|
|
@@ -4719,8 +4734,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4719
4734
|
const sources = [];
|
|
4720
4735
|
const terrainTiles = [];
|
|
4721
4736
|
for (const [index, tile] of tileset.tiles.entries()) {
|
|
4722
|
-
const
|
|
4723
|
-
const role = `tile-${String(index).padStart(2, "0")}-${
|
|
4737
|
+
const slug2 = tile.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "tile";
|
|
4738
|
+
const role = `tile-${String(index).padStart(2, "0")}-${slug2}`;
|
|
4724
4739
|
const file = path.join(_PixelLabProvider.cacheDir(), `${tilesetId}-${index}.png`);
|
|
4725
4740
|
writeFileSync(file, Buffer.from(tile.image.base64, "base64"));
|
|
4726
4741
|
sources.push({ url: `file://${file}`, role });
|
|
@@ -4839,6 +4854,9 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4839
4854
|
async delete(assetId) {
|
|
4840
4855
|
await this.client.deleteObject(assetId);
|
|
4841
4856
|
}
|
|
4857
|
+
async deleteCharacter(id) {
|
|
4858
|
+
await this.client.deleteCharacter(id);
|
|
4859
|
+
}
|
|
4842
4860
|
async *listCharacters() {
|
|
4843
4861
|
for await (const character of this.client.iterateCharacters(100)) {
|
|
4844
4862
|
yield remoteCharacter(character);
|
|
@@ -6862,6 +6880,8 @@ var FakeProvider = class {
|
|
|
6862
6880
|
this.tags.set(objectId, [...tags]);
|
|
6863
6881
|
const asset = this.assets.get(objectId);
|
|
6864
6882
|
if (asset) asset.tags = [...tags];
|
|
6883
|
+
const character = this.characters.get(objectId.split("#")[0]);
|
|
6884
|
+
if (character) character.tags = [...tags];
|
|
6865
6885
|
}
|
|
6866
6886
|
/**
|
|
6867
6887
|
* Assigned in the constructor rather than declared as a method, so a test can
|
|
@@ -6896,6 +6916,10 @@ var FakeProvider = class {
|
|
|
6896
6916
|
this.deleted.push(assetId);
|
|
6897
6917
|
this.assets.delete(assetId);
|
|
6898
6918
|
}
|
|
6919
|
+
async deleteCharacter(id) {
|
|
6920
|
+
this.deleted.push(id);
|
|
6921
|
+
this.characters.delete(id);
|
|
6922
|
+
}
|
|
6899
6923
|
/** Seed a pre-existing remote asset, for adopt / salvage tests. */
|
|
6900
6924
|
seed(asset) {
|
|
6901
6925
|
const full = {
|
|
@@ -6992,6 +7016,91 @@ import { readFile as readFile3 } from "fs/promises";
|
|
|
6992
7016
|
import { existsSync as existsSync4 } from "fs";
|
|
6993
7017
|
import path5 from "path";
|
|
6994
7018
|
|
|
7019
|
+
// src/loop-directions.ts
|
|
7020
|
+
var SINGLE_OUTPUT_FIELDS = ["file", "cell", "remoteId", "source", "sourceByStyle", "outputRole"];
|
|
7021
|
+
var MIRROR_FIELDS = ["category", "tags", "styles"];
|
|
7022
|
+
function loopDirectionId(shorthandId, direction) {
|
|
7023
|
+
return `${shorthandId}.${direction}`;
|
|
7024
|
+
}
|
|
7025
|
+
function expandLoopDirections(raw) {
|
|
7026
|
+
const families = {};
|
|
7027
|
+
const issues = [];
|
|
7028
|
+
if (!isObject2(raw) || !isObject2(raw.assets)) return { raw, families, issues };
|
|
7029
|
+
const assets = raw.assets;
|
|
7030
|
+
if (!Object.values(assets).some((asset) => isObject2(asset) && isObject2(asset.animation) && "directions" in asset.animation)) {
|
|
7031
|
+
return { raw, families, issues };
|
|
7032
|
+
}
|
|
7033
|
+
const expanded = {};
|
|
7034
|
+
const claimed = new Set(Object.keys(assets));
|
|
7035
|
+
for (const [id, asset] of Object.entries(assets)) {
|
|
7036
|
+
if (!isObject2(asset) || !isObject2(asset.animation) || !("directions" in asset.animation)) {
|
|
7037
|
+
expanded[id] = asset;
|
|
7038
|
+
continue;
|
|
7039
|
+
}
|
|
7040
|
+
const where = `assets.${id}.animation.directions`;
|
|
7041
|
+
const { directions: rawDirections, ...animation } = asset.animation;
|
|
7042
|
+
const parsed = CharacterDirectionSchema.array().min(1).safeParse(rawDirections);
|
|
7043
|
+
if (!parsed.success) {
|
|
7044
|
+
issues.push(`${where}: expected a non-empty list of directions (${CharacterDirectionSchema.options.join(", ")})`);
|
|
7045
|
+
continue;
|
|
7046
|
+
}
|
|
7047
|
+
const directions = parsed.data;
|
|
7048
|
+
const duplicate = directions.find((direction, index) => directions.indexOf(direction) !== index);
|
|
7049
|
+
if (duplicate) {
|
|
7050
|
+
issues.push(`${where}: "${duplicate}" is listed twice`);
|
|
7051
|
+
continue;
|
|
7052
|
+
}
|
|
7053
|
+
if ("direction" in animation) {
|
|
7054
|
+
issues.push(`assets.${id}.animation: direction and directions are mutually exclusive; directions names every loop to make`);
|
|
7055
|
+
continue;
|
|
7056
|
+
}
|
|
7057
|
+
const single = SINGLE_OUTPUT_FIELDS.filter((field) => field in asset);
|
|
7058
|
+
if (single.length) {
|
|
7059
|
+
issues.push(
|
|
7060
|
+
`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`
|
|
7061
|
+
);
|
|
7062
|
+
continue;
|
|
7063
|
+
}
|
|
7064
|
+
const loops = directions.map((direction) => ({ id: loopDirectionId(id, direction), direction }));
|
|
7065
|
+
const named = new Set(directions);
|
|
7066
|
+
const mirrors = [];
|
|
7067
|
+
for (const { id: loopId, direction } of loops) {
|
|
7068
|
+
const flipped = MIRRORED_DIRECTION[direction];
|
|
7069
|
+
if (flipped === direction || named.has(flipped)) continue;
|
|
7070
|
+
named.add(flipped);
|
|
7071
|
+
mirrors.push({ id: loopDirectionId(id, flipped), of: loopId });
|
|
7072
|
+
}
|
|
7073
|
+
const ids = [...loops.map((loop) => loop.id), ...mirrors.map((mirror) => mirror.id)];
|
|
7074
|
+
const collision = ids.find((expandedId) => claimed.has(expandedId));
|
|
7075
|
+
if (collision) {
|
|
7076
|
+
issues.push(`${where}: expands to "${collision}", which is already declared`);
|
|
7077
|
+
continue;
|
|
7078
|
+
}
|
|
7079
|
+
for (const expandedId of ids) claimed.add(expandedId);
|
|
7080
|
+
for (const loop of loops) {
|
|
7081
|
+
expanded[loop.id] = { ...asset, animation: { ...animation, direction: loop.direction } };
|
|
7082
|
+
}
|
|
7083
|
+
for (const mirror of mirrors) {
|
|
7084
|
+
const inherited = {};
|
|
7085
|
+
for (const field of MIRROR_FIELDS) if (field in asset) inherited[field] = asset[field];
|
|
7086
|
+
expanded[mirror.id] = { mirror: mirror.of, ...inherited };
|
|
7087
|
+
}
|
|
7088
|
+
families[id] = ids;
|
|
7089
|
+
}
|
|
7090
|
+
return { raw: { ...raw, assets: expanded }, families, issues };
|
|
7091
|
+
}
|
|
7092
|
+
function expandAssetFilter(ids, families) {
|
|
7093
|
+
if (!families) return [...ids];
|
|
7094
|
+
return [...new Set(ids.flatMap((id) => families[id] ?? [id]))];
|
|
7095
|
+
}
|
|
7096
|
+
function loopShorthandOf(assetId, families) {
|
|
7097
|
+
if (!families) return void 0;
|
|
7098
|
+
return Object.keys(families).find((id) => families[id].includes(assetId));
|
|
7099
|
+
}
|
|
7100
|
+
function isObject2(value) {
|
|
7101
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7102
|
+
}
|
|
7103
|
+
|
|
6995
7104
|
// src/outputs.ts
|
|
6996
7105
|
import { existsSync as existsSync3 } from "fs";
|
|
6997
7106
|
import path4 from "path";
|
|
@@ -7310,7 +7419,12 @@ function frameSetFps(entry) {
|
|
|
7310
7419
|
async function loadManifest(manifestPath) {
|
|
7311
7420
|
const abs = path5.resolve(manifestPath);
|
|
7312
7421
|
if (!existsSync4(abs)) throw new ProjectError(`No manifest at ${abs}`);
|
|
7313
|
-
const
|
|
7422
|
+
const expansion = expandLoopDirections(JSON.parse(await readFile3(abs, "utf8")));
|
|
7423
|
+
if (expansion.issues.length) {
|
|
7424
|
+
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
7425
|
+
${expansion.issues.map((i) => ` ${i}`).join("\n")}`);
|
|
7426
|
+
}
|
|
7427
|
+
const input = ManifestInputSchema.safeParse(expansion.raw);
|
|
7314
7428
|
if (!input.success) {
|
|
7315
7429
|
const issues = formatManifestIssues(input.error.issues);
|
|
7316
7430
|
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
@@ -7395,7 +7509,8 @@ ${issues}`);
|
|
|
7395
7509
|
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
7396
7510
|
${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
7397
7511
|
}
|
|
7398
|
-
|
|
7512
|
+
const loopFamilies = Object.keys(expansion.families).length ? { loopFamilies: expansion.families } : {};
|
|
7513
|
+
return { manifest: parsed.data, root: path5.dirname(abs), path: abs, ...loopFamilies };
|
|
7399
7514
|
}
|
|
7400
7515
|
function parentAssetId(asset) {
|
|
7401
7516
|
return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of;
|
|
@@ -7479,12 +7594,13 @@ async function resolveSpecs(loaded, filter) {
|
|
|
7479
7594
|
);
|
|
7480
7595
|
}
|
|
7481
7596
|
}
|
|
7482
|
-
|
|
7597
|
+
const assetFilter = expandAssetFilter(filter?.assets ?? [], loaded.loopFamilies);
|
|
7598
|
+
for (const unknownAsset of assetFilter) {
|
|
7483
7599
|
if (!manifest.assets[unknownAsset]) {
|
|
7484
7600
|
throw new UsageError(`Unknown asset "${unknownAsset}".`);
|
|
7485
7601
|
}
|
|
7486
7602
|
}
|
|
7487
|
-
const requestedAssetIds = new Set(
|
|
7603
|
+
const requestedAssetIds = new Set(assetFilter.length ? assetFilter : Object.keys(manifest.assets));
|
|
7488
7604
|
const resolutionAssetIds = new Set(requestedAssetIds);
|
|
7489
7605
|
for (const assetId of [...requestedAssetIds]) {
|
|
7490
7606
|
let current = parentAssetId(manifest.assets[assetId]);
|
|
@@ -14208,11 +14324,21 @@ function assetStyles(raw, asset) {
|
|
|
14208
14324
|
const own = Array.isArray(asset.styles) ? asset.styles : [];
|
|
14209
14325
|
return own.length ? own : Object.keys(raw.styles ?? {});
|
|
14210
14326
|
}
|
|
14327
|
+
function declaredAssets(raw) {
|
|
14328
|
+
const expanded = expandLoopDirections(raw).raw;
|
|
14329
|
+
return new Set(Object.keys(expanded.assets ?? {}));
|
|
14330
|
+
}
|
|
14331
|
+
function undeclaredAsset(raw, assetId) {
|
|
14332
|
+
const shorthand = loopShorthandOf(assetId, expandLoopDirections(raw).families);
|
|
14333
|
+
return new ManifestEditError(
|
|
14334
|
+
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`
|
|
14335
|
+
);
|
|
14336
|
+
}
|
|
14211
14337
|
function applyEdit(raw, edit) {
|
|
14212
14338
|
raw.assets ??= {};
|
|
14213
14339
|
if (edit.action === "set-source" || edit.action === "clear-source") {
|
|
14214
14340
|
const asset2 = Object.hasOwn(raw.assets, edit.assetId) ? raw.assets[edit.assetId] : void 0;
|
|
14215
|
-
if (!asset2) throw
|
|
14341
|
+
if (!asset2) throw undeclaredAsset(raw, edit.assetId);
|
|
14216
14342
|
if (!raw.styles || !Object.hasOwn(raw.styles, edit.styleId)) {
|
|
14217
14343
|
throw new ManifestEditError(`unknown style "${edit.styleId}"`);
|
|
14218
14344
|
}
|
|
@@ -14264,7 +14390,7 @@ function applyEdit(raw, edit) {
|
|
|
14264
14390
|
return;
|
|
14265
14391
|
}
|
|
14266
14392
|
if (edit.action === "add-asset") {
|
|
14267
|
-
if (
|
|
14393
|
+
if (declaredAssets(raw).has(edit.assetId)) {
|
|
14268
14394
|
throw new ManifestEditError(`asset "${edit.assetId}" already exists`);
|
|
14269
14395
|
}
|
|
14270
14396
|
const { styles, tags, ...rest } = edit.asset;
|
|
@@ -14278,20 +14404,20 @@ function applyEdit(raw, edit) {
|
|
|
14278
14404
|
}
|
|
14279
14405
|
asset2.styles = styles;
|
|
14280
14406
|
}
|
|
14281
|
-
if (edit.asset.revision && !
|
|
14407
|
+
if (edit.asset.revision && !declaredAssets(raw).has(edit.asset.revision.from)) {
|
|
14282
14408
|
throw new ManifestEditError(`revision parent "${edit.asset.revision.from}" is not declared by the manifest`);
|
|
14283
14409
|
}
|
|
14284
|
-
if (edit.asset.state && !
|
|
14410
|
+
if (edit.asset.state && !declaredAssets(raw).has(edit.asset.state.of)) {
|
|
14285
14411
|
throw new ManifestEditError(`state parent "${edit.asset.state.of}" is not declared by the manifest`);
|
|
14286
14412
|
}
|
|
14287
|
-
if (edit.asset.animation && !
|
|
14413
|
+
if (edit.asset.animation && !declaredAssets(raw).has(edit.asset.animation.of)) {
|
|
14288
14414
|
throw new ManifestEditError(`animation parent "${edit.asset.animation.of}" is not declared by the manifest`);
|
|
14289
14415
|
}
|
|
14290
14416
|
raw.assets[edit.assetId] = asset2;
|
|
14291
14417
|
return;
|
|
14292
14418
|
}
|
|
14293
14419
|
const asset = Object.hasOwn(raw.assets, edit.assetId) ? raw.assets[edit.assetId] : void 0;
|
|
14294
|
-
if (!asset) throw
|
|
14420
|
+
if (!asset) throw undeclaredAsset(raw, edit.assetId);
|
|
14295
14421
|
const { patch } = edit;
|
|
14296
14422
|
if (patch.prompt !== void 0) asset.prompt = patch.prompt;
|
|
14297
14423
|
if (patch.promptForStyle) {
|
|
@@ -14905,6 +15031,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
14905
15031
|
}
|
|
14906
15032
|
async function buildGallerySnapshot(opts) {
|
|
14907
15033
|
const { loaded, specs, lock } = opts;
|
|
15034
|
+
const filter = opts.filter?.assets?.length ? { ...opts.filter, assets: expandAssetFilter(opts.filter.assets, loaded.loopFamilies) } : opts.filter;
|
|
14908
15035
|
const root = loaded.root;
|
|
14909
15036
|
const media = /* @__PURE__ */ new Map();
|
|
14910
15037
|
const plan = await buildPlan(specs, lock);
|
|
@@ -15049,7 +15176,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15049
15176
|
for (const [key, entry] of Object.entries(lock.entries)) {
|
|
15050
15177
|
if (shown.has(key)) continue;
|
|
15051
15178
|
if (isDeclared(loaded, entry.styleId, entry.assetId)) continue;
|
|
15052
|
-
if (!matchesFilter(
|
|
15179
|
+
if (!matchesFilter(filter, entry.styleId, entry.assetId)) continue;
|
|
15053
15180
|
const outputs = await Promise.all(
|
|
15054
15181
|
entry.outputs.map(
|
|
15055
15182
|
(output) => describeOutput(media, root, resolveOutputPath(output.path, root), output)
|
|
@@ -15215,7 +15342,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15215
15342
|
try {
|
|
15216
15343
|
const loaded = await loadManifest(manifestPath);
|
|
15217
15344
|
const styleFilter = filter.styles.filter((id) => loaded.manifest.styles[id]);
|
|
15218
|
-
const assetFilter = filter.assets.filter((id) => loaded.manifest.assets[id]);
|
|
15345
|
+
const assetFilter = expandAssetFilter(filter.assets, loaded.loopFamilies).filter((id) => loaded.manifest.assets[id]);
|
|
15219
15346
|
const excluded = filter.styles.length > 0 && styleFilter.length === 0 || filter.assets.length > 0 && assetFilter.length === 0;
|
|
15220
15347
|
const lock = await loadLock(lockPath);
|
|
15221
15348
|
let projectItems = [];
|
|
@@ -16317,6 +16444,9 @@ async function loadClaims(lockPaths, opts = {}) {
|
|
|
16317
16444
|
add(entry.objectId);
|
|
16318
16445
|
add(entry.reviewObjectId);
|
|
16319
16446
|
add(entry.jobId);
|
|
16447
|
+
for (const id of [entry.objectId, entry.jobId]) {
|
|
16448
|
+
if (id?.includes("#")) add(id.split("#")[0]);
|
|
16449
|
+
}
|
|
16320
16450
|
}
|
|
16321
16451
|
}
|
|
16322
16452
|
return claimed;
|
|
@@ -16340,6 +16470,54 @@ async function findOrphans(provider, claimed, opts = {}) {
|
|
|
16340
16470
|
})).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
16341
16471
|
return { orphans, total: all.length };
|
|
16342
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
|
+
}
|
|
16343
16521
|
var MIN_STYLE_FRAGMENT_LENGTH = 12;
|
|
16344
16522
|
function matchStyleByPattern(prompt, manifest) {
|
|
16345
16523
|
const p = prompt.toLowerCase();
|
|
@@ -16469,18 +16647,22 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
16469
16647
|
let tagged = 0;
|
|
16470
16648
|
let failed = 0;
|
|
16471
16649
|
for (const { id, action } of decisions) {
|
|
16472
|
-
const
|
|
16473
|
-
|
|
16474
|
-
)
|
|
16475
|
-
|
|
16476
|
-
|
|
16477
|
-
|
|
16478
|
-
|
|
16479
|
-
|
|
16480
|
-
tagged
|
|
16481
|
-
|
|
16482
|
-
|
|
16483
|
-
|
|
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
|
+
}
|
|
16484
16666
|
}
|
|
16485
16667
|
}
|
|
16486
16668
|
return { tagged, failed };
|
|
@@ -16493,7 +16675,7 @@ import path33 from "path";
|
|
|
16493
16675
|
// src/pick/salvage-sheet.ts
|
|
16494
16676
|
var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
16495
16677
|
function renderSalvageSheet(orphans, ctx) {
|
|
16496
|
-
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) } : {} }));
|
|
16497
16679
|
const data = JSON.stringify(safe).replace(/<\//g, "<\\/");
|
|
16498
16680
|
const styleId = ctx ? escapeHtml3(ctx.styleId) : null;
|
|
16499
16681
|
const importDir = ctx ? escapeHtml3(ctx.importDir) : null;
|
|
@@ -16579,6 +16761,7 @@ ITEMS.forEach((o, i) => {
|
|
|
16579
16761
|
el.innerHTML =
|
|
16580
16762
|
'<div class="thumb"><img src="' + o.previewUrl + '" loading="lazy"></div>' +
|
|
16581
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>' : '') +
|
|
16582
16765
|
'<div class="prompt">' + o.prompt + '</div>' +
|
|
16583
16766
|
'<div class="acts">' +
|
|
16584
16767
|
'<button data-a="import">import</button>' +
|
|
@@ -16657,6 +16840,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
16657
16840
|
});
|
|
16658
16841
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
16659
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
|
+
);
|
|
16660
16846
|
return serveReviewPage({
|
|
16661
16847
|
html,
|
|
16662
16848
|
port: opts.port,
|
|
@@ -16673,10 +16859,34 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
16673
16859
|
const result = { imported: 0, kept: 0, discarded: 0, failed: 0 };
|
|
16674
16860
|
const taken = new Set(Object.keys(ctx.manifest.assets));
|
|
16675
16861
|
const importedAssetIds = [];
|
|
16862
|
+
const characterAssetIds = [];
|
|
16676
16863
|
for (const decision of decisions) {
|
|
16677
16864
|
const orphan = byId.get(decision.id);
|
|
16678
16865
|
if (!orphan) continue;
|
|
16679
|
-
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") {
|
|
16680
16890
|
try {
|
|
16681
16891
|
const buf = await provider.download(orphan.previewUrl);
|
|
16682
16892
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
@@ -16740,11 +16950,12 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
16740
16950
|
result.discarded++;
|
|
16741
16951
|
}
|
|
16742
16952
|
}
|
|
16743
|
-
await applyTags(provider, decisions, existingTags, { onProgress: log });
|
|
16953
|
+
await applyTags(provider, decisions, existingTags, { onProgress: log, characters: characterMembers });
|
|
16744
16954
|
const raw = JSON.parse(await readFile26(ctx.manifestPath, "utf8"));
|
|
16745
16955
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
16746
16956
|
await writeFile13(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
16747
16957
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
16958
|
+
if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
|
|
16748
16959
|
return result;
|
|
16749
16960
|
}
|
|
16750
16961
|
});
|