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/index.cjs
CHANGED
|
@@ -40,6 +40,9 @@ __export(index_exports, {
|
|
|
40
40
|
CharacterAnimationSchema: () => CharacterAnimationSchema,
|
|
41
41
|
CharacterDirectionSchema: () => CharacterDirectionSchema,
|
|
42
42
|
CharacterModeSchema: () => CharacterModeSchema,
|
|
43
|
+
CharacterOutfitSchema: () => CharacterOutfitSchema,
|
|
44
|
+
CharacterPortraitSchema: () => CharacterPortraitSchema,
|
|
45
|
+
CharacterPortraitSizeSchema: () => CharacterPortraitSizeSchema,
|
|
43
46
|
CharacterProportionsSchema: () => CharacterProportionsSchema,
|
|
44
47
|
CharacterStateSchema: () => CharacterStateSchema,
|
|
45
48
|
ComfyUIProvider: () => ComfyUIProvider,
|
|
@@ -504,6 +507,27 @@ var CharacterStateSchema = import_zod.z.object({
|
|
|
504
507
|
height: import_zod.z.number().int().min(16).max(256)
|
|
505
508
|
}).strict().optional()
|
|
506
509
|
}).strict();
|
|
510
|
+
var CharacterPortraitSizeSchema = import_zod.z.union([
|
|
511
|
+
import_zod.z.literal(16),
|
|
512
|
+
import_zod.z.literal(32),
|
|
513
|
+
import_zod.z.literal(48),
|
|
514
|
+
import_zod.z.literal(64),
|
|
515
|
+
import_zod.z.literal(128),
|
|
516
|
+
import_zod.z.literal(160)
|
|
517
|
+
]);
|
|
518
|
+
var CharacterPortraitSchema = import_zod.z.object({
|
|
519
|
+
/** The base or state to portray, in the same style. */
|
|
520
|
+
of: import_zod.z.string().min(1),
|
|
521
|
+
size: CharacterPortraitSizeSchema.default(64)
|
|
522
|
+
}).strict();
|
|
523
|
+
var CharacterOutfitSchema = import_zod.z.object({
|
|
524
|
+
/** The loop (a character animation asset) to re-clothe, in the same style. */
|
|
525
|
+
of: import_zod.z.string().min(1),
|
|
526
|
+
/** Manifest-relative image of the outfit to transfer. */
|
|
527
|
+
reference: import_zod.z.string().min(1),
|
|
528
|
+
/** Extra guidance PixelLab passes through as written, e.g. view/direction hints. */
|
|
529
|
+
additionalInstructions: import_zod.z.string().max(2e3).optional()
|
|
530
|
+
}).strict();
|
|
507
531
|
var CharacterAnimationSchema = import_zod.z.object({
|
|
508
532
|
/** The character to animate: a base or a state in the same style. */
|
|
509
533
|
of: import_zod.z.string().min(1),
|
|
@@ -1125,6 +1149,10 @@ var AssetSchema = import_zod.z.object({
|
|
|
1125
1149
|
pieces: import_zod.z.array(UiPieceSchema).min(1).optional(),
|
|
1126
1150
|
/** `uiAsset` styles: named, auto-positioned UI element scaffolds. Combine with `pieces`; omit both for a default full-canvas panel. */
|
|
1127
1151
|
elements: import_zod.z.array(UiElementSchema).min(1).optional(),
|
|
1152
|
+
/** `character` styles: this asset is a bust portrait of another character asset's south sprite. */
|
|
1153
|
+
portrait: CharacterPortraitSchema.optional(),
|
|
1154
|
+
/** `character` styles: this asset re-clothes an existing loop's frames with a reference outfit. */
|
|
1155
|
+
outfit: CharacterOutfitSchema.optional(),
|
|
1128
1156
|
/** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
|
|
1129
1157
|
proportions: CharacterProportionsSchema.optional(),
|
|
1130
1158
|
/**
|
|
@@ -1201,7 +1229,7 @@ var AssetSchema = import_zod.z.object({
|
|
|
1201
1229
|
path: ["revision"]
|
|
1202
1230
|
});
|
|
1203
1231
|
}
|
|
1204
|
-
const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror", asset.batch && "batch"].filter(Boolean);
|
|
1232
|
+
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);
|
|
1205
1233
|
if (shapes.length > 1) {
|
|
1206
1234
|
context.addIssue({
|
|
1207
1235
|
code: import_zod.z.ZodIssueCode.custom,
|
|
@@ -1216,24 +1244,24 @@ var AssetSchema = import_zod.z.object({
|
|
|
1216
1244
|
path: ["mirror"]
|
|
1217
1245
|
});
|
|
1218
1246
|
}
|
|
1219
|
-
if (asset.prompt === void 0 && !asset.mirror) {
|
|
1247
|
+
if (asset.prompt === void 0 && !asset.mirror && !asset.portrait && !asset.outfit) {
|
|
1220
1248
|
context.addIssue({
|
|
1221
1249
|
code: import_zod.z.ZodIssueCode.custom,
|
|
1222
1250
|
message: "Required",
|
|
1223
1251
|
path: ["prompt"]
|
|
1224
1252
|
});
|
|
1225
1253
|
}
|
|
1226
|
-
if (asset.reference && (asset.state || asset.animation || asset.mirror)) {
|
|
1254
|
+
if (asset.reference && (asset.state || asset.animation || asset.mirror || asset.portrait || asset.outfit)) {
|
|
1227
1255
|
context.addIssue({
|
|
1228
1256
|
code: import_zod.z.ZodIssueCode.custom,
|
|
1229
|
-
message: "a reference sprite belongs on a base; a state, animation, or
|
|
1257
|
+
message: "a reference sprite belongs on a base; a state, animation, mirror, portrait, or outfit takes its look from its parent",
|
|
1230
1258
|
path: ["reference"]
|
|
1231
1259
|
});
|
|
1232
1260
|
}
|
|
1233
|
-
if (asset.concept && (asset.state || asset.animation || asset.mirror)) {
|
|
1261
|
+
if (asset.concept && (asset.state || asset.animation || asset.mirror || asset.portrait || asset.outfit)) {
|
|
1234
1262
|
context.addIssue({
|
|
1235
1263
|
code: import_zod.z.ZodIssueCode.custom,
|
|
1236
|
-
message: "a concept image belongs on a base; a state, animation, or
|
|
1264
|
+
message: "a concept image belongs on a base; a state, animation, mirror, portrait, or outfit takes its look from its parent",
|
|
1237
1265
|
path: ["concept"]
|
|
1238
1266
|
});
|
|
1239
1267
|
}
|
|
@@ -1358,6 +1386,17 @@ var LockEntrySchema = import_zod.z.object({
|
|
|
1358
1386
|
leaderAssetId: import_zod.z.string().min(1).optional(),
|
|
1359
1387
|
index: import_zod.z.number().int().min(1).optional()
|
|
1360
1388
|
}).strict().nullable().default(null),
|
|
1389
|
+
/**
|
|
1390
|
+
* For an outfit: the loop it re-clothed, a hash over that loop's output
|
|
1391
|
+
* hashes when this was made, and the reference image's own hash. Tracked
|
|
1392
|
+
* the way `mirror` is, not through `specHash`: manifest resolution has no
|
|
1393
|
+
* lock to hash the parent's actual current frame bytes against.
|
|
1394
|
+
*/
|
|
1395
|
+
outfit: import_zod.z.object({
|
|
1396
|
+
sourceAssetId: import_zod.z.string().min(1),
|
|
1397
|
+
sourceSha256: import_zod.z.string().regex(/^[0-9a-f]{64}$/),
|
|
1398
|
+
referenceSha256: import_zod.z.string().regex(/^[0-9a-f]{64}$/)
|
|
1399
|
+
}).strict().nullable().default(null),
|
|
1361
1400
|
/**
|
|
1362
1401
|
* Output hashes owned by the previous generation while its replacement is
|
|
1363
1402
|
* pending. They authorize replacing only unchanged PixelKiln-owned files.
|
|
@@ -1761,6 +1800,28 @@ var AnimateObjectSubmitSchema = import_zod2.z.object({
|
|
|
1761
1800
|
submissions: import_zod2.z.array(DirectionSubmissionSchema),
|
|
1762
1801
|
usage: UsageSchema
|
|
1763
1802
|
}).passthrough();
|
|
1803
|
+
var PortraitSubmitSchema = import_zod2.z.object({
|
|
1804
|
+
background_job_id: import_zod2.z.string().min(1),
|
|
1805
|
+
status: import_zod2.z.string().default("processing"),
|
|
1806
|
+
usage: UsageSchema
|
|
1807
|
+
}).passthrough();
|
|
1808
|
+
var PortraitJobSchema = import_zod2.z.object({
|
|
1809
|
+
status: import_zod2.z.string(),
|
|
1810
|
+
download_url: import_zod2.z.string().nullable().optional(),
|
|
1811
|
+
width: import_zod2.z.number().int().nullable().optional(),
|
|
1812
|
+
height: import_zod2.z.number().int().nullable().optional()
|
|
1813
|
+
}).passthrough();
|
|
1814
|
+
var SetPortraitSchema = import_zod2.z.object({
|
|
1815
|
+
character_id: import_zod2.z.string().min(1),
|
|
1816
|
+
size: import_zod2.z.number().int().positive(),
|
|
1817
|
+
url: import_zod2.z.string().min(1),
|
|
1818
|
+
usage: UsageSchema
|
|
1819
|
+
}).passthrough();
|
|
1820
|
+
var TransferOutfitSubmitSchema = import_zod2.z.object({
|
|
1821
|
+
background_job_id: import_zod2.z.string().min(1),
|
|
1822
|
+
status: import_zod2.z.string().default("processing"),
|
|
1823
|
+
usage: UsageSchema
|
|
1824
|
+
}).passthrough();
|
|
1764
1825
|
var CharacterAnimationDirectionSchema = import_zod2.z.object({
|
|
1765
1826
|
direction: import_zod2.z.string(),
|
|
1766
1827
|
frame_count: import_zod2.z.number().int().min(0),
|
|
@@ -2376,6 +2437,69 @@ var PixelLabClient = class {
|
|
|
2376
2437
|
});
|
|
2377
2438
|
return validateResponse(AnimateSubmitSchema, raw, "animate-character");
|
|
2378
2439
|
}
|
|
2440
|
+
/**
|
|
2441
|
+
* A full-body sprite in, a bust portrait out (or the reverse, unused
|
|
2442
|
+
* today). Job-based like a state or `1dir`, not the couple-generations
|
|
2443
|
+
* "conversion" the name suggests: the smallest `resultSize` (16px) billed
|
|
2444
|
+
* 20 generations live (docs/ENDPOINTS.md, "Characters, measured").
|
|
2445
|
+
*/
|
|
2446
|
+
async createPortraitCharacterPro(args) {
|
|
2447
|
+
const raw = await this.request("/portrait-character-pro", {
|
|
2448
|
+
method: "POST",
|
|
2449
|
+
body: JSON.stringify({
|
|
2450
|
+
direction: args.direction,
|
|
2451
|
+
image: { type: "base64", base64: args.image.base64, format: args.image.format },
|
|
2452
|
+
...args.view ? { view: args.view } : {},
|
|
2453
|
+
...args.resultSize != null ? { result_size: args.resultSize } : {},
|
|
2454
|
+
...args.seed != null ? { seed: args.seed } : {}
|
|
2455
|
+
})
|
|
2456
|
+
});
|
|
2457
|
+
return validateResponse(PortraitSubmitSchema, raw, "portrait-character-pro");
|
|
2458
|
+
}
|
|
2459
|
+
/** Throws PixelLabError(423) while the job is still drawing, like tiles and map objects. */
|
|
2460
|
+
async getPortraitCharacterJob(jobId) {
|
|
2461
|
+
const raw = await this.request(`/portrait-character-pro/${encodeURIComponent(jobId)}`);
|
|
2462
|
+
return validateResponse(PortraitJobSchema, raw, "portrait-character-pro/{id}");
|
|
2463
|
+
}
|
|
2464
|
+
/**
|
|
2465
|
+
* Attaches a portrait image to a character record. Synchronous and free
|
|
2466
|
+
* (0 generations, live); PixelLab does not link this to
|
|
2467
|
+
* `createPortraitCharacterPro` automatically, and `GET /characters/{id}`
|
|
2468
|
+
* never grows a portrait field, so this response's `url` is the only place
|
|
2469
|
+
* it appears.
|
|
2470
|
+
*/
|
|
2471
|
+
async setCharacterPortrait(characterId, image) {
|
|
2472
|
+
const raw = await this.request(`/characters/${encodeURIComponent(characterId)}/portrait`, {
|
|
2473
|
+
method: "POST",
|
|
2474
|
+
body: JSON.stringify({ image: { type: "base64", base64: image.base64, format: image.format } })
|
|
2475
|
+
});
|
|
2476
|
+
return validateResponse(SetPortraitSchema, raw, "characters/{id}/portrait");
|
|
2477
|
+
}
|
|
2478
|
+
/**
|
|
2479
|
+
* Applies a reference outfit to 2–16 existing frames. Job-based like
|
|
2480
|
+
* everything else here, but the completed job returns the result frames
|
|
2481
|
+
* inline as base64 (`last_response.quantized_images`), not as storage
|
|
2482
|
+
* URLs — the one PixelLab character tool that works this way. A live
|
|
2483
|
+
* 2-frame, 92x92 job billed 20 generations (docs/ENDPOINTS.md).
|
|
2484
|
+
*/
|
|
2485
|
+
async transferOutfitV2(args) {
|
|
2486
|
+
const image = (img) => ({
|
|
2487
|
+
image: { type: "base64", base64: img.base64, format: img.format },
|
|
2488
|
+
size: { width: img.width, height: img.height }
|
|
2489
|
+
});
|
|
2490
|
+
const raw = await this.request("/transfer-outfit-v2", {
|
|
2491
|
+
method: "POST",
|
|
2492
|
+
body: JSON.stringify({
|
|
2493
|
+
reference_image: image(args.referenceImage),
|
|
2494
|
+
frames: args.frames.map(image),
|
|
2495
|
+
image_size: args.imageSize,
|
|
2496
|
+
...args.seed != null ? { seed: args.seed } : {},
|
|
2497
|
+
...args.noBackground !== void 0 ? { no_background: args.noBackground } : {},
|
|
2498
|
+
...args.additionalInstructions ? { additional_instructions: args.additionalInstructions } : {}
|
|
2499
|
+
})
|
|
2500
|
+
});
|
|
2501
|
+
return validateResponse(TransferOutfitSubmitSchema, raw, "transfer-outfit-v2");
|
|
2502
|
+
}
|
|
2379
2503
|
async getCharacter(characterId) {
|
|
2380
2504
|
const raw = await this.request(`/characters/${encodeURIComponent(characterId)}`);
|
|
2381
2505
|
return validateResponse(CharacterSchema, raw, "characters/{id}");
|
|
@@ -3392,7 +3516,16 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
3392
3516
|
styleTraits: spec.character.styleTraits,
|
|
3393
3517
|
reference: spec.character.reference ? Object.fromEntries(Object.entries(spec.character.reference).map(([direction, image]) => [direction, image.sha256])) : void 0,
|
|
3394
3518
|
concept: spec.character.concept?.sha256,
|
|
3395
|
-
styleAnchor: spec.character.styleAnchor ? { of: spec.character.styleAnchor.assetId, sha256: spec.character.styleAnchor.sha256 } : void 0
|
|
3519
|
+
styleAnchor: spec.character.styleAnchor ? { of: spec.character.styleAnchor.assetId, sha256: spec.character.styleAnchor.sha256 } : void 0,
|
|
3520
|
+
// The parent's own bytes come from its recorded outputs; the plan
|
|
3521
|
+
// compares those directly (like a mirror), so only the choice of
|
|
3522
|
+
// parent, the reference image's bytes, and any extra guidance
|
|
3523
|
+
// are identity here.
|
|
3524
|
+
outfit: spec.character.outfit ? {
|
|
3525
|
+
of: spec.character.parentAssetId,
|
|
3526
|
+
reference: spec.character.outfit.reference.sha256,
|
|
3527
|
+
additionalInstructions: spec.character.outfit.additionalInstructions
|
|
3528
|
+
} : void 0
|
|
3396
3529
|
} : void 0,
|
|
3397
3530
|
// An objectPro family: same shape as character above, minus the
|
|
3398
3531
|
// fields it has none of (mode/template/proportions/isometric/concept/
|
|
@@ -3473,7 +3606,10 @@ function characterCost(spec) {
|
|
|
3473
3606
|
if (animation.mode === "v3") return Math.max(1, Math.ceil(px * animation.frames / 65536));
|
|
3474
3607
|
return generationCost(spec.width, spec.height, "1dir");
|
|
3475
3608
|
}
|
|
3476
|
-
if (character.kind === "state") return generationCost(spec.width, spec.height, "1dir");
|
|
3609
|
+
if (character.kind === "state" || character.kind === "portrait") return generationCost(spec.width, spec.height, "1dir");
|
|
3610
|
+
if (character.kind === "outfit") {
|
|
3611
|
+
return 20;
|
|
3612
|
+
}
|
|
3477
3613
|
if (character.mode === "standard") return 1;
|
|
3478
3614
|
if (character.mode === "v3") return 1 + Math.max(1, Math.ceil(px * 8 / 65536));
|
|
3479
3615
|
if (character.mode === "pro-flash") {
|
|
@@ -3590,15 +3726,18 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3590
3726
|
return { spacingMs: 2500, maxInFlight: 8 };
|
|
3591
3727
|
}
|
|
3592
3728
|
/**
|
|
3593
|
-
* Where synchronous pixflux results
|
|
3729
|
+
* Where synchronous pixflux results (or an outfit transfer's inline
|
|
3730
|
+
* frames) are parked between `submit`/poll and `fetch`.
|
|
3594
3731
|
*
|
|
3595
|
-
* pixflux returns the PNG inline rather than a job id,
|
|
3596
|
-
*
|
|
3597
|
-
*
|
|
3598
|
-
*
|
|
3732
|
+
* pixflux returns the PNG inline rather than a job id, and an outfit
|
|
3733
|
+
* transfer's completed job returns its frames inline too, but the
|
|
3734
|
+
* pipeline is built around submit → poll → fetch running as separate
|
|
3735
|
+
* commands. Writing the bytes to a known path keeps that model intact:
|
|
3736
|
+
* the id is the filename, polling is an existence check, and downloading
|
|
3737
|
+
* is a file read.
|
|
3599
3738
|
*/
|
|
3600
|
-
static cacheDir() {
|
|
3601
|
-
const dir = import_node_path.default.join(import_node_os.default.tmpdir(),
|
|
3739
|
+
static cacheDir(namespace = "pixflux") {
|
|
3740
|
+
const dir = import_node_path.default.join(import_node_os.default.tmpdir(), `pixelkiln-${namespace}`);
|
|
3602
3741
|
(0, import_node_fs2.mkdirSync)(dir, { recursive: true });
|
|
3603
3742
|
return dir;
|
|
3604
3743
|
}
|
|
@@ -3858,6 +3997,12 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3858
3997
|
const character = spec.character;
|
|
3859
3998
|
if (!character) throw new Error(`${spec.styleId}/${spec.assetId} has no character shape`);
|
|
3860
3999
|
const label = `${spec.styleId}/${spec.assetId}`;
|
|
4000
|
+
if (character.kind === "outfit") {
|
|
4001
|
+
const reference = character.outfit.reference;
|
|
4002
|
+
if (reference.width < 32 || reference.width > 256 || reference.height < 32 || reference.height > 256) {
|
|
4003
|
+
throw new Error(`${label}: outfit reference is ${reference.width}x${reference.height}; PixelLab transfer-outfit-v2 takes 32 to 256px per side`);
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
3861
4006
|
if (character.kind === "base" && character.proportions !== void 0) {
|
|
3862
4007
|
if (character.mode !== "standard") {
|
|
3863
4008
|
throw new Error(`${label}: proportions apply to standard bases; the ${character.mode} engine has none to set`);
|
|
@@ -3869,7 +4014,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3869
4014
|
if (character.kind === "base" && character.enhancePrompt !== void 0 && character.mode !== "v3") {
|
|
3870
4015
|
throw new Error(`${label}: enhancePrompt applies to v3 bases; the ${character.mode} engine does not take it`);
|
|
3871
4016
|
}
|
|
3872
|
-
if (character.animation) {
|
|
4017
|
+
if (character.animation && character.kind !== "outfit") {
|
|
3873
4018
|
const { startFrame, endFrame } = character.animation;
|
|
3874
4019
|
for (const [name, image] of [["start frame", startFrame], ["end frame", endFrame]]) {
|
|
3875
4020
|
if (image && (image.width > 256 || image.height > 256)) {
|
|
@@ -3950,9 +4095,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3950
4095
|
if (character.styleTraits && character.mode !== "pro-flash") {
|
|
3951
4096
|
throw new Error(`${label}: styleTraits choose what a pro-flash style image lends; the ${character.mode} engine does not take them`);
|
|
3952
4097
|
}
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
4098
|
+
if (character.kind !== "portrait") {
|
|
4099
|
+
const maxSize = character.mode === "v3" || character.mode === "pro-flash" ? 256 : 128;
|
|
4100
|
+
if (spec.width < 16 || spec.height < 16 || spec.width > maxSize || spec.height > maxSize) {
|
|
4101
|
+
throw new Error(`PixelLab ${character.mode} characters must be between 16 and ${maxSize} pixels`);
|
|
4102
|
+
}
|
|
3956
4103
|
}
|
|
3957
4104
|
if (character.mode === "pro-flash" && character.kind === "base" && (spec.width % 4 || spec.height % 4)) {
|
|
3958
4105
|
throw new Error(`PixelLab pro-flash characters are sized in multiples of 4 pixels; ${spec.width}x${spec.height} is not`);
|
|
@@ -3961,9 +4108,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3961
4108
|
if (!templates.includes(character.template)) {
|
|
3962
4109
|
throw new Error(`PixelLab ${character.mode} character template must be one of: ${templates.join(", ")}`);
|
|
3963
4110
|
}
|
|
3964
|
-
const views = character.mode === "standard" ? ["low top-down", "high top-down", "side", "perspective", "oblique"] : ["low top-down", "high top-down", "side"];
|
|
4111
|
+
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"];
|
|
3965
4112
|
if (!views.includes(spec.view)) {
|
|
3966
|
-
throw new Error(
|
|
4113
|
+
throw new Error(
|
|
4114
|
+
character.kind === "portrait" ? `PixelLab portrait view must be one of: ${views.join(", ")}` : `PixelLab ${character.mode} character view must be one of: ${views.join(", ")}`
|
|
4115
|
+
);
|
|
3967
4116
|
}
|
|
3968
4117
|
if (spec.view === "oblique" && character.directions !== 4) {
|
|
3969
4118
|
throw new Error("PixelLab oblique characters are 4-direction only");
|
|
@@ -3971,7 +4120,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3971
4120
|
if (character.kind !== "base" && !character.parentAssetId) {
|
|
3972
4121
|
throw new Error(`${spec.styleId}/${spec.assetId} is a ${character.kind} with no parent`);
|
|
3973
4122
|
}
|
|
3974
|
-
if (character.animation) {
|
|
4123
|
+
if (character.animation && character.kind !== "outfit") {
|
|
3975
4124
|
const allowed = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
|
|
3976
4125
|
if (!allowed.includes(character.animation.direction)) {
|
|
3977
4126
|
throw new Error(
|
|
@@ -4077,6 +4226,43 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4077
4226
|
});
|
|
4078
4227
|
return { jobId: res2.character_id, metadata: { character: { kind: "base", characterId: res2.character_id, mode: character.mode, directions: character.directions, backgroundJobId: res2.background_job_id } } };
|
|
4079
4228
|
}
|
|
4229
|
+
if (character.kind === "outfit") {
|
|
4230
|
+
const parentOutputs = context?.parentOutputs ?? [];
|
|
4231
|
+
if (parentOutputs.length < 2) {
|
|
4232
|
+
throw new Error(
|
|
4233
|
+
`${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`
|
|
4234
|
+
);
|
|
4235
|
+
}
|
|
4236
|
+
const frames = parentOutputs.slice(0, 16).map((output) => {
|
|
4237
|
+
const bytes = (0, import_node_fs2.readFileSync)(output.path);
|
|
4238
|
+
const dims = imageMetadata(bytes);
|
|
4239
|
+
if (!dims) throw new Error(`${spec.styleId}/${spec.assetId}: outfit source frame is not a readable PNG: ${output.path}`);
|
|
4240
|
+
return { base64: bytes.toString("base64"), format: "png", width: dims.width, height: dims.height };
|
|
4241
|
+
});
|
|
4242
|
+
const outfit = character.outfit;
|
|
4243
|
+
const referenceBytes = (0, import_node_fs2.readFileSync)(outfit.reference.path);
|
|
4244
|
+
if (sha256(referenceBytes) !== outfit.reference.sha256) {
|
|
4245
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: outfit reference changed after the manifest was resolved: ${outfit.reference.path}`);
|
|
4246
|
+
}
|
|
4247
|
+
const res2 = await this.client.transferOutfitV2({
|
|
4248
|
+
referenceImage: { base64: referenceBytes.toString("base64"), format: "png", width: outfit.reference.width, height: outfit.reference.height },
|
|
4249
|
+
frames,
|
|
4250
|
+
imageSize: { width: spec.width, height: spec.height },
|
|
4251
|
+
seed: spec.seed,
|
|
4252
|
+
noBackground: spec.noBackground,
|
|
4253
|
+
additionalInstructions: outfit.additionalInstructions
|
|
4254
|
+
});
|
|
4255
|
+
return {
|
|
4256
|
+
jobId: res2.background_job_id,
|
|
4257
|
+
metadata: {
|
|
4258
|
+
character: {
|
|
4259
|
+
kind: "outfit",
|
|
4260
|
+
backgroundJobId: res2.background_job_id,
|
|
4261
|
+
frameRoles: parentOutputs.slice(0, 16).map((output) => output.role ?? null)
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
};
|
|
4265
|
+
}
|
|
4080
4266
|
const parentId = context?.parentObjectId;
|
|
4081
4267
|
if (!parentId) {
|
|
4082
4268
|
throw new Error(
|
|
@@ -4095,6 +4281,24 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4095
4281
|
});
|
|
4096
4282
|
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 } } };
|
|
4097
4283
|
}
|
|
4284
|
+
if (character.kind === "portrait") {
|
|
4285
|
+
const parentFile = character.parentFile;
|
|
4286
|
+
const bytes = (0, import_node_fs2.readFileSync)(parentFile);
|
|
4287
|
+
if (character.parentSha256 && sha256(bytes) !== character.parentSha256) {
|
|
4288
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: portrait source changed after the manifest was resolved: ${parentFile}`);
|
|
4289
|
+
}
|
|
4290
|
+
const res2 = await this.client.createPortraitCharacterPro({
|
|
4291
|
+
direction: "character_to_portrait",
|
|
4292
|
+
image: { base64: bytes.toString("base64"), format: "png" },
|
|
4293
|
+
view: spec.view,
|
|
4294
|
+
resultSize: character.portrait.size,
|
|
4295
|
+
seed: spec.seed
|
|
4296
|
+
});
|
|
4297
|
+
return {
|
|
4298
|
+
jobId: res2.background_job_id,
|
|
4299
|
+
metadata: { character: { kind: "portrait", parentCharacterId: parentId, backgroundJobId: res2.background_job_id } }
|
|
4300
|
+
};
|
|
4301
|
+
}
|
|
4098
4302
|
const animation = character.animation;
|
|
4099
4303
|
const name = characterAnimationName(spec);
|
|
4100
4304
|
const replaced = context?.replacedMetadata?.character ?? {};
|
|
@@ -4208,6 +4412,9 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4208
4412
|
async pollCharacter(jobId, context) {
|
|
4209
4413
|
const animation = decodeAnimationJob(jobId);
|
|
4210
4414
|
if (animation) return this.pollCharacterAnimation(animation, context);
|
|
4415
|
+
const kind = context?.metadata?.character?.kind ?? context?.spec?.character?.kind;
|
|
4416
|
+
if (kind === "portrait") return this.pollCharacterPortrait(jobId, context);
|
|
4417
|
+
if (kind === "outfit") return this.pollCharacterOutfit(jobId, context);
|
|
4211
4418
|
const character = await this.client.getCharacter(jobId);
|
|
4212
4419
|
if (character.status === "failed") return { status: "failed", error: "character generation failed upstream" };
|
|
4213
4420
|
if (character.status !== "completed" || !character.rotation_urls) return { status: "processing" };
|
|
@@ -4233,6 +4440,66 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4233
4440
|
billed: await this.billedForJob(backgroundJobId)
|
|
4234
4441
|
};
|
|
4235
4442
|
}
|
|
4443
|
+
/**
|
|
4444
|
+
* `jobId` here is `portrait-character-pro`'s own background job id, not a
|
|
4445
|
+
* character id: the endpoint converts an arbitrary image and never touches
|
|
4446
|
+
* the character record. Once the portrait itself is drawn, this also
|
|
4447
|
+
* attaches it to the parent's character record with `SetPortrait` — a
|
|
4448
|
+
* separate, free call PixelLab does not make automatically (docs/PIXELLAB.md).
|
|
4449
|
+
*/
|
|
4450
|
+
async pollCharacterPortrait(jobId, context) {
|
|
4451
|
+
const meta = context?.metadata?.character;
|
|
4452
|
+
try {
|
|
4453
|
+
const job = await this.client.getPortraitCharacterJob(jobId);
|
|
4454
|
+
if (job.status === "failed") return { status: "failed", error: "portrait generation failed upstream" };
|
|
4455
|
+
if (job.status !== "completed" || !job.download_url) return { status: "processing" };
|
|
4456
|
+
if (meta?.parentCharacterId) {
|
|
4457
|
+
const bytes = await this.client.download(job.download_url);
|
|
4458
|
+
await this.client.setCharacterPortrait(meta.parentCharacterId, { base64: bytes.toString("base64"), format: "png" });
|
|
4459
|
+
}
|
|
4460
|
+
return {
|
|
4461
|
+
status: "ready",
|
|
4462
|
+
objectId: jobId,
|
|
4463
|
+
sourceUrl: job.download_url,
|
|
4464
|
+
sources: [{ url: job.download_url }],
|
|
4465
|
+
billed: await this.billedForJob(meta?.backgroundJobId)
|
|
4466
|
+
};
|
|
4467
|
+
} catch (err) {
|
|
4468
|
+
if (err instanceof PixelLabError && err.status === 423) return { status: "processing" };
|
|
4469
|
+
throw err;
|
|
4470
|
+
}
|
|
4471
|
+
}
|
|
4472
|
+
/**
|
|
4473
|
+
* No dedicated status endpoint exists for `transfer-outfit-v2`, unlike
|
|
4474
|
+
* portraits and tiles: the background job is the only place to poll. A
|
|
4475
|
+
* completed job returns its frames inline as base64
|
|
4476
|
+
* (`last_response.quantized_images`), not as storage URLs, so they are
|
|
4477
|
+
* cached locally the way a synchronous pixflux result is, and matched back
|
|
4478
|
+
* to the source frames' own roles recorded at submit time.
|
|
4479
|
+
*/
|
|
4480
|
+
async pollCharacterOutfit(jobId, context) {
|
|
4481
|
+
const meta = context?.metadata?.character;
|
|
4482
|
+
const job = await this.client.getBackgroundJob(jobId);
|
|
4483
|
+
if (job.status === "failed") return { status: "failed", error: "outfit transfer failed upstream" };
|
|
4484
|
+
if (job.status !== "completed") return { status: "processing" };
|
|
4485
|
+
const images = job.last_response?.quantized_images;
|
|
4486
|
+
if (!images?.length) return { status: "failed", error: "outfit transfer completed with no result frames" };
|
|
4487
|
+
const dir = _PixelLabProvider.cacheDir("outfit");
|
|
4488
|
+
const sources = images.map((image, index) => {
|
|
4489
|
+
if (!image.base64) throw new Error(`outfit transfer frame ${index} has no image data`);
|
|
4490
|
+
const role = meta?.frameRoles?.[index] ?? void 0;
|
|
4491
|
+
const file = import_node_path.default.join(dir, `${jobId}-${index}.png`);
|
|
4492
|
+
(0, import_node_fs2.writeFileSync)(file, Buffer.from(image.base64, "base64"));
|
|
4493
|
+
return { url: `file://${file}`, ...role ? { role } : {} };
|
|
4494
|
+
});
|
|
4495
|
+
return {
|
|
4496
|
+
status: "ready",
|
|
4497
|
+
objectId: jobId,
|
|
4498
|
+
sourceUrl: sources[0].url,
|
|
4499
|
+
sources,
|
|
4500
|
+
billed: await this.billedForJob(meta?.backgroundJobId)
|
|
4501
|
+
};
|
|
4502
|
+
}
|
|
4236
4503
|
/**
|
|
4237
4504
|
* The background job is the authoritative record of one direction: it
|
|
4238
4505
|
* completes with the frame URLs, the animation id, and the direction, and
|
|
@@ -5052,8 +5319,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5052
5319
|
const sources = [];
|
|
5053
5320
|
const terrainTiles = [];
|
|
5054
5321
|
for (const [index, tile] of tileset.tiles.entries()) {
|
|
5055
|
-
const
|
|
5056
|
-
const role = `tile-${String(index).padStart(2, "0")}-${
|
|
5322
|
+
const slug2 = tile.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "tile";
|
|
5323
|
+
const role = `tile-${String(index).padStart(2, "0")}-${slug2}`;
|
|
5057
5324
|
const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${tilesetId}-${index}.png`);
|
|
5058
5325
|
(0, import_node_fs2.writeFileSync)(file, Buffer.from(tile.image.base64, "base64"));
|
|
5059
5326
|
sources.push({ url: `file://${file}`, role });
|
|
@@ -5172,6 +5439,9 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5172
5439
|
async delete(assetId) {
|
|
5173
5440
|
await this.client.deleteObject(assetId);
|
|
5174
5441
|
}
|
|
5442
|
+
async deleteCharacter(id) {
|
|
5443
|
+
await this.client.deleteCharacter(id);
|
|
5444
|
+
}
|
|
5175
5445
|
async *listCharacters() {
|
|
5176
5446
|
for await (const character of this.client.iterateCharacters(100)) {
|
|
5177
5447
|
yield remoteCharacter(character);
|
|
@@ -7195,6 +7465,8 @@ var FakeProvider = class {
|
|
|
7195
7465
|
this.tags.set(objectId, [...tags]);
|
|
7196
7466
|
const asset = this.assets.get(objectId);
|
|
7197
7467
|
if (asset) asset.tags = [...tags];
|
|
7468
|
+
const character = this.characters.get(objectId.split("#")[0]);
|
|
7469
|
+
if (character) character.tags = [...tags];
|
|
7198
7470
|
}
|
|
7199
7471
|
/**
|
|
7200
7472
|
* Assigned in the constructor rather than declared as a method, so a test can
|
|
@@ -7229,6 +7501,10 @@ var FakeProvider = class {
|
|
|
7229
7501
|
this.deleted.push(assetId);
|
|
7230
7502
|
this.assets.delete(assetId);
|
|
7231
7503
|
}
|
|
7504
|
+
async deleteCharacter(id) {
|
|
7505
|
+
this.deleted.push(id);
|
|
7506
|
+
this.characters.delete(id);
|
|
7507
|
+
}
|
|
7232
7508
|
/** Seed a pre-existing remote asset, for adopt / salvage tests. */
|
|
7233
7509
|
seed(asset) {
|
|
7234
7510
|
const full = {
|
|
@@ -7822,7 +8098,7 @@ ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
|
7822
8098
|
return { manifest: parsed.data, root: import_node_path5.default.dirname(abs), path: abs, ...loopFamilies };
|
|
7823
8099
|
}
|
|
7824
8100
|
function parentAssetId(asset) {
|
|
7825
|
-
return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of;
|
|
8101
|
+
return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of ?? asset?.portrait?.of ?? asset?.outfit?.of;
|
|
7826
8102
|
}
|
|
7827
8103
|
function formatManifestIssues(issues) {
|
|
7828
8104
|
return issues.flatMap((issue) => {
|
|
@@ -7989,9 +8265,9 @@ async function resolveSpecs(loaded, filter) {
|
|
|
7989
8265
|
height = styleImageDimensions.length ? Math.max(...styleImageDimensions.map((img) => img.height)) : style.tileSize ?? 32;
|
|
7990
8266
|
size = Math.max(width, height);
|
|
7991
8267
|
} else if (generator === "character") {
|
|
7992
|
-
size = asset.state?.canvas ? Math.max(asset.state.canvas.width, asset.state.canvas.height) : asset.size ?? style.size ?? 64;
|
|
7993
|
-
width = asset.state?.canvas?.width ?? size;
|
|
7994
|
-
height = asset.state?.canvas?.height ?? size;
|
|
8268
|
+
size = asset.portrait ? asset.portrait.size : asset.state?.canvas ? Math.max(asset.state.canvas.width, asset.state.canvas.height) : asset.size ?? style.size ?? 64;
|
|
8269
|
+
width = asset.portrait?.size ?? asset.state?.canvas?.width ?? size;
|
|
8270
|
+
height = asset.portrait?.size ?? asset.state?.canvas?.height ?? size;
|
|
7995
8271
|
} else if (generator === "terrain") {
|
|
7996
8272
|
size = style.terrainTileSize ?? 16;
|
|
7997
8273
|
width = size;
|
|
@@ -8018,12 +8294,17 @@ async function resolveSpecs(loaded, filter) {
|
|
|
8018
8294
|
`assets.${assetId}: ${asset.state ? "state" : "animation"} needs a character or objectPro style; "${styleId}" generates ${generator}`
|
|
8019
8295
|
);
|
|
8020
8296
|
}
|
|
8297
|
+
if ((asset.portrait || asset.outfit) && generator !== "character") {
|
|
8298
|
+
throw new Error(
|
|
8299
|
+
`assets.${assetId}: ${asset.portrait ? "portrait" : "outfit"} needs a character style; "${styleId}" generates ${generator}`
|
|
8300
|
+
);
|
|
8301
|
+
}
|
|
8021
8302
|
if ((asset.pieces || asset.elements) && generator !== "uiAsset") {
|
|
8022
8303
|
throw new Error(
|
|
8023
8304
|
`assets.${assetId}: ${asset.pieces ? "pieces" : "elements"} needs a uiAsset style; "${styleId}" generates ${generator}`
|
|
8024
8305
|
);
|
|
8025
8306
|
}
|
|
8026
|
-
const characterKind = asset.animation ? "animation" : asset.state ? "state" : "base";
|
|
8307
|
+
const characterKind = asset.animation ? "animation" : asset.state ? "state" : asset.portrait ? "portrait" : "base";
|
|
8027
8308
|
const subject = asset.promptByStyle[styleId] ?? asset.prompt ?? "";
|
|
8028
8309
|
let terrainLowerDescription;
|
|
8029
8310
|
let terrainUpperDescription;
|
|
@@ -8115,7 +8396,9 @@ async function resolveSpecs(loaded, filter) {
|
|
|
8115
8396
|
// its parent's pixels, not a new character: giving it a base shape
|
|
8116
8397
|
// would have packing, the gallery, and adoption treat it as one.
|
|
8117
8398
|
...generator === "character" && !asset.revision ? { character: await resolveCharacterShape(asset, style, characterKind, { root, load: loadStyleImage }) } : {},
|
|
8118
|
-
|
|
8399
|
+
// A portrait needs a character style (refused above), so an
|
|
8400
|
+
// objectPro asset is only ever a base, state, or animation.
|
|
8401
|
+
...generator === "objectPro" && !asset.revision && characterKind !== "portrait" ? { objectPro: await resolveObjectProShape(assetId, asset, style, characterKind, { root, load: loadStyleImage }) } : {},
|
|
8119
8402
|
cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? tilesCost(size, terrainTiles) : generator === "isometricTile" ? isometricTileCost() : generationCost(width, height, generator),
|
|
8120
8403
|
costUnit: "generations",
|
|
8121
8404
|
candidates: generator === "tiles" ? tileVariations : generator === "terrain" ? terrainTiles : generator === "1dir" || generator === "imagePro" ? candidateCount(size) : 1
|
|
@@ -8226,7 +8509,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
8226
8509
|
styleAnchor: { assetId: anchorId, spec: anchorSpec, file, sha256: (0, import_node_fs5.existsSync)(file) ? await sha256File(file) : null }
|
|
8227
8510
|
};
|
|
8228
8511
|
}
|
|
8229
|
-
const characterParent = asset.state?.of ?? asset.animation?.of;
|
|
8512
|
+
const characterParent = asset.state?.of ?? asset.animation?.of ?? asset.portrait?.of;
|
|
8230
8513
|
if (resolved.character && characterParent) {
|
|
8231
8514
|
const parentSpec = await finalize(characterParent);
|
|
8232
8515
|
const parentDirections = parentSpec.character?.directions ?? 8;
|
|
@@ -8259,6 +8542,38 @@ async function resolveSpecs(loaded, filter) {
|
|
|
8259
8542
|
directions: resolved.objectPro.kind === "base" ? resolved.objectPro.directions : parentSpec.objectPro.directions
|
|
8260
8543
|
};
|
|
8261
8544
|
}
|
|
8545
|
+
if (asset.outfit) {
|
|
8546
|
+
if (asset.outfit.of === assetId) throw new Error(`assets.${assetId}: an asset cannot re-clothe itself`);
|
|
8547
|
+
const sourceSpec = await finalize(asset.outfit.of);
|
|
8548
|
+
if (sourceSpec.character?.kind !== "animation") {
|
|
8549
|
+
throw new Error(`assets.${assetId}: outfit "${asset.outfit.of}" is not a character loop`);
|
|
8550
|
+
}
|
|
8551
|
+
const referenceImage = await loadStyleImage(asset.outfit.reference, "Outfit reference image");
|
|
8552
|
+
resolved.character = {
|
|
8553
|
+
kind: "outfit",
|
|
8554
|
+
mode: sourceSpec.character.mode,
|
|
8555
|
+
directions: sourceSpec.character.directions,
|
|
8556
|
+
template: sourceSpec.character.template,
|
|
8557
|
+
parentAssetId: asset.outfit.of,
|
|
8558
|
+
parentSpec: sourceSpec,
|
|
8559
|
+
animation: sourceSpec.character.animation,
|
|
8560
|
+
outfit: {
|
|
8561
|
+
reference: {
|
|
8562
|
+
path: import_node_path5.default.resolve(root, asset.outfit.reference),
|
|
8563
|
+
sha256: referenceImage.hash,
|
|
8564
|
+
width: referenceImage.width,
|
|
8565
|
+
height: referenceImage.height,
|
|
8566
|
+
format: referenceImage.format
|
|
8567
|
+
},
|
|
8568
|
+
...asset.outfit.additionalInstructions ? { additionalInstructions: asset.outfit.additionalInstructions } : {}
|
|
8569
|
+
}
|
|
8570
|
+
};
|
|
8571
|
+
resolved.generator = sourceSpec.generator;
|
|
8572
|
+
resolved.width = sourceSpec.width;
|
|
8573
|
+
resolved.height = sourceSpec.height;
|
|
8574
|
+
resolved.size = sourceSpec.size;
|
|
8575
|
+
resolved.prompt = "";
|
|
8576
|
+
}
|
|
8262
8577
|
if (asset.revision) {
|
|
8263
8578
|
if (!activeProvider.supportsRevision?.(asset.revision.mode)) {
|
|
8264
8579
|
throw new Error(
|
|
@@ -8532,6 +8847,9 @@ async function resolveCharacterShape(asset, style, kind, files) {
|
|
|
8532
8847
|
...asset.state.canvas ? { canvas: asset.state.canvas } : {}
|
|
8533
8848
|
};
|
|
8534
8849
|
}
|
|
8850
|
+
if (asset.portrait) {
|
|
8851
|
+
shape.portrait = { size: asset.portrait.size };
|
|
8852
|
+
}
|
|
8535
8853
|
if (asset.animation) {
|
|
8536
8854
|
const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
|
|
8537
8855
|
const pose = async (rel, what) => {
|
|
@@ -10084,6 +10402,7 @@ var import_node_fs8 = require("fs");
|
|
|
10084
10402
|
var import_node_path11 = __toESM(require("path"), 1);
|
|
10085
10403
|
async function inspectRevisionReadiness(spec, lock) {
|
|
10086
10404
|
if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
|
|
10405
|
+
if (spec.character?.kind === "outfit") return inspectOutfit(spec, lock, /* @__PURE__ */ new Set());
|
|
10087
10406
|
if (spec.character?.parentSpec) return inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
|
|
10088
10407
|
if (spec.character?.styleAnchor) return inspectStyleAnchor(spec, lock, /* @__PURE__ */ new Set());
|
|
10089
10408
|
if (spec.objectPro?.parentSpec) return inspectObjectProParent(spec, lock, /* @__PURE__ */ new Set());
|
|
@@ -10110,6 +10429,20 @@ async function inspectMirror(spec, lock, seen) {
|
|
|
10110
10429
|
const dependency = await inspectParent(source, lock, seen);
|
|
10111
10430
|
return dependency.ready ? { ready: true, reason: `${label} is current` } : { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
|
|
10112
10431
|
}
|
|
10432
|
+
async function inspectOutfit(spec, lock, seen) {
|
|
10433
|
+
const character = spec.character;
|
|
10434
|
+
const outfit = character.outfit;
|
|
10435
|
+
const label = `outfit source ${spec.styleId}/${character.parentAssetId}`;
|
|
10436
|
+
const dependency = await inspectParent(character.parentSpec, lock, seen);
|
|
10437
|
+
if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
|
|
10438
|
+
if (!(0, import_node_fs8.existsSync)(outfit.reference.path)) {
|
|
10439
|
+
return { ready: false, reason: `outfit reference is missing: ${outfit.reference.path}` };
|
|
10440
|
+
}
|
|
10441
|
+
if (await sha256File(outfit.reference.path) !== outfit.reference.sha256) {
|
|
10442
|
+
return { ready: false, reason: "outfit reference changed after the manifest was resolved" };
|
|
10443
|
+
}
|
|
10444
|
+
return { ready: true, reason: `${label} is current` };
|
|
10445
|
+
}
|
|
10113
10446
|
async function inspectCharacterParent(spec, lock, seen) {
|
|
10114
10447
|
const character = spec.character;
|
|
10115
10448
|
const parentSpec = character.parentSpec;
|
|
@@ -10225,7 +10558,10 @@ async function inspectParent(spec, lock, seen) {
|
|
|
10225
10558
|
const inputs = await inspectRevision(spec, lock, nextSeen);
|
|
10226
10559
|
if (!inputs.ready) return inputs;
|
|
10227
10560
|
}
|
|
10228
|
-
if (spec.character?.
|
|
10561
|
+
if (spec.character?.kind === "outfit") {
|
|
10562
|
+
const inputs = await inspectOutfit(spec, lock, nextSeen);
|
|
10563
|
+
if (!inputs.ready) return inputs;
|
|
10564
|
+
} else if (spec.character?.parentSpec) {
|
|
10229
10565
|
const inputs = await inspectCharacterParent(spec, lock, nextSeen);
|
|
10230
10566
|
if (!inputs.ready) return inputs;
|
|
10231
10567
|
}
|
|
@@ -10348,6 +10684,9 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
10348
10684
|
} else if (spec.mirror && entry.status === "downloaded" && entry.outputs.some((_output, index) => !(0, import_node_fs9.existsSync)(currentEntryOutputPath(entry, spec, index)))) {
|
|
10349
10685
|
state = "stale";
|
|
10350
10686
|
reason = "mirror output missing on disk; flip it again (no generation cost)";
|
|
10687
|
+
} else if (spec.character?.kind === "outfit" && entry.outfit?.sourceSha256 !== mirrorSourceHash(lock.entries[lockKey(spec.styleId, spec.character.parentAssetId)] ?? { outputs: [] })) {
|
|
10688
|
+
state = "stale";
|
|
10689
|
+
reason = `outfit source ${spec.character.parentAssetId} changed; re-clothe the loop again`;
|
|
10351
10690
|
} else if (entry.status === "download-failed") {
|
|
10352
10691
|
state = "recoverable";
|
|
10353
10692
|
const force = entry.error?.includes("pass --force") ? " --force" : "";
|
|
@@ -13045,6 +13384,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
13045
13384
|
const supersededOutputs = previousEntry?.outputs.length ? previousEntry.outputs : previousEntry?.supersededOutputs ?? [];
|
|
13046
13385
|
const submittedAt = resumesCheckpoint ? previousEntry.submittedAt : (/* @__PURE__ */ new Date()).toISOString();
|
|
13047
13386
|
const history = historyAfterReplacing(previousEntry, historyLimit(loaded.manifest), submittedAt ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
13387
|
+
const outfitParentEntry = spec.character?.kind === "outfit" && spec.character.parentSpec ? lock.entries[lockKey(spec.character.parentSpec.styleId, spec.character.parentSpec.assetId)] : void 0;
|
|
13048
13388
|
upsert(lock, key, {
|
|
13049
13389
|
styleId: spec.styleId,
|
|
13050
13390
|
assetId: spec.assetId,
|
|
@@ -13070,6 +13410,11 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
13070
13410
|
...spec.revision.direction ? { direction: spec.revision.direction } : {},
|
|
13071
13411
|
...spec.revision.enhancePrompt == null ? {} : { enhancePrompt: spec.revision.enhancePrompt }
|
|
13072
13412
|
} : null,
|
|
13413
|
+
outfit: spec.character?.kind === "outfit" && outfitParentEntry ? {
|
|
13414
|
+
sourceAssetId: spec.character.parentAssetId,
|
|
13415
|
+
sourceSha256: mirrorSourceHash(outfitParentEntry),
|
|
13416
|
+
referenceSha256: spec.character.outfit.reference.sha256
|
|
13417
|
+
} : null,
|
|
13073
13418
|
status: "pending",
|
|
13074
13419
|
jobId: previousJobId ?? null,
|
|
13075
13420
|
...previousJobId ? { submissionComplete: false } : { submissionComplete: void 0 },
|
|
@@ -13103,10 +13448,15 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
13103
13448
|
const anchor = spec.character?.styleAnchor;
|
|
13104
13449
|
const anchorEntry = anchor ? lock.entries[lockKey(anchor.spec.styleId, anchor.spec.assetId)] : void 0;
|
|
13105
13450
|
const replacedMetadata = previousEntry?.providerMetadata?.[provider.id];
|
|
13451
|
+
const parentOutputs = spec.character?.kind === "outfit" && parentEntry ? parentEntry.outputs.map((output, index) => ({
|
|
13452
|
+
path: currentEntryOutputPath(parentEntry, spec.character.parentSpec, index),
|
|
13453
|
+
role: output.role
|
|
13454
|
+
})) : void 0;
|
|
13106
13455
|
const { jobId, metadata } = await provider.submit(spec, refs, {
|
|
13107
13456
|
...previousJobId ? { previousJobId } : {},
|
|
13108
13457
|
...parentEntry?.objectId ? { parentObjectId: parentEntry.objectId } : {},
|
|
13109
13458
|
...anchorEntry?.objectId ? { styleObjectId: anchorEntry.objectId } : {},
|
|
13459
|
+
...parentOutputs ? { parentOutputs } : {},
|
|
13110
13460
|
...replacedMetadata ? { replacedMetadata } : {},
|
|
13111
13461
|
...previousMetadata ? { previousMetadata } : {},
|
|
13112
13462
|
checkpoint: async (checkpoint) => {
|
|
@@ -15225,7 +15575,7 @@ function describeCharacter(spec, entry) {
|
|
|
15225
15575
|
};
|
|
15226
15576
|
}
|
|
15227
15577
|
if (entry?.generator === "character") {
|
|
15228
|
-
const kind = recordedCharacter?.kind === "animation" || entry.outputs.some((o) => o.role?.startsWith("frame-")) ? "animation" : "base";
|
|
15578
|
+
const kind = recordedCharacter?.kind === "portrait" ? "portrait" : recordedCharacter?.kind === "outfit" ? "outfit" : recordedCharacter?.kind === "animation" || entry.outputs.some((o) => o.role?.startsWith("frame-")) ? "animation" : "base";
|
|
15229
15579
|
return {
|
|
15230
15580
|
generator: "character",
|
|
15231
15581
|
kind,
|
|
@@ -15586,7 +15936,9 @@ async function buildGallerySnapshot(opts) {
|
|
|
15586
15936
|
characters: style?.generator === "character" || style?.generator === "objectPro" ? {
|
|
15587
15937
|
bases: styleItems.filter((item) => item.character?.kind === "base").length,
|
|
15588
15938
|
states: styleItems.filter((item) => item.character?.kind === "state").length,
|
|
15589
|
-
animations: styleItems.filter((item) => item.character?.kind === "animation").length
|
|
15939
|
+
animations: styleItems.filter((item) => item.character?.kind === "animation").length,
|
|
15940
|
+
portraits: styleItems.filter((item) => item.character?.kind === "portrait").length,
|
|
15941
|
+
outfits: styleItems.filter((item) => item.character?.kind === "outfit").length
|
|
15590
15942
|
} : null,
|
|
15591
15943
|
tags: style?.tags ?? [],
|
|
15592
15944
|
extends: typeof rawStyle?.extends === "string" ? rawStyle.extends : null,
|
|
@@ -16753,6 +17105,9 @@ async function loadClaims(lockPaths, opts = {}) {
|
|
|
16753
17105
|
add(entry.objectId);
|
|
16754
17106
|
add(entry.reviewObjectId);
|
|
16755
17107
|
add(entry.jobId);
|
|
17108
|
+
for (const id of [entry.objectId, entry.jobId]) {
|
|
17109
|
+
if (id?.includes("#")) add(id.split("#")[0]);
|
|
17110
|
+
}
|
|
16756
17111
|
}
|
|
16757
17112
|
}
|
|
16758
17113
|
return claimed;
|
|
@@ -16776,6 +17131,54 @@ async function findOrphans(provider, claimed, opts = {}) {
|
|
|
16776
17131
|
})).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
16777
17132
|
return { orphans, total: all.length };
|
|
16778
17133
|
}
|
|
17134
|
+
function isState(character) {
|
|
17135
|
+
return Boolean(character.stateName && character.stateName !== "Idle");
|
|
17136
|
+
}
|
|
17137
|
+
function characterSalvageAssets(characters, ctx) {
|
|
17138
|
+
const ordered = [...characters].sort((a, b) => Number(isState(a)) - Number(isState(b)) || a.createdAt.localeCompare(b.createdAt));
|
|
17139
|
+
const base = ordered[0];
|
|
17140
|
+
const common = { styles: [ctx.styleId], tags: ["salvaged"] };
|
|
17141
|
+
const assets = {};
|
|
17142
|
+
const skipped = [];
|
|
17143
|
+
const baseId = idFromPrompt(base.name || base.prompt, ctx.taken);
|
|
17144
|
+
const size = base.width === base.height && base.width >= 32 && base.width <= 256 && base.width !== ctx.styleSize ? { size: base.width } : {};
|
|
17145
|
+
assets[baseId] = { prompt: base.prompt || base.name || baseId, remoteId: base.id, ...size, ...common };
|
|
17146
|
+
const assetIdOf = /* @__PURE__ */ new Map([[base.id, baseId]]);
|
|
17147
|
+
for (const state of ordered.slice(1)) {
|
|
17148
|
+
const stateId = uniqueId(`${baseId}_${slug(state.stateName ?? "state")}`, ctx.taken);
|
|
17149
|
+
assetIdOf.set(state.id, stateId);
|
|
17150
|
+
assets[stateId] = { prompt: state.prompt || state.stateName || stateId, state: { of: baseId }, remoteId: state.id, ...common };
|
|
17151
|
+
}
|
|
17152
|
+
for (const character of ordered) {
|
|
17153
|
+
const parentId = assetIdOf.get(character.id);
|
|
17154
|
+
for (const animation of character.animations) {
|
|
17155
|
+
if (!animation.frames.length) continue;
|
|
17156
|
+
const label = animation.name || animation.type;
|
|
17157
|
+
if (!animation.groupId) {
|
|
17158
|
+
skipped.push(`${parentId}: "${label}" ${animation.direction} loop has no animation group id to adopt it by`);
|
|
17159
|
+
continue;
|
|
17160
|
+
}
|
|
17161
|
+
const loopId = uniqueId(`${parentId}.${slug(label)}.${animation.direction}`, ctx.taken);
|
|
17162
|
+
assets[loopId] = {
|
|
17163
|
+
prompt: label,
|
|
17164
|
+
animation: { of: parentId, direction: animation.direction },
|
|
17165
|
+
remoteId: `${character.id}#${animation.groupId}`,
|
|
17166
|
+
...common
|
|
17167
|
+
};
|
|
17168
|
+
}
|
|
17169
|
+
}
|
|
17170
|
+
return { assets, skipped };
|
|
17171
|
+
}
|
|
17172
|
+
function slug(text) {
|
|
17173
|
+
return text.toLowerCase().replace(/^pixelkiln:.*\//, "").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "loop";
|
|
17174
|
+
}
|
|
17175
|
+
function uniqueId(base, taken) {
|
|
17176
|
+
let id = base;
|
|
17177
|
+
let n = 2;
|
|
17178
|
+
while (taken.has(id)) id = `${base}_${n++}`;
|
|
17179
|
+
taken.add(id);
|
|
17180
|
+
return id;
|
|
17181
|
+
}
|
|
16779
17182
|
var MIN_STYLE_FRAGMENT_LENGTH = 12;
|
|
16780
17183
|
function matchStyleByPattern(prompt, manifest) {
|
|
16781
17184
|
const p = prompt.toLowerCase();
|
|
@@ -16905,18 +17308,22 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
16905
17308
|
let tagged = 0;
|
|
16906
17309
|
let failed = 0;
|
|
16907
17310
|
for (const { id, action } of decisions) {
|
|
16908
|
-
const
|
|
16909
|
-
|
|
16910
|
-
)
|
|
16911
|
-
|
|
16912
|
-
|
|
16913
|
-
|
|
16914
|
-
|
|
16915
|
-
|
|
16916
|
-
tagged
|
|
16917
|
-
|
|
16918
|
-
|
|
16919
|
-
|
|
17311
|
+
const group = opts.characters?.get(id);
|
|
17312
|
+
const targets = group ?? [{ id, tags: existing.get(id) ?? [] }];
|
|
17313
|
+
for (const target of targets) {
|
|
17314
|
+
const current = target.tags.filter(
|
|
17315
|
+
(t) => t !== "pixelkiln:keep" && t !== "pixelkiln:discard" && t !== "pixelkiln:imported"
|
|
17316
|
+
);
|
|
17317
|
+
const decisionTag = `pixelkiln:${action === "import" ? "imported" : action}`;
|
|
17318
|
+
const next = [...current.slice(0, 19), decisionTag];
|
|
17319
|
+
if (!provider.setTags) return { tagged, failed };
|
|
17320
|
+
try {
|
|
17321
|
+
await provider.setTags(target.id, next, group ? "character" : void 0);
|
|
17322
|
+
tagged++;
|
|
17323
|
+
} catch (err) {
|
|
17324
|
+
failed++;
|
|
17325
|
+
log(` tag failed ${target.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
17326
|
+
}
|
|
16920
17327
|
}
|
|
16921
17328
|
}
|
|
16922
17329
|
return { tagged, failed };
|
|
@@ -16929,7 +17336,7 @@ var import_node_path34 = __toESM(require("path"), 1);
|
|
|
16929
17336
|
// src/pick/salvage-sheet.ts
|
|
16930
17337
|
var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
16931
17338
|
function renderSalvageSheet(orphans, ctx) {
|
|
16932
|
-
const safe = orphans.map((o) => ({ ...o, prompt: escapeHtml3(o.prompt) }));
|
|
17339
|
+
const safe = orphans.map((o) => ({ ...o, prompt: escapeHtml3(o.prompt), ...o.note ? { note: escapeHtml3(o.note) } : {} }));
|
|
16933
17340
|
const data = JSON.stringify(safe).replace(/<\//g, "<\\/");
|
|
16934
17341
|
const styleId = ctx ? escapeHtml3(ctx.styleId) : null;
|
|
16935
17342
|
const importDir = ctx ? escapeHtml3(ctx.importDir) : null;
|
|
@@ -17015,6 +17422,7 @@ ITEMS.forEach((o, i) => {
|
|
|
17015
17422
|
el.innerHTML =
|
|
17016
17423
|
'<div class="thumb"><img src="' + o.previewUrl + '" loading="lazy"></div>' +
|
|
17017
17424
|
'<div class="meta">' + o.width + '\xD7' + o.height + ' \xB7 ' + o.createdAt.slice(0,10) + '</div>' +
|
|
17425
|
+
(o.note ? '<div class="meta">' + o.note + '</div>' : '') +
|
|
17018
17426
|
'<div class="prompt">' + o.prompt + '</div>' +
|
|
17019
17427
|
'<div class="acts">' +
|
|
17020
17428
|
'<button data-a="import">import</button>' +
|
|
@@ -17093,6 +17501,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
17093
17501
|
});
|
|
17094
17502
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
17095
17503
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
17504
|
+
const characterMembers = new Map(
|
|
17505
|
+
orphans.flatMap((o) => o.kind === "character" && o.members ? [[o.id, o.members]] : [])
|
|
17506
|
+
);
|
|
17096
17507
|
return serveReviewPage({
|
|
17097
17508
|
html,
|
|
17098
17509
|
port: opts.port,
|
|
@@ -17109,10 +17520,34 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
17109
17520
|
const result = { imported: 0, kept: 0, discarded: 0, failed: 0 };
|
|
17110
17521
|
const taken = new Set(Object.keys(ctx.manifest.assets));
|
|
17111
17522
|
const importedAssetIds = [];
|
|
17523
|
+
const characterAssetIds = [];
|
|
17112
17524
|
for (const decision of decisions) {
|
|
17113
17525
|
const orphan = byId.get(decision.id);
|
|
17114
17526
|
if (!orphan) continue;
|
|
17115
|
-
if (decision.action === "import") {
|
|
17527
|
+
if (decision.action === "import" && orphan.kind === "character") {
|
|
17528
|
+
try {
|
|
17529
|
+
if (!provider.getCharacter) throw new Error(`provider ${provider.id} cannot read characters`);
|
|
17530
|
+
const details = [];
|
|
17531
|
+
for (const member of orphan.members ?? [{ id: orphan.id }]) details.push(await provider.getCharacter(member.id));
|
|
17532
|
+
const style = ctx.manifest.styles[ctx.styleId];
|
|
17533
|
+
const { assets, skipped } = characterSalvageAssets(details, {
|
|
17534
|
+
styleId: ctx.styleId,
|
|
17535
|
+
styleSize: style?.size,
|
|
17536
|
+
taken
|
|
17537
|
+
});
|
|
17538
|
+
for (const [assetId, asset] of Object.entries(assets)) {
|
|
17539
|
+
ctx.manifest.assets[assetId] = asset;
|
|
17540
|
+
importedAssetIds.push(assetId);
|
|
17541
|
+
characterAssetIds.push(assetId);
|
|
17542
|
+
}
|
|
17543
|
+
for (const line of skipped) log(` not imported: ${line}`);
|
|
17544
|
+
result.imported++;
|
|
17545
|
+
log(` imported character ${orphan.id} as ${Object.keys(assets).length} asset(s)`);
|
|
17546
|
+
} catch (err) {
|
|
17547
|
+
result.failed++;
|
|
17548
|
+
log(` import failed ${orphan.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
17549
|
+
}
|
|
17550
|
+
} else if (decision.action === "import") {
|
|
17116
17551
|
try {
|
|
17117
17552
|
const buf = await provider.download(orphan.previewUrl);
|
|
17118
17553
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
@@ -17176,11 +17611,12 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
17176
17611
|
result.discarded++;
|
|
17177
17612
|
}
|
|
17178
17613
|
}
|
|
17179
|
-
await applyTags(provider, decisions, existingTags, { onProgress: log });
|
|
17614
|
+
await applyTags(provider, decisions, existingTags, { onProgress: log, characters: characterMembers });
|
|
17180
17615
|
const raw = JSON.parse(await (0, import_promises27.readFile)(ctx.manifestPath, "utf8"));
|
|
17181
17616
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
17182
17617
|
await (0, import_promises27.writeFile)(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
17183
17618
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
17619
|
+
if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
|
|
17184
17620
|
return result;
|
|
17185
17621
|
}
|
|
17186
17622
|
});
|
|
@@ -17297,6 +17733,9 @@ async function workspaceStatus(ws, dir) {
|
|
|
17297
17733
|
CharacterAnimationSchema,
|
|
17298
17734
|
CharacterDirectionSchema,
|
|
17299
17735
|
CharacterModeSchema,
|
|
17736
|
+
CharacterOutfitSchema,
|
|
17737
|
+
CharacterPortraitSchema,
|
|
17738
|
+
CharacterPortraitSizeSchema,
|
|
17300
17739
|
CharacterProportionsSchema,
|
|
17301
17740
|
CharacterStateSchema,
|
|
17302
17741
|
ComfyUIProvider,
|