pixelkiln 0.63.0 → 0.65.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 +624 -68
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.js +2 -0
- package/dist/index.cjs +488 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +567 -96
- package/dist/index.d.ts +567 -96
- package/dist/index.js +485 -49
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +20 -12
- package/docs/CHARACTERS.md +61 -0
- package/docs/CLI.md +6 -3
- package/docs/ENDPOINTS.md +26 -6
- package/docs/GENERATORS.md +10 -6
- package/docs/MANIFEST.md +3 -1
- package/docs/PIXELLAB.md +5 -4
- package/docs/RECOVERY.md +36 -3
- package/package.json +1 -1
- package/schema/manifest.schema.json +47 -0
- package/skills/pixelkiln/SKILL.md +2 -1
- package/skills/pixelkiln/references/pixellab-roadmap.md +5 -4
package/dist/cli.js
CHANGED
|
@@ -705,7 +705,16 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
705
705
|
styleTraits: spec.character.styleTraits,
|
|
706
706
|
reference: spec.character.reference ? Object.fromEntries(Object.entries(spec.character.reference).map(([direction, image]) => [direction, image.sha256])) : void 0,
|
|
707
707
|
concept: spec.character.concept?.sha256,
|
|
708
|
-
styleAnchor: spec.character.styleAnchor ? { of: spec.character.styleAnchor.assetId, sha256: spec.character.styleAnchor.sha256 } : void 0
|
|
708
|
+
styleAnchor: spec.character.styleAnchor ? { of: spec.character.styleAnchor.assetId, sha256: spec.character.styleAnchor.sha256 } : void 0,
|
|
709
|
+
// The parent's own bytes come from its recorded outputs; the plan
|
|
710
|
+
// compares those directly (like a mirror), so only the choice of
|
|
711
|
+
// parent, the reference image's bytes, and any extra guidance
|
|
712
|
+
// are identity here.
|
|
713
|
+
outfit: spec.character.outfit ? {
|
|
714
|
+
of: spec.character.parentAssetId,
|
|
715
|
+
reference: spec.character.outfit.reference.sha256,
|
|
716
|
+
additionalInstructions: spec.character.outfit.additionalInstructions
|
|
717
|
+
} : void 0
|
|
709
718
|
} : void 0,
|
|
710
719
|
// An objectPro family: same shape as character above, minus the
|
|
711
720
|
// fields it has none of (mode/template/proportions/isometric/concept/
|
|
@@ -949,6 +958,27 @@ var CharacterStateSchema = z.object({
|
|
|
949
958
|
height: z.number().int().min(16).max(256)
|
|
950
959
|
}).strict().optional()
|
|
951
960
|
}).strict();
|
|
961
|
+
var CharacterPortraitSizeSchema = z.union([
|
|
962
|
+
z.literal(16),
|
|
963
|
+
z.literal(32),
|
|
964
|
+
z.literal(48),
|
|
965
|
+
z.literal(64),
|
|
966
|
+
z.literal(128),
|
|
967
|
+
z.literal(160)
|
|
968
|
+
]);
|
|
969
|
+
var CharacterPortraitSchema = z.object({
|
|
970
|
+
/** The base or state to portray, in the same style. */
|
|
971
|
+
of: z.string().min(1),
|
|
972
|
+
size: CharacterPortraitSizeSchema.default(64)
|
|
973
|
+
}).strict();
|
|
974
|
+
var CharacterOutfitSchema = z.object({
|
|
975
|
+
/** The loop (a character animation asset) to re-clothe, in the same style. */
|
|
976
|
+
of: z.string().min(1),
|
|
977
|
+
/** Manifest-relative image of the outfit to transfer. */
|
|
978
|
+
reference: z.string().min(1),
|
|
979
|
+
/** Extra guidance PixelLab passes through as written, e.g. view/direction hints. */
|
|
980
|
+
additionalInstructions: z.string().max(2e3).optional()
|
|
981
|
+
}).strict();
|
|
952
982
|
var CharacterAnimationSchema = z.object({
|
|
953
983
|
/** The character to animate: a base or a state in the same style. */
|
|
954
984
|
of: z.string().min(1),
|
|
@@ -1570,6 +1600,10 @@ var AssetSchema = z.object({
|
|
|
1570
1600
|
pieces: z.array(UiPieceSchema).min(1).optional(),
|
|
1571
1601
|
/** `uiAsset` styles: named, auto-positioned UI element scaffolds. Combine with `pieces`; omit both for a default full-canvas panel. */
|
|
1572
1602
|
elements: z.array(UiElementSchema).min(1).optional(),
|
|
1603
|
+
/** `character` styles: this asset is a bust portrait of another character asset's south sprite. */
|
|
1604
|
+
portrait: CharacterPortraitSchema.optional(),
|
|
1605
|
+
/** `character` styles: this asset re-clothes an existing loop's frames with a reference outfit. */
|
|
1606
|
+
outfit: CharacterOutfitSchema.optional(),
|
|
1573
1607
|
/** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
|
|
1574
1608
|
proportions: CharacterProportionsSchema.optional(),
|
|
1575
1609
|
/**
|
|
@@ -1646,7 +1680,7 @@ var AssetSchema = z.object({
|
|
|
1646
1680
|
path: ["revision"]
|
|
1647
1681
|
});
|
|
1648
1682
|
}
|
|
1649
|
-
const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror", asset.batch && "batch"].filter(Boolean);
|
|
1683
|
+
const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror", asset.batch && "batch", asset.portrait && "portrait", asset.outfit && "outfit"].filter(Boolean);
|
|
1650
1684
|
if (shapes.length > 1) {
|
|
1651
1685
|
context.addIssue({
|
|
1652
1686
|
code: z.ZodIssueCode.custom,
|
|
@@ -1661,24 +1695,24 @@ var AssetSchema = z.object({
|
|
|
1661
1695
|
path: ["mirror"]
|
|
1662
1696
|
});
|
|
1663
1697
|
}
|
|
1664
|
-
if (asset.prompt === void 0 && !asset.mirror) {
|
|
1698
|
+
if (asset.prompt === void 0 && !asset.mirror && !asset.portrait && !asset.outfit) {
|
|
1665
1699
|
context.addIssue({
|
|
1666
1700
|
code: z.ZodIssueCode.custom,
|
|
1667
1701
|
message: "Required",
|
|
1668
1702
|
path: ["prompt"]
|
|
1669
1703
|
});
|
|
1670
1704
|
}
|
|
1671
|
-
if (asset.reference && (asset.state || asset.animation || asset.mirror)) {
|
|
1705
|
+
if (asset.reference && (asset.state || asset.animation || asset.mirror || asset.portrait || asset.outfit)) {
|
|
1672
1706
|
context.addIssue({
|
|
1673
1707
|
code: z.ZodIssueCode.custom,
|
|
1674
|
-
message: "a reference sprite belongs on a base; a state, animation, or
|
|
1708
|
+
message: "a reference sprite belongs on a base; a state, animation, mirror, portrait, or outfit takes its look from its parent",
|
|
1675
1709
|
path: ["reference"]
|
|
1676
1710
|
});
|
|
1677
1711
|
}
|
|
1678
|
-
if (asset.concept && (asset.state || asset.animation || asset.mirror)) {
|
|
1712
|
+
if (asset.concept && (asset.state || asset.animation || asset.mirror || asset.portrait || asset.outfit)) {
|
|
1679
1713
|
context.addIssue({
|
|
1680
1714
|
code: z.ZodIssueCode.custom,
|
|
1681
|
-
message: "a concept image belongs on a base; a state, animation, or
|
|
1715
|
+
message: "a concept image belongs on a base; a state, animation, mirror, portrait, or outfit takes its look from its parent",
|
|
1682
1716
|
path: ["concept"]
|
|
1683
1717
|
});
|
|
1684
1718
|
}
|
|
@@ -1803,6 +1837,17 @@ var LockEntrySchema = z.object({
|
|
|
1803
1837
|
leaderAssetId: z.string().min(1).optional(),
|
|
1804
1838
|
index: z.number().int().min(1).optional()
|
|
1805
1839
|
}).strict().nullable().default(null),
|
|
1840
|
+
/**
|
|
1841
|
+
* For an outfit: the loop it re-clothed, a hash over that loop's output
|
|
1842
|
+
* hashes when this was made, and the reference image's own hash. Tracked
|
|
1843
|
+
* the way `mirror` is, not through `specHash`: manifest resolution has no
|
|
1844
|
+
* lock to hash the parent's actual current frame bytes against.
|
|
1845
|
+
*/
|
|
1846
|
+
outfit: z.object({
|
|
1847
|
+
sourceAssetId: z.string().min(1),
|
|
1848
|
+
sourceSha256: z.string().regex(/^[0-9a-f]{64}$/),
|
|
1849
|
+
referenceSha256: z.string().regex(/^[0-9a-f]{64}$/)
|
|
1850
|
+
}).strict().nullable().default(null),
|
|
1806
1851
|
/**
|
|
1807
1852
|
* Output hashes owned by the previous generation while its replacement is
|
|
1808
1853
|
* pending. They authorize replacing only unchanged PixelKiln-owned files.
|
|
@@ -4080,6 +4125,7 @@ import { existsSync as existsSync4 } from "fs";
|
|
|
4080
4125
|
import path8 from "path";
|
|
4081
4126
|
async function inspectRevisionReadiness(spec, lock) {
|
|
4082
4127
|
if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
|
|
4128
|
+
if (spec.character?.kind === "outfit") return inspectOutfit(spec, lock, /* @__PURE__ */ new Set());
|
|
4083
4129
|
if (spec.character?.parentSpec) return inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
|
|
4084
4130
|
if (spec.character?.styleAnchor) return inspectStyleAnchor(spec, lock, /* @__PURE__ */ new Set());
|
|
4085
4131
|
if (spec.objectPro?.parentSpec) return inspectObjectProParent(spec, lock, /* @__PURE__ */ new Set());
|
|
@@ -4106,6 +4152,20 @@ async function inspectMirror(spec, lock, seen) {
|
|
|
4106
4152
|
const dependency = await inspectParent(source, lock, seen);
|
|
4107
4153
|
return dependency.ready ? { ready: true, reason: `${label} is current` } : { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
|
|
4108
4154
|
}
|
|
4155
|
+
async function inspectOutfit(spec, lock, seen) {
|
|
4156
|
+
const character = spec.character;
|
|
4157
|
+
const outfit = character.outfit;
|
|
4158
|
+
const label = `outfit source ${spec.styleId}/${character.parentAssetId}`;
|
|
4159
|
+
const dependency = await inspectParent(character.parentSpec, lock, seen);
|
|
4160
|
+
if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
|
|
4161
|
+
if (!existsSync4(outfit.reference.path)) {
|
|
4162
|
+
return { ready: false, reason: `outfit reference is missing: ${outfit.reference.path}` };
|
|
4163
|
+
}
|
|
4164
|
+
if (await sha256File(outfit.reference.path) !== outfit.reference.sha256) {
|
|
4165
|
+
return { ready: false, reason: "outfit reference changed after the manifest was resolved" };
|
|
4166
|
+
}
|
|
4167
|
+
return { ready: true, reason: `${label} is current` };
|
|
4168
|
+
}
|
|
4109
4169
|
async function inspectCharacterParent(spec, lock, seen) {
|
|
4110
4170
|
const character = spec.character;
|
|
4111
4171
|
const parentSpec = character.parentSpec;
|
|
@@ -4221,7 +4281,10 @@ async function inspectParent(spec, lock, seen) {
|
|
|
4221
4281
|
const inputs = await inspectRevision(spec, lock, nextSeen);
|
|
4222
4282
|
if (!inputs.ready) return inputs;
|
|
4223
4283
|
}
|
|
4224
|
-
if (spec.character?.
|
|
4284
|
+
if (spec.character?.kind === "outfit") {
|
|
4285
|
+
const inputs = await inspectOutfit(spec, lock, nextSeen);
|
|
4286
|
+
if (!inputs.ready) return inputs;
|
|
4287
|
+
} else if (spec.character?.parentSpec) {
|
|
4225
4288
|
const inputs = await inspectCharacterParent(spec, lock, nextSeen);
|
|
4226
4289
|
if (!inputs.ready) return inputs;
|
|
4227
4290
|
}
|
|
@@ -4344,6 +4407,9 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
4344
4407
|
} else if (spec.mirror && entry.status === "downloaded" && entry.outputs.some((_output, index) => !existsSync5(currentEntryOutputPath(entry, spec, index)))) {
|
|
4345
4408
|
state = "stale";
|
|
4346
4409
|
reason = "mirror output missing on disk; flip it again (no generation cost)";
|
|
4410
|
+
} else if (spec.character?.kind === "outfit" && entry.outfit?.sourceSha256 !== mirrorSourceHash(lock.entries[lockKey(spec.styleId, spec.character.parentAssetId)] ?? { outputs: [] })) {
|
|
4411
|
+
state = "stale";
|
|
4412
|
+
reason = `outfit source ${spec.character.parentAssetId} changed; re-clothe the loop again`;
|
|
4347
4413
|
} else if (entry.status === "download-failed") {
|
|
4348
4414
|
state = "recoverable";
|
|
4349
4415
|
const force = entry.error?.includes("pass --force") ? " --force" : "";
|
|
@@ -5760,6 +5826,28 @@ var AnimateObjectSubmitSchema = z2.object({
|
|
|
5760
5826
|
submissions: z2.array(DirectionSubmissionSchema),
|
|
5761
5827
|
usage: UsageSchema
|
|
5762
5828
|
}).passthrough();
|
|
5829
|
+
var PortraitSubmitSchema = z2.object({
|
|
5830
|
+
background_job_id: z2.string().min(1),
|
|
5831
|
+
status: z2.string().default("processing"),
|
|
5832
|
+
usage: UsageSchema
|
|
5833
|
+
}).passthrough();
|
|
5834
|
+
var PortraitJobSchema = z2.object({
|
|
5835
|
+
status: z2.string(),
|
|
5836
|
+
download_url: z2.string().nullable().optional(),
|
|
5837
|
+
width: z2.number().int().nullable().optional(),
|
|
5838
|
+
height: z2.number().int().nullable().optional()
|
|
5839
|
+
}).passthrough();
|
|
5840
|
+
var SetPortraitSchema = z2.object({
|
|
5841
|
+
character_id: z2.string().min(1),
|
|
5842
|
+
size: z2.number().int().positive(),
|
|
5843
|
+
url: z2.string().min(1),
|
|
5844
|
+
usage: UsageSchema
|
|
5845
|
+
}).passthrough();
|
|
5846
|
+
var TransferOutfitSubmitSchema = z2.object({
|
|
5847
|
+
background_job_id: z2.string().min(1),
|
|
5848
|
+
status: z2.string().default("processing"),
|
|
5849
|
+
usage: UsageSchema
|
|
5850
|
+
}).passthrough();
|
|
5763
5851
|
var CharacterAnimationDirectionSchema = z2.object({
|
|
5764
5852
|
direction: z2.string(),
|
|
5765
5853
|
frame_count: z2.number().int().min(0),
|
|
@@ -6375,6 +6463,69 @@ var PixelLabClient = class {
|
|
|
6375
6463
|
});
|
|
6376
6464
|
return validateResponse(AnimateSubmitSchema, raw, "animate-character");
|
|
6377
6465
|
}
|
|
6466
|
+
/**
|
|
6467
|
+
* A full-body sprite in, a bust portrait out (or the reverse, unused
|
|
6468
|
+
* today). Job-based like a state or `1dir`, not the couple-generations
|
|
6469
|
+
* "conversion" the name suggests: the smallest `resultSize` (16px) billed
|
|
6470
|
+
* 20 generations live (docs/ENDPOINTS.md, "Characters, measured").
|
|
6471
|
+
*/
|
|
6472
|
+
async createPortraitCharacterPro(args) {
|
|
6473
|
+
const raw = await this.request("/portrait-character-pro", {
|
|
6474
|
+
method: "POST",
|
|
6475
|
+
body: JSON.stringify({
|
|
6476
|
+
direction: args.direction,
|
|
6477
|
+
image: { type: "base64", base64: args.image.base64, format: args.image.format },
|
|
6478
|
+
...args.view ? { view: args.view } : {},
|
|
6479
|
+
...args.resultSize != null ? { result_size: args.resultSize } : {},
|
|
6480
|
+
...args.seed != null ? { seed: args.seed } : {}
|
|
6481
|
+
})
|
|
6482
|
+
});
|
|
6483
|
+
return validateResponse(PortraitSubmitSchema, raw, "portrait-character-pro");
|
|
6484
|
+
}
|
|
6485
|
+
/** Throws PixelLabError(423) while the job is still drawing, like tiles and map objects. */
|
|
6486
|
+
async getPortraitCharacterJob(jobId) {
|
|
6487
|
+
const raw = await this.request(`/portrait-character-pro/${encodeURIComponent(jobId)}`);
|
|
6488
|
+
return validateResponse(PortraitJobSchema, raw, "portrait-character-pro/{id}");
|
|
6489
|
+
}
|
|
6490
|
+
/**
|
|
6491
|
+
* Attaches a portrait image to a character record. Synchronous and free
|
|
6492
|
+
* (0 generations, live); PixelLab does not link this to
|
|
6493
|
+
* `createPortraitCharacterPro` automatically, and `GET /characters/{id}`
|
|
6494
|
+
* never grows a portrait field, so this response's `url` is the only place
|
|
6495
|
+
* it appears.
|
|
6496
|
+
*/
|
|
6497
|
+
async setCharacterPortrait(characterId, image) {
|
|
6498
|
+
const raw = await this.request(`/characters/${encodeURIComponent(characterId)}/portrait`, {
|
|
6499
|
+
method: "POST",
|
|
6500
|
+
body: JSON.stringify({ image: { type: "base64", base64: image.base64, format: image.format } })
|
|
6501
|
+
});
|
|
6502
|
+
return validateResponse(SetPortraitSchema, raw, "characters/{id}/portrait");
|
|
6503
|
+
}
|
|
6504
|
+
/**
|
|
6505
|
+
* Applies a reference outfit to 2–16 existing frames. Job-based like
|
|
6506
|
+
* everything else here, but the completed job returns the result frames
|
|
6507
|
+
* inline as base64 (`last_response.quantized_images`), not as storage
|
|
6508
|
+
* URLs — the one PixelLab character tool that works this way. A live
|
|
6509
|
+
* 2-frame, 92x92 job billed 20 generations (docs/ENDPOINTS.md).
|
|
6510
|
+
*/
|
|
6511
|
+
async transferOutfitV2(args) {
|
|
6512
|
+
const image = (img) => ({
|
|
6513
|
+
image: { type: "base64", base64: img.base64, format: img.format },
|
|
6514
|
+
size: { width: img.width, height: img.height }
|
|
6515
|
+
});
|
|
6516
|
+
const raw = await this.request("/transfer-outfit-v2", {
|
|
6517
|
+
method: "POST",
|
|
6518
|
+
body: JSON.stringify({
|
|
6519
|
+
reference_image: image(args.referenceImage),
|
|
6520
|
+
frames: args.frames.map(image),
|
|
6521
|
+
image_size: args.imageSize,
|
|
6522
|
+
...args.seed != null ? { seed: args.seed } : {},
|
|
6523
|
+
...args.noBackground !== void 0 ? { no_background: args.noBackground } : {},
|
|
6524
|
+
...args.additionalInstructions ? { additional_instructions: args.additionalInstructions } : {}
|
|
6525
|
+
})
|
|
6526
|
+
});
|
|
6527
|
+
return validateResponse(TransferOutfitSubmitSchema, raw, "transfer-outfit-v2");
|
|
6528
|
+
}
|
|
6378
6529
|
async getCharacter(characterId) {
|
|
6379
6530
|
const raw = await this.request(`/characters/${encodeURIComponent(characterId)}`);
|
|
6380
6531
|
return validateResponse(CharacterSchema, raw, "characters/{id}");
|
|
@@ -6794,7 +6945,10 @@ function characterCost(spec) {
|
|
|
6794
6945
|
if (animation.mode === "v3") return Math.max(1, Math.ceil(px * animation.frames / 65536));
|
|
6795
6946
|
return generationCost(spec.width, spec.height, "1dir");
|
|
6796
6947
|
}
|
|
6797
|
-
if (character.kind === "state") return generationCost(spec.width, spec.height, "1dir");
|
|
6948
|
+
if (character.kind === "state" || character.kind === "portrait") return generationCost(spec.width, spec.height, "1dir");
|
|
6949
|
+
if (character.kind === "outfit") {
|
|
6950
|
+
return 20;
|
|
6951
|
+
}
|
|
6798
6952
|
if (character.mode === "standard") return 1;
|
|
6799
6953
|
if (character.mode === "v3") return 1 + Math.max(1, Math.ceil(px * 8 / 65536));
|
|
6800
6954
|
if (character.mode === "pro-flash") {
|
|
@@ -6911,15 +7065,18 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6911
7065
|
return { spacingMs: 2500, maxInFlight: 8 };
|
|
6912
7066
|
}
|
|
6913
7067
|
/**
|
|
6914
|
-
* Where synchronous pixflux results
|
|
7068
|
+
* Where synchronous pixflux results (or an outfit transfer's inline
|
|
7069
|
+
* frames) are parked between `submit`/poll and `fetch`.
|
|
6915
7070
|
*
|
|
6916
|
-
* pixflux returns the PNG inline rather than a job id,
|
|
6917
|
-
*
|
|
6918
|
-
*
|
|
6919
|
-
*
|
|
7071
|
+
* pixflux returns the PNG inline rather than a job id, and an outfit
|
|
7072
|
+
* transfer's completed job returns its frames inline too, but the
|
|
7073
|
+
* pipeline is built around submit → poll → fetch running as separate
|
|
7074
|
+
* commands. Writing the bytes to a known path keeps that model intact:
|
|
7075
|
+
* the id is the filename, polling is an existence check, and downloading
|
|
7076
|
+
* is a file read.
|
|
6920
7077
|
*/
|
|
6921
|
-
static cacheDir() {
|
|
6922
|
-
const dir = path12.join(os.tmpdir(),
|
|
7078
|
+
static cacheDir(namespace = "pixflux") {
|
|
7079
|
+
const dir = path12.join(os.tmpdir(), `pixelkiln-${namespace}`);
|
|
6923
7080
|
mkdirSync(dir, { recursive: true });
|
|
6924
7081
|
return dir;
|
|
6925
7082
|
}
|
|
@@ -7179,6 +7336,12 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7179
7336
|
const character = spec.character;
|
|
7180
7337
|
if (!character) throw new Error(`${spec.styleId}/${spec.assetId} has no character shape`);
|
|
7181
7338
|
const label = `${spec.styleId}/${spec.assetId}`;
|
|
7339
|
+
if (character.kind === "outfit") {
|
|
7340
|
+
const reference = character.outfit.reference;
|
|
7341
|
+
if (reference.width < 32 || reference.width > 256 || reference.height < 32 || reference.height > 256) {
|
|
7342
|
+
throw new Error(`${label}: outfit reference is ${reference.width}x${reference.height}; PixelLab transfer-outfit-v2 takes 32 to 256px per side`);
|
|
7343
|
+
}
|
|
7344
|
+
}
|
|
7182
7345
|
if (character.kind === "base" && character.proportions !== void 0) {
|
|
7183
7346
|
if (character.mode !== "standard") {
|
|
7184
7347
|
throw new Error(`${label}: proportions apply to standard bases; the ${character.mode} engine has none to set`);
|
|
@@ -7190,7 +7353,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7190
7353
|
if (character.kind === "base" && character.enhancePrompt !== void 0 && character.mode !== "v3") {
|
|
7191
7354
|
throw new Error(`${label}: enhancePrompt applies to v3 bases; the ${character.mode} engine does not take it`);
|
|
7192
7355
|
}
|
|
7193
|
-
if (character.animation) {
|
|
7356
|
+
if (character.animation && character.kind !== "outfit") {
|
|
7194
7357
|
const { startFrame, endFrame } = character.animation;
|
|
7195
7358
|
for (const [name, image] of [["start frame", startFrame], ["end frame", endFrame]]) {
|
|
7196
7359
|
if (image && (image.width > 256 || image.height > 256)) {
|
|
@@ -7271,9 +7434,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7271
7434
|
if (character.styleTraits && character.mode !== "pro-flash") {
|
|
7272
7435
|
throw new Error(`${label}: styleTraits choose what a pro-flash style image lends; the ${character.mode} engine does not take them`);
|
|
7273
7436
|
}
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7437
|
+
if (character.kind !== "portrait") {
|
|
7438
|
+
const maxSize = character.mode === "v3" || character.mode === "pro-flash" ? 256 : 128;
|
|
7439
|
+
if (spec.width < 16 || spec.height < 16 || spec.width > maxSize || spec.height > maxSize) {
|
|
7440
|
+
throw new Error(`PixelLab ${character.mode} characters must be between 16 and ${maxSize} pixels`);
|
|
7441
|
+
}
|
|
7277
7442
|
}
|
|
7278
7443
|
if (character.mode === "pro-flash" && character.kind === "base" && (spec.width % 4 || spec.height % 4)) {
|
|
7279
7444
|
throw new Error(`PixelLab pro-flash characters are sized in multiples of 4 pixels; ${spec.width}x${spec.height} is not`);
|
|
@@ -7282,9 +7447,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7282
7447
|
if (!templates.includes(character.template)) {
|
|
7283
7448
|
throw new Error(`PixelLab ${character.mode} character template must be one of: ${templates.join(", ")}`);
|
|
7284
7449
|
}
|
|
7285
|
-
const views = character.mode === "standard" ? ["low top-down", "high top-down", "side", "perspective", "oblique"] : ["low top-down", "high top-down", "side"];
|
|
7450
|
+
const views = character.kind === "portrait" ? ["low top-down", "high top-down", "side"] : character.mode === "standard" ? ["low top-down", "high top-down", "side", "perspective", "oblique"] : ["low top-down", "high top-down", "side"];
|
|
7286
7451
|
if (!views.includes(spec.view)) {
|
|
7287
|
-
throw new Error(
|
|
7452
|
+
throw new Error(
|
|
7453
|
+
character.kind === "portrait" ? `PixelLab portrait view must be one of: ${views.join(", ")}` : `PixelLab ${character.mode} character view must be one of: ${views.join(", ")}`
|
|
7454
|
+
);
|
|
7288
7455
|
}
|
|
7289
7456
|
if (spec.view === "oblique" && character.directions !== 4) {
|
|
7290
7457
|
throw new Error("PixelLab oblique characters are 4-direction only");
|
|
@@ -7292,7 +7459,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7292
7459
|
if (character.kind !== "base" && !character.parentAssetId) {
|
|
7293
7460
|
throw new Error(`${spec.styleId}/${spec.assetId} is a ${character.kind} with no parent`);
|
|
7294
7461
|
}
|
|
7295
|
-
if (character.animation) {
|
|
7462
|
+
if (character.animation && character.kind !== "outfit") {
|
|
7296
7463
|
const allowed = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
|
|
7297
7464
|
if (!allowed.includes(character.animation.direction)) {
|
|
7298
7465
|
throw new Error(
|
|
@@ -7398,6 +7565,43 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7398
7565
|
});
|
|
7399
7566
|
return { jobId: res2.character_id, metadata: { character: { kind: "base", characterId: res2.character_id, mode: character.mode, directions: character.directions, backgroundJobId: res2.background_job_id } } };
|
|
7400
7567
|
}
|
|
7568
|
+
if (character.kind === "outfit") {
|
|
7569
|
+
const parentOutputs = context?.parentOutputs ?? [];
|
|
7570
|
+
if (parentOutputs.length < 2) {
|
|
7571
|
+
throw new Error(
|
|
7572
|
+
`${spec.styleId}/${spec.assetId} is an outfit of ${character.parentAssetId}, which has ${parentOutputs.length} downloaded frame(s); PixelLab transfer-outfit-v2 needs 2 to 16`
|
|
7573
|
+
);
|
|
7574
|
+
}
|
|
7575
|
+
const frames = parentOutputs.slice(0, 16).map((output) => {
|
|
7576
|
+
const bytes = readFileSync2(output.path);
|
|
7577
|
+
const dims = imageMetadata(bytes);
|
|
7578
|
+
if (!dims) throw new Error(`${spec.styleId}/${spec.assetId}: outfit source frame is not a readable PNG: ${output.path}`);
|
|
7579
|
+
return { base64: bytes.toString("base64"), format: "png", width: dims.width, height: dims.height };
|
|
7580
|
+
});
|
|
7581
|
+
const outfit = character.outfit;
|
|
7582
|
+
const referenceBytes = readFileSync2(outfit.reference.path);
|
|
7583
|
+
if (sha256(referenceBytes) !== outfit.reference.sha256) {
|
|
7584
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: outfit reference changed after the manifest was resolved: ${outfit.reference.path}`);
|
|
7585
|
+
}
|
|
7586
|
+
const res2 = await this.client.transferOutfitV2({
|
|
7587
|
+
referenceImage: { base64: referenceBytes.toString("base64"), format: "png", width: outfit.reference.width, height: outfit.reference.height },
|
|
7588
|
+
frames,
|
|
7589
|
+
imageSize: { width: spec.width, height: spec.height },
|
|
7590
|
+
seed: spec.seed,
|
|
7591
|
+
noBackground: spec.noBackground,
|
|
7592
|
+
additionalInstructions: outfit.additionalInstructions
|
|
7593
|
+
});
|
|
7594
|
+
return {
|
|
7595
|
+
jobId: res2.background_job_id,
|
|
7596
|
+
metadata: {
|
|
7597
|
+
character: {
|
|
7598
|
+
kind: "outfit",
|
|
7599
|
+
backgroundJobId: res2.background_job_id,
|
|
7600
|
+
frameRoles: parentOutputs.slice(0, 16).map((output) => output.role ?? null)
|
|
7601
|
+
}
|
|
7602
|
+
}
|
|
7603
|
+
};
|
|
7604
|
+
}
|
|
7401
7605
|
const parentId = context?.parentObjectId;
|
|
7402
7606
|
if (!parentId) {
|
|
7403
7607
|
throw new Error(
|
|
@@ -7416,6 +7620,24 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7416
7620
|
});
|
|
7417
7621
|
return { jobId: res2.character_id, metadata: { character: { kind: "state", characterId: res2.character_id, parentCharacterId: parentId, mode: character.mode, directions: character.directions, backgroundJobId: res2.background_job_id } } };
|
|
7418
7622
|
}
|
|
7623
|
+
if (character.kind === "portrait") {
|
|
7624
|
+
const parentFile = character.parentFile;
|
|
7625
|
+
const bytes = readFileSync2(parentFile);
|
|
7626
|
+
if (character.parentSha256 && sha256(bytes) !== character.parentSha256) {
|
|
7627
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: portrait source changed after the manifest was resolved: ${parentFile}`);
|
|
7628
|
+
}
|
|
7629
|
+
const res2 = await this.client.createPortraitCharacterPro({
|
|
7630
|
+
direction: "character_to_portrait",
|
|
7631
|
+
image: { base64: bytes.toString("base64"), format: "png" },
|
|
7632
|
+
view: spec.view,
|
|
7633
|
+
resultSize: character.portrait.size,
|
|
7634
|
+
seed: spec.seed
|
|
7635
|
+
});
|
|
7636
|
+
return {
|
|
7637
|
+
jobId: res2.background_job_id,
|
|
7638
|
+
metadata: { character: { kind: "portrait", parentCharacterId: parentId, backgroundJobId: res2.background_job_id } }
|
|
7639
|
+
};
|
|
7640
|
+
}
|
|
7419
7641
|
const animation = character.animation;
|
|
7420
7642
|
const name = characterAnimationName(spec);
|
|
7421
7643
|
const replaced = context?.replacedMetadata?.character ?? {};
|
|
@@ -7529,6 +7751,9 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7529
7751
|
async pollCharacter(jobId, context) {
|
|
7530
7752
|
const animation = decodeAnimationJob(jobId);
|
|
7531
7753
|
if (animation) return this.pollCharacterAnimation(animation, context);
|
|
7754
|
+
const kind = context?.metadata?.character?.kind ?? context?.spec?.character?.kind;
|
|
7755
|
+
if (kind === "portrait") return this.pollCharacterPortrait(jobId, context);
|
|
7756
|
+
if (kind === "outfit") return this.pollCharacterOutfit(jobId, context);
|
|
7532
7757
|
const character = await this.client.getCharacter(jobId);
|
|
7533
7758
|
if (character.status === "failed") return { status: "failed", error: "character generation failed upstream" };
|
|
7534
7759
|
if (character.status !== "completed" || !character.rotation_urls) return { status: "processing" };
|
|
@@ -7554,6 +7779,66 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7554
7779
|
billed: await this.billedForJob(backgroundJobId)
|
|
7555
7780
|
};
|
|
7556
7781
|
}
|
|
7782
|
+
/**
|
|
7783
|
+
* `jobId` here is `portrait-character-pro`'s own background job id, not a
|
|
7784
|
+
* character id: the endpoint converts an arbitrary image and never touches
|
|
7785
|
+
* the character record. Once the portrait itself is drawn, this also
|
|
7786
|
+
* attaches it to the parent's character record with `SetPortrait` — a
|
|
7787
|
+
* separate, free call PixelLab does not make automatically (docs/PIXELLAB.md).
|
|
7788
|
+
*/
|
|
7789
|
+
async pollCharacterPortrait(jobId, context) {
|
|
7790
|
+
const meta = context?.metadata?.character;
|
|
7791
|
+
try {
|
|
7792
|
+
const job = await this.client.getPortraitCharacterJob(jobId);
|
|
7793
|
+
if (job.status === "failed") return { status: "failed", error: "portrait generation failed upstream" };
|
|
7794
|
+
if (job.status !== "completed" || !job.download_url) return { status: "processing" };
|
|
7795
|
+
if (meta?.parentCharacterId) {
|
|
7796
|
+
const bytes = await this.client.download(job.download_url);
|
|
7797
|
+
await this.client.setCharacterPortrait(meta.parentCharacterId, { base64: bytes.toString("base64"), format: "png" });
|
|
7798
|
+
}
|
|
7799
|
+
return {
|
|
7800
|
+
status: "ready",
|
|
7801
|
+
objectId: jobId,
|
|
7802
|
+
sourceUrl: job.download_url,
|
|
7803
|
+
sources: [{ url: job.download_url }],
|
|
7804
|
+
billed: await this.billedForJob(meta?.backgroundJobId)
|
|
7805
|
+
};
|
|
7806
|
+
} catch (err) {
|
|
7807
|
+
if (err instanceof PixelLabError && err.status === 423) return { status: "processing" };
|
|
7808
|
+
throw err;
|
|
7809
|
+
}
|
|
7810
|
+
}
|
|
7811
|
+
/**
|
|
7812
|
+
* No dedicated status endpoint exists for `transfer-outfit-v2`, unlike
|
|
7813
|
+
* portraits and tiles: the background job is the only place to poll. A
|
|
7814
|
+
* completed job returns its frames inline as base64
|
|
7815
|
+
* (`last_response.quantized_images`), not as storage URLs, so they are
|
|
7816
|
+
* cached locally the way a synchronous pixflux result is, and matched back
|
|
7817
|
+
* to the source frames' own roles recorded at submit time.
|
|
7818
|
+
*/
|
|
7819
|
+
async pollCharacterOutfit(jobId, context) {
|
|
7820
|
+
const meta = context?.metadata?.character;
|
|
7821
|
+
const job = await this.client.getBackgroundJob(jobId);
|
|
7822
|
+
if (job.status === "failed") return { status: "failed", error: "outfit transfer failed upstream" };
|
|
7823
|
+
if (job.status !== "completed") return { status: "processing" };
|
|
7824
|
+
const images = job.last_response?.quantized_images;
|
|
7825
|
+
if (!images?.length) return { status: "failed", error: "outfit transfer completed with no result frames" };
|
|
7826
|
+
const dir = _PixelLabProvider.cacheDir("outfit");
|
|
7827
|
+
const sources = images.map((image, index) => {
|
|
7828
|
+
if (!image.base64) throw new Error(`outfit transfer frame ${index} has no image data`);
|
|
7829
|
+
const role = meta?.frameRoles?.[index] ?? void 0;
|
|
7830
|
+
const file = path12.join(dir, `${jobId}-${index}.png`);
|
|
7831
|
+
writeFileSync(file, Buffer.from(image.base64, "base64"));
|
|
7832
|
+
return { url: `file://${file}`, ...role ? { role } : {} };
|
|
7833
|
+
});
|
|
7834
|
+
return {
|
|
7835
|
+
status: "ready",
|
|
7836
|
+
objectId: jobId,
|
|
7837
|
+
sourceUrl: sources[0].url,
|
|
7838
|
+
sources,
|
|
7839
|
+
billed: await this.billedForJob(meta?.backgroundJobId)
|
|
7840
|
+
};
|
|
7841
|
+
}
|
|
7557
7842
|
/**
|
|
7558
7843
|
* The background job is the authoritative record of one direction: it
|
|
7559
7844
|
* completes with the frame URLs, the animation id, and the direction, and
|
|
@@ -8373,8 +8658,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
8373
8658
|
const sources = [];
|
|
8374
8659
|
const terrainTiles = [];
|
|
8375
8660
|
for (const [index, tile] of tileset.tiles.entries()) {
|
|
8376
|
-
const
|
|
8377
|
-
const role = `tile-${String(index).padStart(2, "0")}-${
|
|
8661
|
+
const slug2 = tile.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "tile";
|
|
8662
|
+
const role = `tile-${String(index).padStart(2, "0")}-${slug2}`;
|
|
8378
8663
|
const file = path12.join(_PixelLabProvider.cacheDir(), `${tilesetId}-${index}.png`);
|
|
8379
8664
|
writeFileSync(file, Buffer.from(tile.image.base64, "base64"));
|
|
8380
8665
|
sources.push({ url: `file://${file}`, role });
|
|
@@ -8493,6 +8778,9 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
8493
8778
|
async delete(assetId) {
|
|
8494
8779
|
await this.client.deleteObject(assetId);
|
|
8495
8780
|
}
|
|
8781
|
+
async deleteCharacter(id) {
|
|
8782
|
+
await this.client.deleteCharacter(id);
|
|
8783
|
+
}
|
|
8496
8784
|
async *listCharacters() {
|
|
8497
8785
|
for await (const character of this.client.iterateCharacters(100)) {
|
|
8498
8786
|
yield remoteCharacter(character);
|
|
@@ -9748,7 +10036,7 @@ ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
|
9748
10036
|
return { manifest: parsed.data, root: path13.dirname(abs), path: abs, ...loopFamilies };
|
|
9749
10037
|
}
|
|
9750
10038
|
function parentAssetId(asset) {
|
|
9751
|
-
return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of;
|
|
10039
|
+
return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of ?? asset?.portrait?.of ?? asset?.outfit?.of;
|
|
9752
10040
|
}
|
|
9753
10041
|
function formatManifestIssues(issues) {
|
|
9754
10042
|
return issues.flatMap((issue) => {
|
|
@@ -9915,9 +10203,9 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9915
10203
|
height = styleImageDimensions.length ? Math.max(...styleImageDimensions.map((img) => img.height)) : style.tileSize ?? 32;
|
|
9916
10204
|
size = Math.max(width, height);
|
|
9917
10205
|
} else if (generator === "character") {
|
|
9918
|
-
size = asset.state?.canvas ? Math.max(asset.state.canvas.width, asset.state.canvas.height) : asset.size ?? style.size ?? 64;
|
|
9919
|
-
width = asset.state?.canvas?.width ?? size;
|
|
9920
|
-
height = asset.state?.canvas?.height ?? size;
|
|
10206
|
+
size = asset.portrait ? asset.portrait.size : asset.state?.canvas ? Math.max(asset.state.canvas.width, asset.state.canvas.height) : asset.size ?? style.size ?? 64;
|
|
10207
|
+
width = asset.portrait?.size ?? asset.state?.canvas?.width ?? size;
|
|
10208
|
+
height = asset.portrait?.size ?? asset.state?.canvas?.height ?? size;
|
|
9921
10209
|
} else if (generator === "terrain") {
|
|
9922
10210
|
size = style.terrainTileSize ?? 16;
|
|
9923
10211
|
width = size;
|
|
@@ -9944,12 +10232,17 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9944
10232
|
`assets.${assetId}: ${asset.state ? "state" : "animation"} needs a character or objectPro style; "${styleId}" generates ${generator}`
|
|
9945
10233
|
);
|
|
9946
10234
|
}
|
|
10235
|
+
if ((asset.portrait || asset.outfit) && generator !== "character") {
|
|
10236
|
+
throw new Error(
|
|
10237
|
+
`assets.${assetId}: ${asset.portrait ? "portrait" : "outfit"} needs a character style; "${styleId}" generates ${generator}`
|
|
10238
|
+
);
|
|
10239
|
+
}
|
|
9947
10240
|
if ((asset.pieces || asset.elements) && generator !== "uiAsset") {
|
|
9948
10241
|
throw new Error(
|
|
9949
10242
|
`assets.${assetId}: ${asset.pieces ? "pieces" : "elements"} needs a uiAsset style; "${styleId}" generates ${generator}`
|
|
9950
10243
|
);
|
|
9951
10244
|
}
|
|
9952
|
-
const characterKind = asset.animation ? "animation" : asset.state ? "state" : "base";
|
|
10245
|
+
const characterKind = asset.animation ? "animation" : asset.state ? "state" : asset.portrait ? "portrait" : "base";
|
|
9953
10246
|
const subject = asset.promptByStyle[styleId] ?? asset.prompt ?? "";
|
|
9954
10247
|
let terrainLowerDescription;
|
|
9955
10248
|
let terrainUpperDescription;
|
|
@@ -10041,7 +10334,9 @@ async function resolveSpecs(loaded, filter) {
|
|
|
10041
10334
|
// its parent's pixels, not a new character: giving it a base shape
|
|
10042
10335
|
// would have packing, the gallery, and adoption treat it as one.
|
|
10043
10336
|
...generator === "character" && !asset.revision ? { character: await resolveCharacterShape(asset, style, characterKind, { root, load: loadStyleImage }) } : {},
|
|
10044
|
-
|
|
10337
|
+
// A portrait needs a character style (refused above), so an
|
|
10338
|
+
// objectPro asset is only ever a base, state, or animation.
|
|
10339
|
+
...generator === "objectPro" && !asset.revision && characterKind !== "portrait" ? { objectPro: await resolveObjectProShape(assetId, asset, style, characterKind, { root, load: loadStyleImage }) } : {},
|
|
10045
10340
|
cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? tilesCost(size, terrainTiles) : generator === "isometricTile" ? isometricTileCost() : generationCost(width, height, generator),
|
|
10046
10341
|
costUnit: "generations",
|
|
10047
10342
|
candidates: generator === "tiles" ? tileVariations : generator === "terrain" ? terrainTiles : generator === "1dir" || generator === "imagePro" ? candidateCount(size) : 1
|
|
@@ -10152,7 +10447,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
10152
10447
|
styleAnchor: { assetId: anchorId, spec: anchorSpec, file, sha256: existsSync8(file) ? await sha256File(file) : null }
|
|
10153
10448
|
};
|
|
10154
10449
|
}
|
|
10155
|
-
const characterParent = asset.state?.of ?? asset.animation?.of;
|
|
10450
|
+
const characterParent = asset.state?.of ?? asset.animation?.of ?? asset.portrait?.of;
|
|
10156
10451
|
if (resolved.character && characterParent) {
|
|
10157
10452
|
const parentSpec = await finalize(characterParent);
|
|
10158
10453
|
const parentDirections = parentSpec.character?.directions ?? 8;
|
|
@@ -10185,6 +10480,38 @@ async function resolveSpecs(loaded, filter) {
|
|
|
10185
10480
|
directions: resolved.objectPro.kind === "base" ? resolved.objectPro.directions : parentSpec.objectPro.directions
|
|
10186
10481
|
};
|
|
10187
10482
|
}
|
|
10483
|
+
if (asset.outfit) {
|
|
10484
|
+
if (asset.outfit.of === assetId) throw new Error(`assets.${assetId}: an asset cannot re-clothe itself`);
|
|
10485
|
+
const sourceSpec = await finalize(asset.outfit.of);
|
|
10486
|
+
if (sourceSpec.character?.kind !== "animation") {
|
|
10487
|
+
throw new Error(`assets.${assetId}: outfit "${asset.outfit.of}" is not a character loop`);
|
|
10488
|
+
}
|
|
10489
|
+
const referenceImage = await loadStyleImage(asset.outfit.reference, "Outfit reference image");
|
|
10490
|
+
resolved.character = {
|
|
10491
|
+
kind: "outfit",
|
|
10492
|
+
mode: sourceSpec.character.mode,
|
|
10493
|
+
directions: sourceSpec.character.directions,
|
|
10494
|
+
template: sourceSpec.character.template,
|
|
10495
|
+
parentAssetId: asset.outfit.of,
|
|
10496
|
+
parentSpec: sourceSpec,
|
|
10497
|
+
animation: sourceSpec.character.animation,
|
|
10498
|
+
outfit: {
|
|
10499
|
+
reference: {
|
|
10500
|
+
path: path13.resolve(root, asset.outfit.reference),
|
|
10501
|
+
sha256: referenceImage.hash,
|
|
10502
|
+
width: referenceImage.width,
|
|
10503
|
+
height: referenceImage.height,
|
|
10504
|
+
format: referenceImage.format
|
|
10505
|
+
},
|
|
10506
|
+
...asset.outfit.additionalInstructions ? { additionalInstructions: asset.outfit.additionalInstructions } : {}
|
|
10507
|
+
}
|
|
10508
|
+
};
|
|
10509
|
+
resolved.generator = sourceSpec.generator;
|
|
10510
|
+
resolved.width = sourceSpec.width;
|
|
10511
|
+
resolved.height = sourceSpec.height;
|
|
10512
|
+
resolved.size = sourceSpec.size;
|
|
10513
|
+
resolved.prompt = "";
|
|
10514
|
+
}
|
|
10188
10515
|
if (asset.revision) {
|
|
10189
10516
|
if (!activeProvider.supportsRevision?.(asset.revision.mode)) {
|
|
10190
10517
|
throw new Error(
|
|
@@ -10455,6 +10782,9 @@ async function resolveCharacterShape(asset, style, kind, files) {
|
|
|
10455
10782
|
...asset.state.canvas ? { canvas: asset.state.canvas } : {}
|
|
10456
10783
|
};
|
|
10457
10784
|
}
|
|
10785
|
+
if (asset.portrait) {
|
|
10786
|
+
shape.portrait = { size: asset.portrait.size };
|
|
10787
|
+
}
|
|
10458
10788
|
if (asset.animation) {
|
|
10459
10789
|
const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
|
|
10460
10790
|
const pose = async (rel, what) => {
|
|
@@ -11406,6 +11736,9 @@ async function loadClaims(lockPaths, opts = {}) {
|
|
|
11406
11736
|
add(entry.objectId);
|
|
11407
11737
|
add(entry.reviewObjectId);
|
|
11408
11738
|
add(entry.jobId);
|
|
11739
|
+
for (const id of [entry.objectId, entry.jobId]) {
|
|
11740
|
+
if (id?.includes("#")) add(id.split("#")[0]);
|
|
11741
|
+
}
|
|
11409
11742
|
}
|
|
11410
11743
|
}
|
|
11411
11744
|
return claimed;
|
|
@@ -11429,6 +11762,122 @@ async function findOrphans(provider, claimed, opts = {}) {
|
|
|
11429
11762
|
})).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
11430
11763
|
return { orphans, total: all.length };
|
|
11431
11764
|
}
|
|
11765
|
+
async function findOrphanCharacters(provider, claimed, opts = {}) {
|
|
11766
|
+
const log2 = opts.onProgress ?? (() => {
|
|
11767
|
+
});
|
|
11768
|
+
if (!provider.listCharacters) return { orphans: [], total: 0, partlyClaimed: 0 };
|
|
11769
|
+
const all = [];
|
|
11770
|
+
for await (const character of provider.listCharacters()) all.push(character);
|
|
11771
|
+
log2(` scanned ${all.length} character(s) on the account`);
|
|
11772
|
+
const isClaimed = (id) => claimed.has(id) || claimed.has(providerClaimId(provider.id, id));
|
|
11773
|
+
const groups = /* @__PURE__ */ new Map();
|
|
11774
|
+
for (const character of all) {
|
|
11775
|
+
const key = character.groupId ?? character.id;
|
|
11776
|
+
groups.set(key, [...groups.get(key) ?? [], character]);
|
|
11777
|
+
}
|
|
11778
|
+
let partlyClaimed = 0;
|
|
11779
|
+
const orphans = [];
|
|
11780
|
+
for (const members of groups.values()) {
|
|
11781
|
+
const unclaimed = members.filter((character) => !isClaimed(character.id));
|
|
11782
|
+
if (!unclaimed.length) continue;
|
|
11783
|
+
if (unclaimed.length < members.length) {
|
|
11784
|
+
partlyClaimed++;
|
|
11785
|
+
continue;
|
|
11786
|
+
}
|
|
11787
|
+
const ordered = [...members].sort((a, b) => Number(isState(a)) - Number(isState(b)) || a.createdAt.localeCompare(b.createdAt));
|
|
11788
|
+
const base = ordered[0];
|
|
11789
|
+
if (base.status !== "completed" || !base.previewUrl) continue;
|
|
11790
|
+
const states = ordered.length - 1;
|
|
11791
|
+
const loops = ordered.reduce((sum, character) => sum + character.animationCount, 0);
|
|
11792
|
+
orphans.push({
|
|
11793
|
+
id: base.id,
|
|
11794
|
+
prompt: base.prompt || base.name || "(no prompt recorded)",
|
|
11795
|
+
width: base.width,
|
|
11796
|
+
height: base.height,
|
|
11797
|
+
createdAt: base.createdAt,
|
|
11798
|
+
previewUrl: base.previewUrl,
|
|
11799
|
+
tags: base.tags ?? [],
|
|
11800
|
+
kind: "character",
|
|
11801
|
+
note: `character \xB7 ${base.directions} directions \xB7 ${states} state${states === 1 ? "" : "s"} \xB7 ${loops} loop${loops === 1 ? "" : "s"}`,
|
|
11802
|
+
members: ordered.map((character) => ({ id: character.id, tags: character.tags ?? [] }))
|
|
11803
|
+
});
|
|
11804
|
+
}
|
|
11805
|
+
orphans.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
11806
|
+
return { orphans, total: all.length, partlyClaimed };
|
|
11807
|
+
}
|
|
11808
|
+
function isState(character) {
|
|
11809
|
+
return Boolean(character.stateName && character.stateName !== "Idle");
|
|
11810
|
+
}
|
|
11811
|
+
function characterStyleIds(manifest) {
|
|
11812
|
+
return Object.keys(manifest.styles).filter((id) => manifest.styles[id].generator === "character");
|
|
11813
|
+
}
|
|
11814
|
+
function withoutCharacterStyles(manifest) {
|
|
11815
|
+
const characters = new Set(characterStyleIds(manifest));
|
|
11816
|
+
return {
|
|
11817
|
+
...manifest,
|
|
11818
|
+
styles: Object.fromEntries(Object.entries(manifest.styles).filter(([id]) => !characters.has(id)))
|
|
11819
|
+
};
|
|
11820
|
+
}
|
|
11821
|
+
function routeCharacterOrphans(orphans, manifest) {
|
|
11822
|
+
const styleIds = characterStyleIds(manifest);
|
|
11823
|
+
const matched = /* @__PURE__ */ new Map();
|
|
11824
|
+
const unmatched = [];
|
|
11825
|
+
const only = { ...manifest, styles: Object.fromEntries(styleIds.map((id) => [id, manifest.styles[id]])) };
|
|
11826
|
+
for (const orphan of orphans) {
|
|
11827
|
+
const styleId = styleIds.length === 1 ? styleIds[0] : styleIds.length ? matchStyleByPattern(orphan.prompt, only) : null;
|
|
11828
|
+
if (!styleId) {
|
|
11829
|
+
unmatched.push(orphan);
|
|
11830
|
+
continue;
|
|
11831
|
+
}
|
|
11832
|
+
matched.set(styleId, [...matched.get(styleId) ?? [], orphan]);
|
|
11833
|
+
}
|
|
11834
|
+
return { matched, unmatched };
|
|
11835
|
+
}
|
|
11836
|
+
function characterSalvageAssets(characters, ctx) {
|
|
11837
|
+
const ordered = [...characters].sort((a, b) => Number(isState(a)) - Number(isState(b)) || a.createdAt.localeCompare(b.createdAt));
|
|
11838
|
+
const base = ordered[0];
|
|
11839
|
+
const common = { styles: [ctx.styleId], tags: ["salvaged"] };
|
|
11840
|
+
const assets = {};
|
|
11841
|
+
const skipped = [];
|
|
11842
|
+
const baseId = idFromPrompt(base.name || base.prompt, ctx.taken);
|
|
11843
|
+
const size = base.width === base.height && base.width >= 32 && base.width <= 256 && base.width !== ctx.styleSize ? { size: base.width } : {};
|
|
11844
|
+
assets[baseId] = { prompt: base.prompt || base.name || baseId, remoteId: base.id, ...size, ...common };
|
|
11845
|
+
const assetIdOf = /* @__PURE__ */ new Map([[base.id, baseId]]);
|
|
11846
|
+
for (const state of ordered.slice(1)) {
|
|
11847
|
+
const stateId = uniqueId(`${baseId}_${slug(state.stateName ?? "state")}`, ctx.taken);
|
|
11848
|
+
assetIdOf.set(state.id, stateId);
|
|
11849
|
+
assets[stateId] = { prompt: state.prompt || state.stateName || stateId, state: { of: baseId }, remoteId: state.id, ...common };
|
|
11850
|
+
}
|
|
11851
|
+
for (const character of ordered) {
|
|
11852
|
+
const parentId = assetIdOf.get(character.id);
|
|
11853
|
+
for (const animation of character.animations) {
|
|
11854
|
+
if (!animation.frames.length) continue;
|
|
11855
|
+
const label = animation.name || animation.type;
|
|
11856
|
+
if (!animation.groupId) {
|
|
11857
|
+
skipped.push(`${parentId}: "${label}" ${animation.direction} loop has no animation group id to adopt it by`);
|
|
11858
|
+
continue;
|
|
11859
|
+
}
|
|
11860
|
+
const loopId = uniqueId(`${parentId}.${slug(label)}.${animation.direction}`, ctx.taken);
|
|
11861
|
+
assets[loopId] = {
|
|
11862
|
+
prompt: label,
|
|
11863
|
+
animation: { of: parentId, direction: animation.direction },
|
|
11864
|
+
remoteId: `${character.id}#${animation.groupId}`,
|
|
11865
|
+
...common
|
|
11866
|
+
};
|
|
11867
|
+
}
|
|
11868
|
+
}
|
|
11869
|
+
return { assets, skipped };
|
|
11870
|
+
}
|
|
11871
|
+
function slug(text) {
|
|
11872
|
+
return text.toLowerCase().replace(/^pixelkiln:.*\//, "").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "loop";
|
|
11873
|
+
}
|
|
11874
|
+
function uniqueId(base, taken) {
|
|
11875
|
+
let id = base;
|
|
11876
|
+
let n = 2;
|
|
11877
|
+
while (taken.has(id)) id = `${base}_${n++}`;
|
|
11878
|
+
taken.add(id);
|
|
11879
|
+
return id;
|
|
11880
|
+
}
|
|
11432
11881
|
var MIN_STYLE_FRAGMENT_LENGTH = 12;
|
|
11433
11882
|
function matchStyleByPattern(prompt, manifest) {
|
|
11434
11883
|
const p = prompt.toLowerCase();
|
|
@@ -11553,18 +12002,22 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
11553
12002
|
let tagged = 0;
|
|
11554
12003
|
let failed = 0;
|
|
11555
12004
|
for (const { id, action } of decisions) {
|
|
11556
|
-
const
|
|
11557
|
-
|
|
11558
|
-
)
|
|
11559
|
-
|
|
11560
|
-
|
|
11561
|
-
|
|
11562
|
-
|
|
11563
|
-
|
|
11564
|
-
tagged
|
|
11565
|
-
|
|
11566
|
-
|
|
11567
|
-
|
|
12005
|
+
const group = opts.characters?.get(id);
|
|
12006
|
+
const targets = group ?? [{ id, tags: existing.get(id) ?? [] }];
|
|
12007
|
+
for (const target of targets) {
|
|
12008
|
+
const current = target.tags.filter(
|
|
12009
|
+
(t) => t !== "pixelkiln:keep" && t !== "pixelkiln:discard" && t !== "pixelkiln:imported"
|
|
12010
|
+
);
|
|
12011
|
+
const decisionTag = `pixelkiln:${action === "import" ? "imported" : action}`;
|
|
12012
|
+
const next = [...current.slice(0, 19), decisionTag];
|
|
12013
|
+
if (!provider.setTags) return { tagged, failed };
|
|
12014
|
+
try {
|
|
12015
|
+
await provider.setTags(target.id, next, group ? "character" : void 0);
|
|
12016
|
+
tagged++;
|
|
12017
|
+
} catch (err) {
|
|
12018
|
+
failed++;
|
|
12019
|
+
log2(` tag failed ${target.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
12020
|
+
}
|
|
11568
12021
|
}
|
|
11569
12022
|
}
|
|
11570
12023
|
return { tagged, failed };
|
|
@@ -11710,7 +12163,7 @@ import path19 from "path";
|
|
|
11710
12163
|
// src/pick/salvage-sheet.ts
|
|
11711
12164
|
var escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
11712
12165
|
function renderSalvageSheet(orphans, ctx) {
|
|
11713
|
-
const safe = orphans.map((o) => ({ ...o, prompt: escapeHtml(o.prompt) }));
|
|
12166
|
+
const safe = orphans.map((o) => ({ ...o, prompt: escapeHtml(o.prompt), ...o.note ? { note: escapeHtml(o.note) } : {} }));
|
|
11714
12167
|
const data = JSON.stringify(safe).replace(/<\//g, "<\\/");
|
|
11715
12168
|
const styleId = ctx ? escapeHtml(ctx.styleId) : null;
|
|
11716
12169
|
const importDir = ctx ? escapeHtml(ctx.importDir) : null;
|
|
@@ -11796,6 +12249,7 @@ ITEMS.forEach((o, i) => {
|
|
|
11796
12249
|
el.innerHTML =
|
|
11797
12250
|
'<div class="thumb"><img src="' + o.previewUrl + '" loading="lazy"></div>' +
|
|
11798
12251
|
'<div class="meta">' + o.width + '\xD7' + o.height + ' \xB7 ' + o.createdAt.slice(0,10) + '</div>' +
|
|
12252
|
+
(o.note ? '<div class="meta">' + o.note + '</div>' : '') +
|
|
11799
12253
|
'<div class="prompt">' + o.prompt + '</div>' +
|
|
11800
12254
|
'<div class="acts">' +
|
|
11801
12255
|
'<button data-a="import">import</button>' +
|
|
@@ -11973,6 +12427,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
11973
12427
|
});
|
|
11974
12428
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
11975
12429
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
12430
|
+
const characterMembers = new Map(
|
|
12431
|
+
orphans.flatMap((o) => o.kind === "character" && o.members ? [[o.id, o.members]] : [])
|
|
12432
|
+
);
|
|
11976
12433
|
return serveReviewPage({
|
|
11977
12434
|
html,
|
|
11978
12435
|
port: opts.port,
|
|
@@ -11989,10 +12446,34 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
11989
12446
|
const result = { imported: 0, kept: 0, discarded: 0, failed: 0 };
|
|
11990
12447
|
const taken = new Set(Object.keys(ctx.manifest.assets));
|
|
11991
12448
|
const importedAssetIds = [];
|
|
12449
|
+
const characterAssetIds = [];
|
|
11992
12450
|
for (const decision of decisions) {
|
|
11993
12451
|
const orphan = byId.get(decision.id);
|
|
11994
12452
|
if (!orphan) continue;
|
|
11995
|
-
if (decision.action === "import") {
|
|
12453
|
+
if (decision.action === "import" && orphan.kind === "character") {
|
|
12454
|
+
try {
|
|
12455
|
+
if (!provider.getCharacter) throw new Error(`provider ${provider.id} cannot read characters`);
|
|
12456
|
+
const details = [];
|
|
12457
|
+
for (const member of orphan.members ?? [{ id: orphan.id }]) details.push(await provider.getCharacter(member.id));
|
|
12458
|
+
const style = ctx.manifest.styles[ctx.styleId];
|
|
12459
|
+
const { assets, skipped } = characterSalvageAssets(details, {
|
|
12460
|
+
styleId: ctx.styleId,
|
|
12461
|
+
styleSize: style?.size,
|
|
12462
|
+
taken
|
|
12463
|
+
});
|
|
12464
|
+
for (const [assetId, asset] of Object.entries(assets)) {
|
|
12465
|
+
ctx.manifest.assets[assetId] = asset;
|
|
12466
|
+
importedAssetIds.push(assetId);
|
|
12467
|
+
characterAssetIds.push(assetId);
|
|
12468
|
+
}
|
|
12469
|
+
for (const line of skipped) log2(` not imported: ${line}`);
|
|
12470
|
+
result.imported++;
|
|
12471
|
+
log2(` imported character ${orphan.id} as ${Object.keys(assets).length} asset(s)`);
|
|
12472
|
+
} catch (err) {
|
|
12473
|
+
result.failed++;
|
|
12474
|
+
log2(` import failed ${orphan.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
12475
|
+
}
|
|
12476
|
+
} else if (decision.action === "import") {
|
|
11996
12477
|
try {
|
|
11997
12478
|
const buf = await provider.download(orphan.previewUrl);
|
|
11998
12479
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
@@ -12056,11 +12537,12 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
12056
12537
|
result.discarded++;
|
|
12057
12538
|
}
|
|
12058
12539
|
}
|
|
12059
|
-
await applyTags(provider, decisions, existingTags, { onProgress: log2 });
|
|
12540
|
+
await applyTags(provider, decisions, existingTags, { onProgress: log2, characters: characterMembers });
|
|
12060
12541
|
const raw = JSON.parse(await readFile14(ctx.manifestPath, "utf8"));
|
|
12061
12542
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
12062
12543
|
await writeFile9(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
12063
12544
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
12545
|
+
if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
|
|
12064
12546
|
return result;
|
|
12065
12547
|
}
|
|
12066
12548
|
});
|
|
@@ -12502,7 +12984,18 @@ async function runSalvage2(args) {
|
|
|
12502
12984
|
const claimed = await loadClaims(lockPaths, { provider: provider.id });
|
|
12503
12985
|
const { orphans, total } = await findOrphans(provider, claimed, { onProgress: diag });
|
|
12504
12986
|
diag(` ${claimed.size} claimed \xB7 ${orphans.length} unclaimed of ${total}`);
|
|
12505
|
-
|
|
12987
|
+
const characterScan = await findOrphanCharacters(provider, claimed, { onProgress: diag });
|
|
12988
|
+
const characterOrphans = characterScan.orphans;
|
|
12989
|
+
if (characterScan.total) {
|
|
12990
|
+
diag(` ${characterOrphans.length} unclaimed character group(s) of ${characterScan.total} character(s)`);
|
|
12991
|
+
}
|
|
12992
|
+
if (characterScan.partlyClaimed) {
|
|
12993
|
+
diag(
|
|
12994
|
+
` ${characterScan.partlyClaimed} character group(s) are partly claimed (a base or state some project uses);
|
|
12995
|
+
they are left out here. Declare their other states and run \`pixelkiln adopt\` instead.`
|
|
12996
|
+
);
|
|
12997
|
+
}
|
|
12998
|
+
if (!orphans.length && !characterOrphans.length) {
|
|
12506
12999
|
if (jsonMode) log("[]");
|
|
12507
13000
|
else log(`
|
|
12508
13001
|
nothing to triage`);
|
|
@@ -12513,9 +13006,10 @@ async function runSalvage2(args) {
|
|
|
12513
13006
|
workspaceProjects.map((p) => resolveProject(workspaceDir, p).manifestPath),
|
|
12514
13007
|
args.claims
|
|
12515
13008
|
);
|
|
12516
|
-
const
|
|
12517
|
-
const
|
|
12518
|
-
|
|
13009
|
+
const objectManifest = withoutCharacterStyles(accountManifest);
|
|
13010
|
+
const { matched, unmatched, elsewhere } = groupOrphansByStyle(orphans, objectManifest, siblings);
|
|
13011
|
+
const multiStyle = Object.keys(objectManifest.styles).length > 1;
|
|
13012
|
+
if (multiStyle && orphans.length) {
|
|
12519
13013
|
diag(`
|
|
12520
13014
|
by style (matched against each style's prompt prefix/suffix):`);
|
|
12521
13015
|
for (const [id, list] of matched) diag(` ${id.padEnd(24)} ${list.length}`);
|
|
@@ -12536,16 +13030,33 @@ async function runSalvage2(args) {
|
|
|
12536
13030
|
out of the sessions below; force them into one style with --style <id>.`
|
|
12537
13031
|
);
|
|
12538
13032
|
}
|
|
13033
|
+
const characterRoutes = routeCharacterOrphans(characterOrphans, accountManifest);
|
|
13034
|
+
if (characterOrphans.length && !characterStyleIds(accountManifest).length) {
|
|
13035
|
+
diag(
|
|
13036
|
+
`
|
|
13037
|
+
${characterOrphans.length} unclaimed character group(s), but this manifest has no character style
|
|
13038
|
+
to import them into. Add a style with "generator": "character" to triage them.`
|
|
13039
|
+
);
|
|
13040
|
+
} else if (characterRoutes.unmatched.length) {
|
|
13041
|
+
diag(
|
|
13042
|
+
`
|
|
13043
|
+
${characterRoutes.unmatched.length} character group(s) don't match any character style's pattern;
|
|
13044
|
+
force them into one with --style <character style id>.`
|
|
13045
|
+
);
|
|
13046
|
+
}
|
|
12539
13047
|
if (args.dryRun) {
|
|
13048
|
+
const listed = [...orphans, ...characterOrphans];
|
|
12540
13049
|
if (args.json) {
|
|
12541
|
-
log(JSON.stringify(
|
|
13050
|
+
log(JSON.stringify(listed, null, 2));
|
|
12542
13051
|
} else {
|
|
12543
|
-
const shown = args.all ?
|
|
13052
|
+
const shown = args.all ? listed : listed.slice(0, 30);
|
|
12544
13053
|
for (const o of shown) {
|
|
12545
|
-
log(
|
|
13054
|
+
log(
|
|
13055
|
+
` ${o.id} ${o.width}x${o.height} ${o.createdAt.slice(0, 10)} ` + (o.kind === "character" ? `[${o.note}] ` : "") + o.prompt.slice(0, 50)
|
|
13056
|
+
);
|
|
12546
13057
|
}
|
|
12547
|
-
if (!args.all &&
|
|
12548
|
-
log(` \u2026 and ${
|
|
13058
|
+
if (!args.all && listed.length > 30) {
|
|
13059
|
+
log(` \u2026 and ${listed.length - 30} more (--all for the full list, --json for machine-readable)`);
|
|
12549
13060
|
}
|
|
12550
13061
|
}
|
|
12551
13062
|
diag(`
|
|
@@ -12566,6 +13077,12 @@ async function runSalvage2(args) {
|
|
|
12566
13077
|
await saveLock(args.lock, lock);
|
|
12567
13078
|
if (n) log(` baselined ${n} imported asset(s) against the manifest`);
|
|
12568
13079
|
};
|
|
13080
|
+
const adoptImported = async (assetIds) => {
|
|
13081
|
+
const resolve = async () => (await resolveSpecs(await loadManifest(args.manifest), { assets: assetIds })).filter((spec) => spec.provider === provider.id);
|
|
13082
|
+
const adopted = await adoptCharacters(provider, await resolve(), lock, args.lock, { onProgress: log, resolve });
|
|
13083
|
+
log(` adopted ${adopted.matched} of ${assetIds.length} imported character asset(s)`);
|
|
13084
|
+
for (const line of adopted.unmatched) log(` not adopted: ${line}`);
|
|
13085
|
+
};
|
|
12569
13086
|
const runOne = async (styleId, list) => {
|
|
12570
13087
|
const style = accountManifest.styles[styleId];
|
|
12571
13088
|
const res = await runSalvage(
|
|
@@ -12585,10 +13102,19 @@ async function runSalvage2(args) {
|
|
|
12585
13102
|
` imported ${res.imported} \xB7 kept ${res.kept} \xB7 tagged-discard ${res.discarded}` + (res.failed ? ` \xB7 failed ${res.failed}` : "")
|
|
12586
13103
|
);
|
|
12587
13104
|
if (res.imported > 0) await rebaseline();
|
|
13105
|
+
if (res.characterAssetIds?.length) await adoptImported(res.characterAssetIds);
|
|
12588
13106
|
return res;
|
|
12589
13107
|
};
|
|
12590
13108
|
if (args.styles.length) {
|
|
12591
|
-
const
|
|
13109
|
+
const styleId = args.styles[0];
|
|
13110
|
+
const isCharacterStyle = characterStyleIds(accountManifest).includes(styleId);
|
|
13111
|
+
const list = isCharacterStyle ? characterOrphans : orphans;
|
|
13112
|
+
if (!list.length) {
|
|
13113
|
+
log(`
|
|
13114
|
+
nothing unclaimed to triage into ${styleId}`);
|
|
13115
|
+
return;
|
|
13116
|
+
}
|
|
13117
|
+
const res = await runOne(styleId, list);
|
|
12592
13118
|
if (res.discarded) {
|
|
12593
13119
|
log(`
|
|
12594
13120
|
Nothing was deleted. To actually remove the discarded objects:`);
|
|
@@ -12597,17 +13123,18 @@ async function runSalvage2(args) {
|
|
|
12597
13123
|
}
|
|
12598
13124
|
return;
|
|
12599
13125
|
}
|
|
12600
|
-
|
|
13126
|
+
const sessions = [...matched, ...characterRoutes.matched];
|
|
13127
|
+
if (!sessions.length) {
|
|
12601
13128
|
log(`
|
|
12602
13129
|
nothing matched a known style; nothing to triage`);
|
|
12603
13130
|
return;
|
|
12604
13131
|
}
|
|
12605
13132
|
log(`
|
|
12606
|
-
${
|
|
13133
|
+
${sessions.length} session(s), one style at a time:`);
|
|
12607
13134
|
let totalDiscarded = 0;
|
|
12608
|
-
for (const [styleId, list] of
|
|
13135
|
+
for (const [styleId, list] of sessions) {
|
|
12609
13136
|
log(`
|
|
12610
|
-
${styleId} (${list.length})`);
|
|
13137
|
+
${styleId} (${list.length}${list[0]?.kind === "character" ? " character group(s)" : ""})`);
|
|
12611
13138
|
const res = await runOne(styleId, list);
|
|
12612
13139
|
totalDiscarded += res.discarded;
|
|
12613
13140
|
}
|
|
@@ -12625,16 +13152,30 @@ async function runPurge(args) {
|
|
|
12625
13152
|
const doomed = [];
|
|
12626
13153
|
for await (const obj of requireList(provider)()) {
|
|
12627
13154
|
if (obj.tags.includes("pixelkiln:discard")) {
|
|
12628
|
-
doomed.push({ id: obj.id, prompt: obj.prompt });
|
|
13155
|
+
doomed.push({ id: obj.id, prompt: obj.prompt, kind: "object" });
|
|
13156
|
+
}
|
|
13157
|
+
}
|
|
13158
|
+
if (provider.listCharacters && provider.deleteCharacter) {
|
|
13159
|
+
for await (const character of provider.listCharacters()) {
|
|
13160
|
+
if (character.tags.includes("pixelkiln:discard")) {
|
|
13161
|
+
const label = character.stateName && character.stateName !== "Idle" ? ` (${character.stateName})` : "";
|
|
13162
|
+
doomed.push({ id: character.id, prompt: `${character.prompt || character.name}${label}`, kind: "character" });
|
|
13163
|
+
}
|
|
12629
13164
|
}
|
|
12630
13165
|
}
|
|
12631
13166
|
if (!doomed.length) {
|
|
12632
13167
|
log(` nothing tagged pixelkiln:discard; run \`pixelkiln salvage\` first`);
|
|
12633
13168
|
return;
|
|
12634
13169
|
}
|
|
13170
|
+
const characters = doomed.filter((d) => d.kind === "character").length;
|
|
13171
|
+
const objects = doomed.length - characters;
|
|
13172
|
+
const counted = [
|
|
13173
|
+
objects ? `${objects} object(s)` : "",
|
|
13174
|
+
characters ? `${characters} character(s)` : ""
|
|
13175
|
+
].filter(Boolean).join(" and ");
|
|
12635
13176
|
log(`
|
|
12636
|
-
${
|
|
12637
|
-
for (const d of doomed.slice(0, 20)) log(` ${d.id} ${d.prompt.slice(0, 60)}`);
|
|
13177
|
+
${counted} tagged for discard:`);
|
|
13178
|
+
for (const d of doomed.slice(0, 20)) log(` ${d.id} ${d.kind === "character" ? "[character] " : ""}${d.prompt.slice(0, 60)}`);
|
|
12638
13179
|
if (doomed.length > 20) log(` \u2026 and ${doomed.length - 20} more`);
|
|
12639
13180
|
if (args.dryRun) {
|
|
12640
13181
|
log(`
|
|
@@ -12645,7 +13186,8 @@ async function runPurge(args) {
|
|
|
12645
13186
|
This permanently deletes them from your ${provider.id} account.`);
|
|
12646
13187
|
log(` Any local files already downloaded are untouched, but the objects`);
|
|
12647
13188
|
log(` and their URLs are gone and cannot be re-downloaded.`);
|
|
12648
|
-
if (
|
|
13189
|
+
if (characters) log(` A deleted character takes its animations with it.`);
|
|
13190
|
+
if (!await confirm(` Delete ${counted}?`, args.yes)) {
|
|
12649
13191
|
log(` aborted`);
|
|
12650
13192
|
return;
|
|
12651
13193
|
}
|
|
@@ -12653,7 +13195,8 @@ async function runPurge(args) {
|
|
|
12653
13195
|
let failed = 0;
|
|
12654
13196
|
for (const d of doomed) {
|
|
12655
13197
|
try {
|
|
12656
|
-
await
|
|
13198
|
+
if (d.kind === "character") await provider.deleteCharacter(d.id);
|
|
13199
|
+
else await requireDelete(provider)(d.id);
|
|
12657
13200
|
deleted++;
|
|
12658
13201
|
} catch (err) {
|
|
12659
13202
|
failed++;
|
|
@@ -13608,7 +14151,7 @@ function describeCharacter(spec, entry) {
|
|
|
13608
14151
|
};
|
|
13609
14152
|
}
|
|
13610
14153
|
if (entry?.generator === "character") {
|
|
13611
|
-
const kind = recordedCharacter?.kind === "animation" || entry.outputs.some((o) => o.role?.startsWith("frame-")) ? "animation" : "base";
|
|
14154
|
+
const kind = recordedCharacter?.kind === "portrait" ? "portrait" : recordedCharacter?.kind === "outfit" ? "outfit" : recordedCharacter?.kind === "animation" || entry.outputs.some((o) => o.role?.startsWith("frame-")) ? "animation" : "base";
|
|
13612
14155
|
return {
|
|
13613
14156
|
generator: "character",
|
|
13614
14157
|
kind,
|
|
@@ -13969,7 +14512,9 @@ async function buildGallerySnapshot(opts) {
|
|
|
13969
14512
|
characters: style?.generator === "character" || style?.generator === "objectPro" ? {
|
|
13970
14513
|
bases: styleItems.filter((item) => item.character?.kind === "base").length,
|
|
13971
14514
|
states: styleItems.filter((item) => item.character?.kind === "state").length,
|
|
13972
|
-
animations: styleItems.filter((item) => item.character?.kind === "animation").length
|
|
14515
|
+
animations: styleItems.filter((item) => item.character?.kind === "animation").length,
|
|
14516
|
+
portraits: styleItems.filter((item) => item.character?.kind === "portrait").length,
|
|
14517
|
+
outfits: styleItems.filter((item) => item.character?.kind === "outfit").length
|
|
13973
14518
|
} : null,
|
|
13974
14519
|
tags: style?.tags ?? [],
|
|
13975
14520
|
extends: typeof rawStyle?.extends === "string" ? rawStyle.extends : null,
|
|
@@ -14746,6 +15291,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
14746
15291
|
const supersededOutputs = previousEntry?.outputs.length ? previousEntry.outputs : previousEntry?.supersededOutputs ?? [];
|
|
14747
15292
|
const submittedAt = resumesCheckpoint ? previousEntry.submittedAt : (/* @__PURE__ */ new Date()).toISOString();
|
|
14748
15293
|
const history = historyAfterReplacing(previousEntry, historyLimit(loaded.manifest), submittedAt ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
15294
|
+
const outfitParentEntry = spec.character?.kind === "outfit" && spec.character.parentSpec ? lock.entries[lockKey(spec.character.parentSpec.styleId, spec.character.parentSpec.assetId)] : void 0;
|
|
14749
15295
|
upsert(lock, key, {
|
|
14750
15296
|
styleId: spec.styleId,
|
|
14751
15297
|
assetId: spec.assetId,
|
|
@@ -14771,6 +15317,11 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
14771
15317
|
...spec.revision.direction ? { direction: spec.revision.direction } : {},
|
|
14772
15318
|
...spec.revision.enhancePrompt == null ? {} : { enhancePrompt: spec.revision.enhancePrompt }
|
|
14773
15319
|
} : null,
|
|
15320
|
+
outfit: spec.character?.kind === "outfit" && outfitParentEntry ? {
|
|
15321
|
+
sourceAssetId: spec.character.parentAssetId,
|
|
15322
|
+
sourceSha256: mirrorSourceHash(outfitParentEntry),
|
|
15323
|
+
referenceSha256: spec.character.outfit.reference.sha256
|
|
15324
|
+
} : null,
|
|
14774
15325
|
status: "pending",
|
|
14775
15326
|
jobId: previousJobId ?? null,
|
|
14776
15327
|
...previousJobId ? { submissionComplete: false } : { submissionComplete: void 0 },
|
|
@@ -14804,10 +15355,15 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
14804
15355
|
const anchor = spec.character?.styleAnchor;
|
|
14805
15356
|
const anchorEntry = anchor ? lock.entries[lockKey(anchor.spec.styleId, anchor.spec.assetId)] : void 0;
|
|
14806
15357
|
const replacedMetadata = previousEntry?.providerMetadata?.[provider.id];
|
|
15358
|
+
const parentOutputs = spec.character?.kind === "outfit" && parentEntry ? parentEntry.outputs.map((output, index) => ({
|
|
15359
|
+
path: currentEntryOutputPath(parentEntry, spec.character.parentSpec, index),
|
|
15360
|
+
role: output.role
|
|
15361
|
+
})) : void 0;
|
|
14807
15362
|
const { jobId, metadata } = await provider.submit(spec, refs, {
|
|
14808
15363
|
...previousJobId ? { previousJobId } : {},
|
|
14809
15364
|
...parentEntry?.objectId ? { parentObjectId: parentEntry.objectId } : {},
|
|
14810
15365
|
...anchorEntry?.objectId ? { styleObjectId: anchorEntry.objectId } : {},
|
|
15366
|
+
...parentOutputs ? { parentOutputs } : {},
|
|
14811
15367
|
...replacedMetadata ? { replacedMetadata } : {},
|
|
14812
15368
|
...previousMetadata ? { previousMetadata } : {},
|
|
14813
15369
|
checkpoint: async (checkpoint) => {
|