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.js
CHANGED
|
@@ -186,6 +186,27 @@ var CharacterStateSchema = z.object({
|
|
|
186
186
|
height: z.number().int().min(16).max(256)
|
|
187
187
|
}).strict().optional()
|
|
188
188
|
}).strict();
|
|
189
|
+
var CharacterPortraitSizeSchema = z.union([
|
|
190
|
+
z.literal(16),
|
|
191
|
+
z.literal(32),
|
|
192
|
+
z.literal(48),
|
|
193
|
+
z.literal(64),
|
|
194
|
+
z.literal(128),
|
|
195
|
+
z.literal(160)
|
|
196
|
+
]);
|
|
197
|
+
var CharacterPortraitSchema = z.object({
|
|
198
|
+
/** The base or state to portray, in the same style. */
|
|
199
|
+
of: z.string().min(1),
|
|
200
|
+
size: CharacterPortraitSizeSchema.default(64)
|
|
201
|
+
}).strict();
|
|
202
|
+
var CharacterOutfitSchema = z.object({
|
|
203
|
+
/** The loop (a character animation asset) to re-clothe, in the same style. */
|
|
204
|
+
of: z.string().min(1),
|
|
205
|
+
/** Manifest-relative image of the outfit to transfer. */
|
|
206
|
+
reference: z.string().min(1),
|
|
207
|
+
/** Extra guidance PixelLab passes through as written, e.g. view/direction hints. */
|
|
208
|
+
additionalInstructions: z.string().max(2e3).optional()
|
|
209
|
+
}).strict();
|
|
189
210
|
var CharacterAnimationSchema = z.object({
|
|
190
211
|
/** The character to animate: a base or a state in the same style. */
|
|
191
212
|
of: z.string().min(1),
|
|
@@ -807,6 +828,10 @@ var AssetSchema = z.object({
|
|
|
807
828
|
pieces: z.array(UiPieceSchema).min(1).optional(),
|
|
808
829
|
/** `uiAsset` styles: named, auto-positioned UI element scaffolds. Combine with `pieces`; omit both for a default full-canvas panel. */
|
|
809
830
|
elements: z.array(UiElementSchema).min(1).optional(),
|
|
831
|
+
/** `character` styles: this asset is a bust portrait of another character asset's south sprite. */
|
|
832
|
+
portrait: CharacterPortraitSchema.optional(),
|
|
833
|
+
/** `character` styles: this asset re-clothes an existing loop's frames with a reference outfit. */
|
|
834
|
+
outfit: CharacterOutfitSchema.optional(),
|
|
810
835
|
/** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
|
|
811
836
|
proportions: CharacterProportionsSchema.optional(),
|
|
812
837
|
/**
|
|
@@ -883,7 +908,7 @@ var AssetSchema = z.object({
|
|
|
883
908
|
path: ["revision"]
|
|
884
909
|
});
|
|
885
910
|
}
|
|
886
|
-
const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror", asset.batch && "batch"].filter(Boolean);
|
|
911
|
+
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);
|
|
887
912
|
if (shapes.length > 1) {
|
|
888
913
|
context.addIssue({
|
|
889
914
|
code: z.ZodIssueCode.custom,
|
|
@@ -898,24 +923,24 @@ var AssetSchema = z.object({
|
|
|
898
923
|
path: ["mirror"]
|
|
899
924
|
});
|
|
900
925
|
}
|
|
901
|
-
if (asset.prompt === void 0 && !asset.mirror) {
|
|
926
|
+
if (asset.prompt === void 0 && !asset.mirror && !asset.portrait && !asset.outfit) {
|
|
902
927
|
context.addIssue({
|
|
903
928
|
code: z.ZodIssueCode.custom,
|
|
904
929
|
message: "Required",
|
|
905
930
|
path: ["prompt"]
|
|
906
931
|
});
|
|
907
932
|
}
|
|
908
|
-
if (asset.reference && (asset.state || asset.animation || asset.mirror)) {
|
|
933
|
+
if (asset.reference && (asset.state || asset.animation || asset.mirror || asset.portrait || asset.outfit)) {
|
|
909
934
|
context.addIssue({
|
|
910
935
|
code: z.ZodIssueCode.custom,
|
|
911
|
-
message: "a reference sprite belongs on a base; a state, animation, or
|
|
936
|
+
message: "a reference sprite belongs on a base; a state, animation, mirror, portrait, or outfit takes its look from its parent",
|
|
912
937
|
path: ["reference"]
|
|
913
938
|
});
|
|
914
939
|
}
|
|
915
|
-
if (asset.concept && (asset.state || asset.animation || asset.mirror)) {
|
|
940
|
+
if (asset.concept && (asset.state || asset.animation || asset.mirror || asset.portrait || asset.outfit)) {
|
|
916
941
|
context.addIssue({
|
|
917
942
|
code: z.ZodIssueCode.custom,
|
|
918
|
-
message: "a concept image belongs on a base; a state, animation, or
|
|
943
|
+
message: "a concept image belongs on a base; a state, animation, mirror, portrait, or outfit takes its look from its parent",
|
|
919
944
|
path: ["concept"]
|
|
920
945
|
});
|
|
921
946
|
}
|
|
@@ -1040,6 +1065,17 @@ var LockEntrySchema = z.object({
|
|
|
1040
1065
|
leaderAssetId: z.string().min(1).optional(),
|
|
1041
1066
|
index: z.number().int().min(1).optional()
|
|
1042
1067
|
}).strict().nullable().default(null),
|
|
1068
|
+
/**
|
|
1069
|
+
* For an outfit: the loop it re-clothed, a hash over that loop's output
|
|
1070
|
+
* hashes when this was made, and the reference image's own hash. Tracked
|
|
1071
|
+
* the way `mirror` is, not through `specHash`: manifest resolution has no
|
|
1072
|
+
* lock to hash the parent's actual current frame bytes against.
|
|
1073
|
+
*/
|
|
1074
|
+
outfit: z.object({
|
|
1075
|
+
sourceAssetId: z.string().min(1),
|
|
1076
|
+
sourceSha256: z.string().regex(/^[0-9a-f]{64}$/),
|
|
1077
|
+
referenceSha256: z.string().regex(/^[0-9a-f]{64}$/)
|
|
1078
|
+
}).strict().nullable().default(null),
|
|
1043
1079
|
/**
|
|
1044
1080
|
* Output hashes owned by the previous generation while its replacement is
|
|
1045
1081
|
* pending. They authorize replacing only unchanged PixelKiln-owned files.
|
|
@@ -1443,6 +1479,28 @@ var AnimateObjectSubmitSchema = z2.object({
|
|
|
1443
1479
|
submissions: z2.array(DirectionSubmissionSchema),
|
|
1444
1480
|
usage: UsageSchema
|
|
1445
1481
|
}).passthrough();
|
|
1482
|
+
var PortraitSubmitSchema = z2.object({
|
|
1483
|
+
background_job_id: z2.string().min(1),
|
|
1484
|
+
status: z2.string().default("processing"),
|
|
1485
|
+
usage: UsageSchema
|
|
1486
|
+
}).passthrough();
|
|
1487
|
+
var PortraitJobSchema = z2.object({
|
|
1488
|
+
status: z2.string(),
|
|
1489
|
+
download_url: z2.string().nullable().optional(),
|
|
1490
|
+
width: z2.number().int().nullable().optional(),
|
|
1491
|
+
height: z2.number().int().nullable().optional()
|
|
1492
|
+
}).passthrough();
|
|
1493
|
+
var SetPortraitSchema = z2.object({
|
|
1494
|
+
character_id: z2.string().min(1),
|
|
1495
|
+
size: z2.number().int().positive(),
|
|
1496
|
+
url: z2.string().min(1),
|
|
1497
|
+
usage: UsageSchema
|
|
1498
|
+
}).passthrough();
|
|
1499
|
+
var TransferOutfitSubmitSchema = z2.object({
|
|
1500
|
+
background_job_id: z2.string().min(1),
|
|
1501
|
+
status: z2.string().default("processing"),
|
|
1502
|
+
usage: UsageSchema
|
|
1503
|
+
}).passthrough();
|
|
1446
1504
|
var CharacterAnimationDirectionSchema = z2.object({
|
|
1447
1505
|
direction: z2.string(),
|
|
1448
1506
|
frame_count: z2.number().int().min(0),
|
|
@@ -2058,6 +2116,69 @@ var PixelLabClient = class {
|
|
|
2058
2116
|
});
|
|
2059
2117
|
return validateResponse(AnimateSubmitSchema, raw, "animate-character");
|
|
2060
2118
|
}
|
|
2119
|
+
/**
|
|
2120
|
+
* A full-body sprite in, a bust portrait out (or the reverse, unused
|
|
2121
|
+
* today). Job-based like a state or `1dir`, not the couple-generations
|
|
2122
|
+
* "conversion" the name suggests: the smallest `resultSize` (16px) billed
|
|
2123
|
+
* 20 generations live (docs/ENDPOINTS.md, "Characters, measured").
|
|
2124
|
+
*/
|
|
2125
|
+
async createPortraitCharacterPro(args) {
|
|
2126
|
+
const raw = await this.request("/portrait-character-pro", {
|
|
2127
|
+
method: "POST",
|
|
2128
|
+
body: JSON.stringify({
|
|
2129
|
+
direction: args.direction,
|
|
2130
|
+
image: { type: "base64", base64: args.image.base64, format: args.image.format },
|
|
2131
|
+
...args.view ? { view: args.view } : {},
|
|
2132
|
+
...args.resultSize != null ? { result_size: args.resultSize } : {},
|
|
2133
|
+
...args.seed != null ? { seed: args.seed } : {}
|
|
2134
|
+
})
|
|
2135
|
+
});
|
|
2136
|
+
return validateResponse(PortraitSubmitSchema, raw, "portrait-character-pro");
|
|
2137
|
+
}
|
|
2138
|
+
/** Throws PixelLabError(423) while the job is still drawing, like tiles and map objects. */
|
|
2139
|
+
async getPortraitCharacterJob(jobId) {
|
|
2140
|
+
const raw = await this.request(`/portrait-character-pro/${encodeURIComponent(jobId)}`);
|
|
2141
|
+
return validateResponse(PortraitJobSchema, raw, "portrait-character-pro/{id}");
|
|
2142
|
+
}
|
|
2143
|
+
/**
|
|
2144
|
+
* Attaches a portrait image to a character record. Synchronous and free
|
|
2145
|
+
* (0 generations, live); PixelLab does not link this to
|
|
2146
|
+
* `createPortraitCharacterPro` automatically, and `GET /characters/{id}`
|
|
2147
|
+
* never grows a portrait field, so this response's `url` is the only place
|
|
2148
|
+
* it appears.
|
|
2149
|
+
*/
|
|
2150
|
+
async setCharacterPortrait(characterId, image) {
|
|
2151
|
+
const raw = await this.request(`/characters/${encodeURIComponent(characterId)}/portrait`, {
|
|
2152
|
+
method: "POST",
|
|
2153
|
+
body: JSON.stringify({ image: { type: "base64", base64: image.base64, format: image.format } })
|
|
2154
|
+
});
|
|
2155
|
+
return validateResponse(SetPortraitSchema, raw, "characters/{id}/portrait");
|
|
2156
|
+
}
|
|
2157
|
+
/**
|
|
2158
|
+
* Applies a reference outfit to 2–16 existing frames. Job-based like
|
|
2159
|
+
* everything else here, but the completed job returns the result frames
|
|
2160
|
+
* inline as base64 (`last_response.quantized_images`), not as storage
|
|
2161
|
+
* URLs — the one PixelLab character tool that works this way. A live
|
|
2162
|
+
* 2-frame, 92x92 job billed 20 generations (docs/ENDPOINTS.md).
|
|
2163
|
+
*/
|
|
2164
|
+
async transferOutfitV2(args) {
|
|
2165
|
+
const image = (img) => ({
|
|
2166
|
+
image: { type: "base64", base64: img.base64, format: img.format },
|
|
2167
|
+
size: { width: img.width, height: img.height }
|
|
2168
|
+
});
|
|
2169
|
+
const raw = await this.request("/transfer-outfit-v2", {
|
|
2170
|
+
method: "POST",
|
|
2171
|
+
body: JSON.stringify({
|
|
2172
|
+
reference_image: image(args.referenceImage),
|
|
2173
|
+
frames: args.frames.map(image),
|
|
2174
|
+
image_size: args.imageSize,
|
|
2175
|
+
...args.seed != null ? { seed: args.seed } : {},
|
|
2176
|
+
...args.noBackground !== void 0 ? { no_background: args.noBackground } : {},
|
|
2177
|
+
...args.additionalInstructions ? { additional_instructions: args.additionalInstructions } : {}
|
|
2178
|
+
})
|
|
2179
|
+
});
|
|
2180
|
+
return validateResponse(TransferOutfitSubmitSchema, raw, "transfer-outfit-v2");
|
|
2181
|
+
}
|
|
2061
2182
|
async getCharacter(characterId) {
|
|
2062
2183
|
const raw = await this.request(`/characters/${encodeURIComponent(characterId)}`);
|
|
2063
2184
|
return validateResponse(CharacterSchema, raw, "characters/{id}");
|
|
@@ -3074,7 +3195,16 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
3074
3195
|
styleTraits: spec.character.styleTraits,
|
|
3075
3196
|
reference: spec.character.reference ? Object.fromEntries(Object.entries(spec.character.reference).map(([direction, image]) => [direction, image.sha256])) : void 0,
|
|
3076
3197
|
concept: spec.character.concept?.sha256,
|
|
3077
|
-
styleAnchor: spec.character.styleAnchor ? { of: spec.character.styleAnchor.assetId, sha256: spec.character.styleAnchor.sha256 } : void 0
|
|
3198
|
+
styleAnchor: spec.character.styleAnchor ? { of: spec.character.styleAnchor.assetId, sha256: spec.character.styleAnchor.sha256 } : void 0,
|
|
3199
|
+
// The parent's own bytes come from its recorded outputs; the plan
|
|
3200
|
+
// compares those directly (like a mirror), so only the choice of
|
|
3201
|
+
// parent, the reference image's bytes, and any extra guidance
|
|
3202
|
+
// are identity here.
|
|
3203
|
+
outfit: spec.character.outfit ? {
|
|
3204
|
+
of: spec.character.parentAssetId,
|
|
3205
|
+
reference: spec.character.outfit.reference.sha256,
|
|
3206
|
+
additionalInstructions: spec.character.outfit.additionalInstructions
|
|
3207
|
+
} : void 0
|
|
3078
3208
|
} : void 0,
|
|
3079
3209
|
// An objectPro family: same shape as character above, minus the
|
|
3080
3210
|
// fields it has none of (mode/template/proportions/isometric/concept/
|
|
@@ -3155,7 +3285,10 @@ function characterCost(spec) {
|
|
|
3155
3285
|
if (animation.mode === "v3") return Math.max(1, Math.ceil(px * animation.frames / 65536));
|
|
3156
3286
|
return generationCost(spec.width, spec.height, "1dir");
|
|
3157
3287
|
}
|
|
3158
|
-
if (character.kind === "state") return generationCost(spec.width, spec.height, "1dir");
|
|
3288
|
+
if (character.kind === "state" || character.kind === "portrait") return generationCost(spec.width, spec.height, "1dir");
|
|
3289
|
+
if (character.kind === "outfit") {
|
|
3290
|
+
return 20;
|
|
3291
|
+
}
|
|
3159
3292
|
if (character.mode === "standard") return 1;
|
|
3160
3293
|
if (character.mode === "v3") return 1 + Math.max(1, Math.ceil(px * 8 / 65536));
|
|
3161
3294
|
if (character.mode === "pro-flash") {
|
|
@@ -3272,15 +3405,18 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3272
3405
|
return { spacingMs: 2500, maxInFlight: 8 };
|
|
3273
3406
|
}
|
|
3274
3407
|
/**
|
|
3275
|
-
* Where synchronous pixflux results
|
|
3408
|
+
* Where synchronous pixflux results (or an outfit transfer's inline
|
|
3409
|
+
* frames) are parked between `submit`/poll and `fetch`.
|
|
3276
3410
|
*
|
|
3277
|
-
* pixflux returns the PNG inline rather than a job id,
|
|
3278
|
-
*
|
|
3279
|
-
*
|
|
3280
|
-
*
|
|
3411
|
+
* pixflux returns the PNG inline rather than a job id, and an outfit
|
|
3412
|
+
* transfer's completed job returns its frames inline too, but the
|
|
3413
|
+
* pipeline is built around submit → poll → fetch running as separate
|
|
3414
|
+
* commands. Writing the bytes to a known path keeps that model intact:
|
|
3415
|
+
* the id is the filename, polling is an existence check, and downloading
|
|
3416
|
+
* is a file read.
|
|
3281
3417
|
*/
|
|
3282
|
-
static cacheDir() {
|
|
3283
|
-
const dir = path.join(os.tmpdir(),
|
|
3418
|
+
static cacheDir(namespace = "pixflux") {
|
|
3419
|
+
const dir = path.join(os.tmpdir(), `pixelkiln-${namespace}`);
|
|
3284
3420
|
mkdirSync(dir, { recursive: true });
|
|
3285
3421
|
return dir;
|
|
3286
3422
|
}
|
|
@@ -3540,6 +3676,12 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3540
3676
|
const character = spec.character;
|
|
3541
3677
|
if (!character) throw new Error(`${spec.styleId}/${spec.assetId} has no character shape`);
|
|
3542
3678
|
const label = `${spec.styleId}/${spec.assetId}`;
|
|
3679
|
+
if (character.kind === "outfit") {
|
|
3680
|
+
const reference = character.outfit.reference;
|
|
3681
|
+
if (reference.width < 32 || reference.width > 256 || reference.height < 32 || reference.height > 256) {
|
|
3682
|
+
throw new Error(`${label}: outfit reference is ${reference.width}x${reference.height}; PixelLab transfer-outfit-v2 takes 32 to 256px per side`);
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3543
3685
|
if (character.kind === "base" && character.proportions !== void 0) {
|
|
3544
3686
|
if (character.mode !== "standard") {
|
|
3545
3687
|
throw new Error(`${label}: proportions apply to standard bases; the ${character.mode} engine has none to set`);
|
|
@@ -3551,7 +3693,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3551
3693
|
if (character.kind === "base" && character.enhancePrompt !== void 0 && character.mode !== "v3") {
|
|
3552
3694
|
throw new Error(`${label}: enhancePrompt applies to v3 bases; the ${character.mode} engine does not take it`);
|
|
3553
3695
|
}
|
|
3554
|
-
if (character.animation) {
|
|
3696
|
+
if (character.animation && character.kind !== "outfit") {
|
|
3555
3697
|
const { startFrame, endFrame } = character.animation;
|
|
3556
3698
|
for (const [name, image] of [["start frame", startFrame], ["end frame", endFrame]]) {
|
|
3557
3699
|
if (image && (image.width > 256 || image.height > 256)) {
|
|
@@ -3632,9 +3774,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3632
3774
|
if (character.styleTraits && character.mode !== "pro-flash") {
|
|
3633
3775
|
throw new Error(`${label}: styleTraits choose what a pro-flash style image lends; the ${character.mode} engine does not take them`);
|
|
3634
3776
|
}
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3777
|
+
if (character.kind !== "portrait") {
|
|
3778
|
+
const maxSize = character.mode === "v3" || character.mode === "pro-flash" ? 256 : 128;
|
|
3779
|
+
if (spec.width < 16 || spec.height < 16 || spec.width > maxSize || spec.height > maxSize) {
|
|
3780
|
+
throw new Error(`PixelLab ${character.mode} characters must be between 16 and ${maxSize} pixels`);
|
|
3781
|
+
}
|
|
3638
3782
|
}
|
|
3639
3783
|
if (character.mode === "pro-flash" && character.kind === "base" && (spec.width % 4 || spec.height % 4)) {
|
|
3640
3784
|
throw new Error(`PixelLab pro-flash characters are sized in multiples of 4 pixels; ${spec.width}x${spec.height} is not`);
|
|
@@ -3643,9 +3787,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3643
3787
|
if (!templates.includes(character.template)) {
|
|
3644
3788
|
throw new Error(`PixelLab ${character.mode} character template must be one of: ${templates.join(", ")}`);
|
|
3645
3789
|
}
|
|
3646
|
-
const views = character.mode === "standard" ? ["low top-down", "high top-down", "side", "perspective", "oblique"] : ["low top-down", "high top-down", "side"];
|
|
3790
|
+
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"];
|
|
3647
3791
|
if (!views.includes(spec.view)) {
|
|
3648
|
-
throw new Error(
|
|
3792
|
+
throw new Error(
|
|
3793
|
+
character.kind === "portrait" ? `PixelLab portrait view must be one of: ${views.join(", ")}` : `PixelLab ${character.mode} character view must be one of: ${views.join(", ")}`
|
|
3794
|
+
);
|
|
3649
3795
|
}
|
|
3650
3796
|
if (spec.view === "oblique" && character.directions !== 4) {
|
|
3651
3797
|
throw new Error("PixelLab oblique characters are 4-direction only");
|
|
@@ -3653,7 +3799,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3653
3799
|
if (character.kind !== "base" && !character.parentAssetId) {
|
|
3654
3800
|
throw new Error(`${spec.styleId}/${spec.assetId} is a ${character.kind} with no parent`);
|
|
3655
3801
|
}
|
|
3656
|
-
if (character.animation) {
|
|
3802
|
+
if (character.animation && character.kind !== "outfit") {
|
|
3657
3803
|
const allowed = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
|
|
3658
3804
|
if (!allowed.includes(character.animation.direction)) {
|
|
3659
3805
|
throw new Error(
|
|
@@ -3759,6 +3905,43 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3759
3905
|
});
|
|
3760
3906
|
return { jobId: res2.character_id, metadata: { character: { kind: "base", characterId: res2.character_id, mode: character.mode, directions: character.directions, backgroundJobId: res2.background_job_id } } };
|
|
3761
3907
|
}
|
|
3908
|
+
if (character.kind === "outfit") {
|
|
3909
|
+
const parentOutputs = context?.parentOutputs ?? [];
|
|
3910
|
+
if (parentOutputs.length < 2) {
|
|
3911
|
+
throw new Error(
|
|
3912
|
+
`${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`
|
|
3913
|
+
);
|
|
3914
|
+
}
|
|
3915
|
+
const frames = parentOutputs.slice(0, 16).map((output) => {
|
|
3916
|
+
const bytes = readFileSync(output.path);
|
|
3917
|
+
const dims = imageMetadata(bytes);
|
|
3918
|
+
if (!dims) throw new Error(`${spec.styleId}/${spec.assetId}: outfit source frame is not a readable PNG: ${output.path}`);
|
|
3919
|
+
return { base64: bytes.toString("base64"), format: "png", width: dims.width, height: dims.height };
|
|
3920
|
+
});
|
|
3921
|
+
const outfit = character.outfit;
|
|
3922
|
+
const referenceBytes = readFileSync(outfit.reference.path);
|
|
3923
|
+
if (sha256(referenceBytes) !== outfit.reference.sha256) {
|
|
3924
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: outfit reference changed after the manifest was resolved: ${outfit.reference.path}`);
|
|
3925
|
+
}
|
|
3926
|
+
const res2 = await this.client.transferOutfitV2({
|
|
3927
|
+
referenceImage: { base64: referenceBytes.toString("base64"), format: "png", width: outfit.reference.width, height: outfit.reference.height },
|
|
3928
|
+
frames,
|
|
3929
|
+
imageSize: { width: spec.width, height: spec.height },
|
|
3930
|
+
seed: spec.seed,
|
|
3931
|
+
noBackground: spec.noBackground,
|
|
3932
|
+
additionalInstructions: outfit.additionalInstructions
|
|
3933
|
+
});
|
|
3934
|
+
return {
|
|
3935
|
+
jobId: res2.background_job_id,
|
|
3936
|
+
metadata: {
|
|
3937
|
+
character: {
|
|
3938
|
+
kind: "outfit",
|
|
3939
|
+
backgroundJobId: res2.background_job_id,
|
|
3940
|
+
frameRoles: parentOutputs.slice(0, 16).map((output) => output.role ?? null)
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
};
|
|
3944
|
+
}
|
|
3762
3945
|
const parentId = context?.parentObjectId;
|
|
3763
3946
|
if (!parentId) {
|
|
3764
3947
|
throw new Error(
|
|
@@ -3777,6 +3960,24 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3777
3960
|
});
|
|
3778
3961
|
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 } } };
|
|
3779
3962
|
}
|
|
3963
|
+
if (character.kind === "portrait") {
|
|
3964
|
+
const parentFile = character.parentFile;
|
|
3965
|
+
const bytes = readFileSync(parentFile);
|
|
3966
|
+
if (character.parentSha256 && sha256(bytes) !== character.parentSha256) {
|
|
3967
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: portrait source changed after the manifest was resolved: ${parentFile}`);
|
|
3968
|
+
}
|
|
3969
|
+
const res2 = await this.client.createPortraitCharacterPro({
|
|
3970
|
+
direction: "character_to_portrait",
|
|
3971
|
+
image: { base64: bytes.toString("base64"), format: "png" },
|
|
3972
|
+
view: spec.view,
|
|
3973
|
+
resultSize: character.portrait.size,
|
|
3974
|
+
seed: spec.seed
|
|
3975
|
+
});
|
|
3976
|
+
return {
|
|
3977
|
+
jobId: res2.background_job_id,
|
|
3978
|
+
metadata: { character: { kind: "portrait", parentCharacterId: parentId, backgroundJobId: res2.background_job_id } }
|
|
3979
|
+
};
|
|
3980
|
+
}
|
|
3780
3981
|
const animation = character.animation;
|
|
3781
3982
|
const name = characterAnimationName(spec);
|
|
3782
3983
|
const replaced = context?.replacedMetadata?.character ?? {};
|
|
@@ -3890,6 +4091,9 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3890
4091
|
async pollCharacter(jobId, context) {
|
|
3891
4092
|
const animation = decodeAnimationJob(jobId);
|
|
3892
4093
|
if (animation) return this.pollCharacterAnimation(animation, context);
|
|
4094
|
+
const kind = context?.metadata?.character?.kind ?? context?.spec?.character?.kind;
|
|
4095
|
+
if (kind === "portrait") return this.pollCharacterPortrait(jobId, context);
|
|
4096
|
+
if (kind === "outfit") return this.pollCharacterOutfit(jobId, context);
|
|
3893
4097
|
const character = await this.client.getCharacter(jobId);
|
|
3894
4098
|
if (character.status === "failed") return { status: "failed", error: "character generation failed upstream" };
|
|
3895
4099
|
if (character.status !== "completed" || !character.rotation_urls) return { status: "processing" };
|
|
@@ -3915,6 +4119,66 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3915
4119
|
billed: await this.billedForJob(backgroundJobId)
|
|
3916
4120
|
};
|
|
3917
4121
|
}
|
|
4122
|
+
/**
|
|
4123
|
+
* `jobId` here is `portrait-character-pro`'s own background job id, not a
|
|
4124
|
+
* character id: the endpoint converts an arbitrary image and never touches
|
|
4125
|
+
* the character record. Once the portrait itself is drawn, this also
|
|
4126
|
+
* attaches it to the parent's character record with `SetPortrait` — a
|
|
4127
|
+
* separate, free call PixelLab does not make automatically (docs/PIXELLAB.md).
|
|
4128
|
+
*/
|
|
4129
|
+
async pollCharacterPortrait(jobId, context) {
|
|
4130
|
+
const meta = context?.metadata?.character;
|
|
4131
|
+
try {
|
|
4132
|
+
const job = await this.client.getPortraitCharacterJob(jobId);
|
|
4133
|
+
if (job.status === "failed") return { status: "failed", error: "portrait generation failed upstream" };
|
|
4134
|
+
if (job.status !== "completed" || !job.download_url) return { status: "processing" };
|
|
4135
|
+
if (meta?.parentCharacterId) {
|
|
4136
|
+
const bytes = await this.client.download(job.download_url);
|
|
4137
|
+
await this.client.setCharacterPortrait(meta.parentCharacterId, { base64: bytes.toString("base64"), format: "png" });
|
|
4138
|
+
}
|
|
4139
|
+
return {
|
|
4140
|
+
status: "ready",
|
|
4141
|
+
objectId: jobId,
|
|
4142
|
+
sourceUrl: job.download_url,
|
|
4143
|
+
sources: [{ url: job.download_url }],
|
|
4144
|
+
billed: await this.billedForJob(meta?.backgroundJobId)
|
|
4145
|
+
};
|
|
4146
|
+
} catch (err) {
|
|
4147
|
+
if (err instanceof PixelLabError && err.status === 423) return { status: "processing" };
|
|
4148
|
+
throw err;
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
/**
|
|
4152
|
+
* No dedicated status endpoint exists for `transfer-outfit-v2`, unlike
|
|
4153
|
+
* portraits and tiles: the background job is the only place to poll. A
|
|
4154
|
+
* completed job returns its frames inline as base64
|
|
4155
|
+
* (`last_response.quantized_images`), not as storage URLs, so they are
|
|
4156
|
+
* cached locally the way a synchronous pixflux result is, and matched back
|
|
4157
|
+
* to the source frames' own roles recorded at submit time.
|
|
4158
|
+
*/
|
|
4159
|
+
async pollCharacterOutfit(jobId, context) {
|
|
4160
|
+
const meta = context?.metadata?.character;
|
|
4161
|
+
const job = await this.client.getBackgroundJob(jobId);
|
|
4162
|
+
if (job.status === "failed") return { status: "failed", error: "outfit transfer failed upstream" };
|
|
4163
|
+
if (job.status !== "completed") return { status: "processing" };
|
|
4164
|
+
const images = job.last_response?.quantized_images;
|
|
4165
|
+
if (!images?.length) return { status: "failed", error: "outfit transfer completed with no result frames" };
|
|
4166
|
+
const dir = _PixelLabProvider.cacheDir("outfit");
|
|
4167
|
+
const sources = images.map((image, index) => {
|
|
4168
|
+
if (!image.base64) throw new Error(`outfit transfer frame ${index} has no image data`);
|
|
4169
|
+
const role = meta?.frameRoles?.[index] ?? void 0;
|
|
4170
|
+
const file = path.join(dir, `${jobId}-${index}.png`);
|
|
4171
|
+
writeFileSync(file, Buffer.from(image.base64, "base64"));
|
|
4172
|
+
return { url: `file://${file}`, ...role ? { role } : {} };
|
|
4173
|
+
});
|
|
4174
|
+
return {
|
|
4175
|
+
status: "ready",
|
|
4176
|
+
objectId: jobId,
|
|
4177
|
+
sourceUrl: sources[0].url,
|
|
4178
|
+
sources,
|
|
4179
|
+
billed: await this.billedForJob(meta?.backgroundJobId)
|
|
4180
|
+
};
|
|
4181
|
+
}
|
|
3918
4182
|
/**
|
|
3919
4183
|
* The background job is the authoritative record of one direction: it
|
|
3920
4184
|
* completes with the frame URLs, the animation id, and the direction, and
|
|
@@ -4734,8 +4998,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4734
4998
|
const sources = [];
|
|
4735
4999
|
const terrainTiles = [];
|
|
4736
5000
|
for (const [index, tile] of tileset.tiles.entries()) {
|
|
4737
|
-
const
|
|
4738
|
-
const role = `tile-${String(index).padStart(2, "0")}-${
|
|
5001
|
+
const slug2 = tile.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "tile";
|
|
5002
|
+
const role = `tile-${String(index).padStart(2, "0")}-${slug2}`;
|
|
4739
5003
|
const file = path.join(_PixelLabProvider.cacheDir(), `${tilesetId}-${index}.png`);
|
|
4740
5004
|
writeFileSync(file, Buffer.from(tile.image.base64, "base64"));
|
|
4741
5005
|
sources.push({ url: `file://${file}`, role });
|
|
@@ -4854,6 +5118,9 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4854
5118
|
async delete(assetId) {
|
|
4855
5119
|
await this.client.deleteObject(assetId);
|
|
4856
5120
|
}
|
|
5121
|
+
async deleteCharacter(id) {
|
|
5122
|
+
await this.client.deleteCharacter(id);
|
|
5123
|
+
}
|
|
4857
5124
|
async *listCharacters() {
|
|
4858
5125
|
for await (const character of this.client.iterateCharacters(100)) {
|
|
4859
5126
|
yield remoteCharacter(character);
|
|
@@ -6877,6 +7144,8 @@ var FakeProvider = class {
|
|
|
6877
7144
|
this.tags.set(objectId, [...tags]);
|
|
6878
7145
|
const asset = this.assets.get(objectId);
|
|
6879
7146
|
if (asset) asset.tags = [...tags];
|
|
7147
|
+
const character = this.characters.get(objectId.split("#")[0]);
|
|
7148
|
+
if (character) character.tags = [...tags];
|
|
6880
7149
|
}
|
|
6881
7150
|
/**
|
|
6882
7151
|
* Assigned in the constructor rather than declared as a method, so a test can
|
|
@@ -6911,6 +7180,10 @@ var FakeProvider = class {
|
|
|
6911
7180
|
this.deleted.push(assetId);
|
|
6912
7181
|
this.assets.delete(assetId);
|
|
6913
7182
|
}
|
|
7183
|
+
async deleteCharacter(id) {
|
|
7184
|
+
this.deleted.push(id);
|
|
7185
|
+
this.characters.delete(id);
|
|
7186
|
+
}
|
|
6914
7187
|
/** Seed a pre-existing remote asset, for adopt / salvage tests. */
|
|
6915
7188
|
seed(asset) {
|
|
6916
7189
|
const full = {
|
|
@@ -7504,7 +7777,7 @@ ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
|
7504
7777
|
return { manifest: parsed.data, root: path5.dirname(abs), path: abs, ...loopFamilies };
|
|
7505
7778
|
}
|
|
7506
7779
|
function parentAssetId(asset) {
|
|
7507
|
-
return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of;
|
|
7780
|
+
return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of ?? asset?.portrait?.of ?? asset?.outfit?.of;
|
|
7508
7781
|
}
|
|
7509
7782
|
function formatManifestIssues(issues) {
|
|
7510
7783
|
return issues.flatMap((issue) => {
|
|
@@ -7671,9 +7944,9 @@ async function resolveSpecs(loaded, filter) {
|
|
|
7671
7944
|
height = styleImageDimensions.length ? Math.max(...styleImageDimensions.map((img) => img.height)) : style.tileSize ?? 32;
|
|
7672
7945
|
size = Math.max(width, height);
|
|
7673
7946
|
} else if (generator === "character") {
|
|
7674
|
-
size = asset.state?.canvas ? Math.max(asset.state.canvas.width, asset.state.canvas.height) : asset.size ?? style.size ?? 64;
|
|
7675
|
-
width = asset.state?.canvas?.width ?? size;
|
|
7676
|
-
height = asset.state?.canvas?.height ?? size;
|
|
7947
|
+
size = asset.portrait ? asset.portrait.size : asset.state?.canvas ? Math.max(asset.state.canvas.width, asset.state.canvas.height) : asset.size ?? style.size ?? 64;
|
|
7948
|
+
width = asset.portrait?.size ?? asset.state?.canvas?.width ?? size;
|
|
7949
|
+
height = asset.portrait?.size ?? asset.state?.canvas?.height ?? size;
|
|
7677
7950
|
} else if (generator === "terrain") {
|
|
7678
7951
|
size = style.terrainTileSize ?? 16;
|
|
7679
7952
|
width = size;
|
|
@@ -7700,12 +7973,17 @@ async function resolveSpecs(loaded, filter) {
|
|
|
7700
7973
|
`assets.${assetId}: ${asset.state ? "state" : "animation"} needs a character or objectPro style; "${styleId}" generates ${generator}`
|
|
7701
7974
|
);
|
|
7702
7975
|
}
|
|
7976
|
+
if ((asset.portrait || asset.outfit) && generator !== "character") {
|
|
7977
|
+
throw new Error(
|
|
7978
|
+
`assets.${assetId}: ${asset.portrait ? "portrait" : "outfit"} needs a character style; "${styleId}" generates ${generator}`
|
|
7979
|
+
);
|
|
7980
|
+
}
|
|
7703
7981
|
if ((asset.pieces || asset.elements) && generator !== "uiAsset") {
|
|
7704
7982
|
throw new Error(
|
|
7705
7983
|
`assets.${assetId}: ${asset.pieces ? "pieces" : "elements"} needs a uiAsset style; "${styleId}" generates ${generator}`
|
|
7706
7984
|
);
|
|
7707
7985
|
}
|
|
7708
|
-
const characterKind = asset.animation ? "animation" : asset.state ? "state" : "base";
|
|
7986
|
+
const characterKind = asset.animation ? "animation" : asset.state ? "state" : asset.portrait ? "portrait" : "base";
|
|
7709
7987
|
const subject = asset.promptByStyle[styleId] ?? asset.prompt ?? "";
|
|
7710
7988
|
let terrainLowerDescription;
|
|
7711
7989
|
let terrainUpperDescription;
|
|
@@ -7797,7 +8075,9 @@ async function resolveSpecs(loaded, filter) {
|
|
|
7797
8075
|
// its parent's pixels, not a new character: giving it a base shape
|
|
7798
8076
|
// would have packing, the gallery, and adoption treat it as one.
|
|
7799
8077
|
...generator === "character" && !asset.revision ? { character: await resolveCharacterShape(asset, style, characterKind, { root, load: loadStyleImage }) } : {},
|
|
7800
|
-
|
|
8078
|
+
// A portrait needs a character style (refused above), so an
|
|
8079
|
+
// objectPro asset is only ever a base, state, or animation.
|
|
8080
|
+
...generator === "objectPro" && !asset.revision && characterKind !== "portrait" ? { objectPro: await resolveObjectProShape(assetId, asset, style, characterKind, { root, load: loadStyleImage }) } : {},
|
|
7801
8081
|
cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? tilesCost(size, terrainTiles) : generator === "isometricTile" ? isometricTileCost() : generationCost(width, height, generator),
|
|
7802
8082
|
costUnit: "generations",
|
|
7803
8083
|
candidates: generator === "tiles" ? tileVariations : generator === "terrain" ? terrainTiles : generator === "1dir" || generator === "imagePro" ? candidateCount(size) : 1
|
|
@@ -7908,7 +8188,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
7908
8188
|
styleAnchor: { assetId: anchorId, spec: anchorSpec, file, sha256: existsSync4(file) ? await sha256File(file) : null }
|
|
7909
8189
|
};
|
|
7910
8190
|
}
|
|
7911
|
-
const characterParent = asset.state?.of ?? asset.animation?.of;
|
|
8191
|
+
const characterParent = asset.state?.of ?? asset.animation?.of ?? asset.portrait?.of;
|
|
7912
8192
|
if (resolved.character && characterParent) {
|
|
7913
8193
|
const parentSpec = await finalize(characterParent);
|
|
7914
8194
|
const parentDirections = parentSpec.character?.directions ?? 8;
|
|
@@ -7941,6 +8221,38 @@ async function resolveSpecs(loaded, filter) {
|
|
|
7941
8221
|
directions: resolved.objectPro.kind === "base" ? resolved.objectPro.directions : parentSpec.objectPro.directions
|
|
7942
8222
|
};
|
|
7943
8223
|
}
|
|
8224
|
+
if (asset.outfit) {
|
|
8225
|
+
if (asset.outfit.of === assetId) throw new Error(`assets.${assetId}: an asset cannot re-clothe itself`);
|
|
8226
|
+
const sourceSpec = await finalize(asset.outfit.of);
|
|
8227
|
+
if (sourceSpec.character?.kind !== "animation") {
|
|
8228
|
+
throw new Error(`assets.${assetId}: outfit "${asset.outfit.of}" is not a character loop`);
|
|
8229
|
+
}
|
|
8230
|
+
const referenceImage = await loadStyleImage(asset.outfit.reference, "Outfit reference image");
|
|
8231
|
+
resolved.character = {
|
|
8232
|
+
kind: "outfit",
|
|
8233
|
+
mode: sourceSpec.character.mode,
|
|
8234
|
+
directions: sourceSpec.character.directions,
|
|
8235
|
+
template: sourceSpec.character.template,
|
|
8236
|
+
parentAssetId: asset.outfit.of,
|
|
8237
|
+
parentSpec: sourceSpec,
|
|
8238
|
+
animation: sourceSpec.character.animation,
|
|
8239
|
+
outfit: {
|
|
8240
|
+
reference: {
|
|
8241
|
+
path: path5.resolve(root, asset.outfit.reference),
|
|
8242
|
+
sha256: referenceImage.hash,
|
|
8243
|
+
width: referenceImage.width,
|
|
8244
|
+
height: referenceImage.height,
|
|
8245
|
+
format: referenceImage.format
|
|
8246
|
+
},
|
|
8247
|
+
...asset.outfit.additionalInstructions ? { additionalInstructions: asset.outfit.additionalInstructions } : {}
|
|
8248
|
+
}
|
|
8249
|
+
};
|
|
8250
|
+
resolved.generator = sourceSpec.generator;
|
|
8251
|
+
resolved.width = sourceSpec.width;
|
|
8252
|
+
resolved.height = sourceSpec.height;
|
|
8253
|
+
resolved.size = sourceSpec.size;
|
|
8254
|
+
resolved.prompt = "";
|
|
8255
|
+
}
|
|
7944
8256
|
if (asset.revision) {
|
|
7945
8257
|
if (!activeProvider.supportsRevision?.(asset.revision.mode)) {
|
|
7946
8258
|
throw new Error(
|
|
@@ -8214,6 +8526,9 @@ async function resolveCharacterShape(asset, style, kind, files) {
|
|
|
8214
8526
|
...asset.state.canvas ? { canvas: asset.state.canvas } : {}
|
|
8215
8527
|
};
|
|
8216
8528
|
}
|
|
8529
|
+
if (asset.portrait) {
|
|
8530
|
+
shape.portrait = { size: asset.portrait.size };
|
|
8531
|
+
}
|
|
8217
8532
|
if (asset.animation) {
|
|
8218
8533
|
const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
|
|
8219
8534
|
const pose = async (rel, what) => {
|
|
@@ -9766,6 +10081,7 @@ import { existsSync as existsSync7 } from "fs";
|
|
|
9766
10081
|
import path11 from "path";
|
|
9767
10082
|
async function inspectRevisionReadiness(spec, lock) {
|
|
9768
10083
|
if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
|
|
10084
|
+
if (spec.character?.kind === "outfit") return inspectOutfit(spec, lock, /* @__PURE__ */ new Set());
|
|
9769
10085
|
if (spec.character?.parentSpec) return inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
|
|
9770
10086
|
if (spec.character?.styleAnchor) return inspectStyleAnchor(spec, lock, /* @__PURE__ */ new Set());
|
|
9771
10087
|
if (spec.objectPro?.parentSpec) return inspectObjectProParent(spec, lock, /* @__PURE__ */ new Set());
|
|
@@ -9792,6 +10108,20 @@ async function inspectMirror(spec, lock, seen) {
|
|
|
9792
10108
|
const dependency = await inspectParent(source, lock, seen);
|
|
9793
10109
|
return dependency.ready ? { ready: true, reason: `${label} is current` } : { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
|
|
9794
10110
|
}
|
|
10111
|
+
async function inspectOutfit(spec, lock, seen) {
|
|
10112
|
+
const character = spec.character;
|
|
10113
|
+
const outfit = character.outfit;
|
|
10114
|
+
const label = `outfit source ${spec.styleId}/${character.parentAssetId}`;
|
|
10115
|
+
const dependency = await inspectParent(character.parentSpec, lock, seen);
|
|
10116
|
+
if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
|
|
10117
|
+
if (!existsSync7(outfit.reference.path)) {
|
|
10118
|
+
return { ready: false, reason: `outfit reference is missing: ${outfit.reference.path}` };
|
|
10119
|
+
}
|
|
10120
|
+
if (await sha256File(outfit.reference.path) !== outfit.reference.sha256) {
|
|
10121
|
+
return { ready: false, reason: "outfit reference changed after the manifest was resolved" };
|
|
10122
|
+
}
|
|
10123
|
+
return { ready: true, reason: `${label} is current` };
|
|
10124
|
+
}
|
|
9795
10125
|
async function inspectCharacterParent(spec, lock, seen) {
|
|
9796
10126
|
const character = spec.character;
|
|
9797
10127
|
const parentSpec = character.parentSpec;
|
|
@@ -9907,7 +10237,10 @@ async function inspectParent(spec, lock, seen) {
|
|
|
9907
10237
|
const inputs = await inspectRevision(spec, lock, nextSeen);
|
|
9908
10238
|
if (!inputs.ready) return inputs;
|
|
9909
10239
|
}
|
|
9910
|
-
if (spec.character?.
|
|
10240
|
+
if (spec.character?.kind === "outfit") {
|
|
10241
|
+
const inputs = await inspectOutfit(spec, lock, nextSeen);
|
|
10242
|
+
if (!inputs.ready) return inputs;
|
|
10243
|
+
} else if (spec.character?.parentSpec) {
|
|
9911
10244
|
const inputs = await inspectCharacterParent(spec, lock, nextSeen);
|
|
9912
10245
|
if (!inputs.ready) return inputs;
|
|
9913
10246
|
}
|
|
@@ -10030,6 +10363,9 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
10030
10363
|
} else if (spec.mirror && entry.status === "downloaded" && entry.outputs.some((_output, index) => !existsSync8(currentEntryOutputPath(entry, spec, index)))) {
|
|
10031
10364
|
state = "stale";
|
|
10032
10365
|
reason = "mirror output missing on disk; flip it again (no generation cost)";
|
|
10366
|
+
} else if (spec.character?.kind === "outfit" && entry.outfit?.sourceSha256 !== mirrorSourceHash(lock.entries[lockKey(spec.styleId, spec.character.parentAssetId)] ?? { outputs: [] })) {
|
|
10367
|
+
state = "stale";
|
|
10368
|
+
reason = `outfit source ${spec.character.parentAssetId} changed; re-clothe the loop again`;
|
|
10033
10369
|
} else if (entry.status === "download-failed") {
|
|
10034
10370
|
state = "recoverable";
|
|
10035
10371
|
const force = entry.error?.includes("pass --force") ? " --force" : "";
|
|
@@ -12727,6 +13063,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
12727
13063
|
const supersededOutputs = previousEntry?.outputs.length ? previousEntry.outputs : previousEntry?.supersededOutputs ?? [];
|
|
12728
13064
|
const submittedAt = resumesCheckpoint ? previousEntry.submittedAt : (/* @__PURE__ */ new Date()).toISOString();
|
|
12729
13065
|
const history = historyAfterReplacing(previousEntry, historyLimit(loaded.manifest), submittedAt ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
13066
|
+
const outfitParentEntry = spec.character?.kind === "outfit" && spec.character.parentSpec ? lock.entries[lockKey(spec.character.parentSpec.styleId, spec.character.parentSpec.assetId)] : void 0;
|
|
12730
13067
|
upsert(lock, key, {
|
|
12731
13068
|
styleId: spec.styleId,
|
|
12732
13069
|
assetId: spec.assetId,
|
|
@@ -12752,6 +13089,11 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
12752
13089
|
...spec.revision.direction ? { direction: spec.revision.direction } : {},
|
|
12753
13090
|
...spec.revision.enhancePrompt == null ? {} : { enhancePrompt: spec.revision.enhancePrompt }
|
|
12754
13091
|
} : null,
|
|
13092
|
+
outfit: spec.character?.kind === "outfit" && outfitParentEntry ? {
|
|
13093
|
+
sourceAssetId: spec.character.parentAssetId,
|
|
13094
|
+
sourceSha256: mirrorSourceHash(outfitParentEntry),
|
|
13095
|
+
referenceSha256: spec.character.outfit.reference.sha256
|
|
13096
|
+
} : null,
|
|
12755
13097
|
status: "pending",
|
|
12756
13098
|
jobId: previousJobId ?? null,
|
|
12757
13099
|
...previousJobId ? { submissionComplete: false } : { submissionComplete: void 0 },
|
|
@@ -12785,10 +13127,15 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
12785
13127
|
const anchor = spec.character?.styleAnchor;
|
|
12786
13128
|
const anchorEntry = anchor ? lock.entries[lockKey(anchor.spec.styleId, anchor.spec.assetId)] : void 0;
|
|
12787
13129
|
const replacedMetadata = previousEntry?.providerMetadata?.[provider.id];
|
|
13130
|
+
const parentOutputs = spec.character?.kind === "outfit" && parentEntry ? parentEntry.outputs.map((output, index) => ({
|
|
13131
|
+
path: currentEntryOutputPath(parentEntry, spec.character.parentSpec, index),
|
|
13132
|
+
role: output.role
|
|
13133
|
+
})) : void 0;
|
|
12788
13134
|
const { jobId, metadata } = await provider.submit(spec, refs, {
|
|
12789
13135
|
...previousJobId ? { previousJobId } : {},
|
|
12790
13136
|
...parentEntry?.objectId ? { parentObjectId: parentEntry.objectId } : {},
|
|
12791
13137
|
...anchorEntry?.objectId ? { styleObjectId: anchorEntry.objectId } : {},
|
|
13138
|
+
...parentOutputs ? { parentOutputs } : {},
|
|
12792
13139
|
...replacedMetadata ? { replacedMetadata } : {},
|
|
12793
13140
|
...previousMetadata ? { previousMetadata } : {},
|
|
12794
13141
|
checkpoint: async (checkpoint) => {
|
|
@@ -14907,7 +15254,7 @@ function describeCharacter(spec, entry) {
|
|
|
14907
15254
|
};
|
|
14908
15255
|
}
|
|
14909
15256
|
if (entry?.generator === "character") {
|
|
14910
|
-
const kind = recordedCharacter?.kind === "animation" || entry.outputs.some((o) => o.role?.startsWith("frame-")) ? "animation" : "base";
|
|
15257
|
+
const kind = recordedCharacter?.kind === "portrait" ? "portrait" : recordedCharacter?.kind === "outfit" ? "outfit" : recordedCharacter?.kind === "animation" || entry.outputs.some((o) => o.role?.startsWith("frame-")) ? "animation" : "base";
|
|
14911
15258
|
return {
|
|
14912
15259
|
generator: "character",
|
|
14913
15260
|
kind,
|
|
@@ -15268,7 +15615,9 @@ async function buildGallerySnapshot(opts) {
|
|
|
15268
15615
|
characters: style?.generator === "character" || style?.generator === "objectPro" ? {
|
|
15269
15616
|
bases: styleItems.filter((item) => item.character?.kind === "base").length,
|
|
15270
15617
|
states: styleItems.filter((item) => item.character?.kind === "state").length,
|
|
15271
|
-
animations: styleItems.filter((item) => item.character?.kind === "animation").length
|
|
15618
|
+
animations: styleItems.filter((item) => item.character?.kind === "animation").length,
|
|
15619
|
+
portraits: styleItems.filter((item) => item.character?.kind === "portrait").length,
|
|
15620
|
+
outfits: styleItems.filter((item) => item.character?.kind === "outfit").length
|
|
15272
15621
|
} : null,
|
|
15273
15622
|
tags: style?.tags ?? [],
|
|
15274
15623
|
extends: typeof rawStyle?.extends === "string" ? rawStyle.extends : null,
|
|
@@ -16435,6 +16784,9 @@ async function loadClaims(lockPaths, opts = {}) {
|
|
|
16435
16784
|
add(entry.objectId);
|
|
16436
16785
|
add(entry.reviewObjectId);
|
|
16437
16786
|
add(entry.jobId);
|
|
16787
|
+
for (const id of [entry.objectId, entry.jobId]) {
|
|
16788
|
+
if (id?.includes("#")) add(id.split("#")[0]);
|
|
16789
|
+
}
|
|
16438
16790
|
}
|
|
16439
16791
|
}
|
|
16440
16792
|
return claimed;
|
|
@@ -16458,6 +16810,54 @@ async function findOrphans(provider, claimed, opts = {}) {
|
|
|
16458
16810
|
})).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
16459
16811
|
return { orphans, total: all.length };
|
|
16460
16812
|
}
|
|
16813
|
+
function isState(character) {
|
|
16814
|
+
return Boolean(character.stateName && character.stateName !== "Idle");
|
|
16815
|
+
}
|
|
16816
|
+
function characterSalvageAssets(characters, ctx) {
|
|
16817
|
+
const ordered = [...characters].sort((a, b) => Number(isState(a)) - Number(isState(b)) || a.createdAt.localeCompare(b.createdAt));
|
|
16818
|
+
const base = ordered[0];
|
|
16819
|
+
const common = { styles: [ctx.styleId], tags: ["salvaged"] };
|
|
16820
|
+
const assets = {};
|
|
16821
|
+
const skipped = [];
|
|
16822
|
+
const baseId = idFromPrompt(base.name || base.prompt, ctx.taken);
|
|
16823
|
+
const size = base.width === base.height && base.width >= 32 && base.width <= 256 && base.width !== ctx.styleSize ? { size: base.width } : {};
|
|
16824
|
+
assets[baseId] = { prompt: base.prompt || base.name || baseId, remoteId: base.id, ...size, ...common };
|
|
16825
|
+
const assetIdOf = /* @__PURE__ */ new Map([[base.id, baseId]]);
|
|
16826
|
+
for (const state of ordered.slice(1)) {
|
|
16827
|
+
const stateId = uniqueId(`${baseId}_${slug(state.stateName ?? "state")}`, ctx.taken);
|
|
16828
|
+
assetIdOf.set(state.id, stateId);
|
|
16829
|
+
assets[stateId] = { prompt: state.prompt || state.stateName || stateId, state: { of: baseId }, remoteId: state.id, ...common };
|
|
16830
|
+
}
|
|
16831
|
+
for (const character of ordered) {
|
|
16832
|
+
const parentId = assetIdOf.get(character.id);
|
|
16833
|
+
for (const animation of character.animations) {
|
|
16834
|
+
if (!animation.frames.length) continue;
|
|
16835
|
+
const label = animation.name || animation.type;
|
|
16836
|
+
if (!animation.groupId) {
|
|
16837
|
+
skipped.push(`${parentId}: "${label}" ${animation.direction} loop has no animation group id to adopt it by`);
|
|
16838
|
+
continue;
|
|
16839
|
+
}
|
|
16840
|
+
const loopId = uniqueId(`${parentId}.${slug(label)}.${animation.direction}`, ctx.taken);
|
|
16841
|
+
assets[loopId] = {
|
|
16842
|
+
prompt: label,
|
|
16843
|
+
animation: { of: parentId, direction: animation.direction },
|
|
16844
|
+
remoteId: `${character.id}#${animation.groupId}`,
|
|
16845
|
+
...common
|
|
16846
|
+
};
|
|
16847
|
+
}
|
|
16848
|
+
}
|
|
16849
|
+
return { assets, skipped };
|
|
16850
|
+
}
|
|
16851
|
+
function slug(text) {
|
|
16852
|
+
return text.toLowerCase().replace(/^pixelkiln:.*\//, "").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "loop";
|
|
16853
|
+
}
|
|
16854
|
+
function uniqueId(base, taken) {
|
|
16855
|
+
let id = base;
|
|
16856
|
+
let n = 2;
|
|
16857
|
+
while (taken.has(id)) id = `${base}_${n++}`;
|
|
16858
|
+
taken.add(id);
|
|
16859
|
+
return id;
|
|
16860
|
+
}
|
|
16461
16861
|
var MIN_STYLE_FRAGMENT_LENGTH = 12;
|
|
16462
16862
|
function matchStyleByPattern(prompt, manifest) {
|
|
16463
16863
|
const p = prompt.toLowerCase();
|
|
@@ -16587,18 +16987,22 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
16587
16987
|
let tagged = 0;
|
|
16588
16988
|
let failed = 0;
|
|
16589
16989
|
for (const { id, action } of decisions) {
|
|
16590
|
-
const
|
|
16591
|
-
|
|
16592
|
-
)
|
|
16593
|
-
|
|
16594
|
-
|
|
16595
|
-
|
|
16596
|
-
|
|
16597
|
-
|
|
16598
|
-
tagged
|
|
16599
|
-
|
|
16600
|
-
|
|
16601
|
-
|
|
16990
|
+
const group = opts.characters?.get(id);
|
|
16991
|
+
const targets = group ?? [{ id, tags: existing.get(id) ?? [] }];
|
|
16992
|
+
for (const target of targets) {
|
|
16993
|
+
const current = target.tags.filter(
|
|
16994
|
+
(t) => t !== "pixelkiln:keep" && t !== "pixelkiln:discard" && t !== "pixelkiln:imported"
|
|
16995
|
+
);
|
|
16996
|
+
const decisionTag = `pixelkiln:${action === "import" ? "imported" : action}`;
|
|
16997
|
+
const next = [...current.slice(0, 19), decisionTag];
|
|
16998
|
+
if (!provider.setTags) return { tagged, failed };
|
|
16999
|
+
try {
|
|
17000
|
+
await provider.setTags(target.id, next, group ? "character" : void 0);
|
|
17001
|
+
tagged++;
|
|
17002
|
+
} catch (err) {
|
|
17003
|
+
failed++;
|
|
17004
|
+
log(` tag failed ${target.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
17005
|
+
}
|
|
16602
17006
|
}
|
|
16603
17007
|
}
|
|
16604
17008
|
return { tagged, failed };
|
|
@@ -16611,7 +17015,7 @@ import path33 from "path";
|
|
|
16611
17015
|
// src/pick/salvage-sheet.ts
|
|
16612
17016
|
var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
16613
17017
|
function renderSalvageSheet(orphans, ctx) {
|
|
16614
|
-
const safe = orphans.map((o) => ({ ...o, prompt: escapeHtml3(o.prompt) }));
|
|
17018
|
+
const safe = orphans.map((o) => ({ ...o, prompt: escapeHtml3(o.prompt), ...o.note ? { note: escapeHtml3(o.note) } : {} }));
|
|
16615
17019
|
const data = JSON.stringify(safe).replace(/<\//g, "<\\/");
|
|
16616
17020
|
const styleId = ctx ? escapeHtml3(ctx.styleId) : null;
|
|
16617
17021
|
const importDir = ctx ? escapeHtml3(ctx.importDir) : null;
|
|
@@ -16697,6 +17101,7 @@ ITEMS.forEach((o, i) => {
|
|
|
16697
17101
|
el.innerHTML =
|
|
16698
17102
|
'<div class="thumb"><img src="' + o.previewUrl + '" loading="lazy"></div>' +
|
|
16699
17103
|
'<div class="meta">' + o.width + '\xD7' + o.height + ' \xB7 ' + o.createdAt.slice(0,10) + '</div>' +
|
|
17104
|
+
(o.note ? '<div class="meta">' + o.note + '</div>' : '') +
|
|
16700
17105
|
'<div class="prompt">' + o.prompt + '</div>' +
|
|
16701
17106
|
'<div class="acts">' +
|
|
16702
17107
|
'<button data-a="import">import</button>' +
|
|
@@ -16775,6 +17180,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
16775
17180
|
});
|
|
16776
17181
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
16777
17182
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
17183
|
+
const characterMembers = new Map(
|
|
17184
|
+
orphans.flatMap((o) => o.kind === "character" && o.members ? [[o.id, o.members]] : [])
|
|
17185
|
+
);
|
|
16778
17186
|
return serveReviewPage({
|
|
16779
17187
|
html,
|
|
16780
17188
|
port: opts.port,
|
|
@@ -16791,10 +17199,34 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
16791
17199
|
const result = { imported: 0, kept: 0, discarded: 0, failed: 0 };
|
|
16792
17200
|
const taken = new Set(Object.keys(ctx.manifest.assets));
|
|
16793
17201
|
const importedAssetIds = [];
|
|
17202
|
+
const characterAssetIds = [];
|
|
16794
17203
|
for (const decision of decisions) {
|
|
16795
17204
|
const orphan = byId.get(decision.id);
|
|
16796
17205
|
if (!orphan) continue;
|
|
16797
|
-
if (decision.action === "import") {
|
|
17206
|
+
if (decision.action === "import" && orphan.kind === "character") {
|
|
17207
|
+
try {
|
|
17208
|
+
if (!provider.getCharacter) throw new Error(`provider ${provider.id} cannot read characters`);
|
|
17209
|
+
const details = [];
|
|
17210
|
+
for (const member of orphan.members ?? [{ id: orphan.id }]) details.push(await provider.getCharacter(member.id));
|
|
17211
|
+
const style = ctx.manifest.styles[ctx.styleId];
|
|
17212
|
+
const { assets, skipped } = characterSalvageAssets(details, {
|
|
17213
|
+
styleId: ctx.styleId,
|
|
17214
|
+
styleSize: style?.size,
|
|
17215
|
+
taken
|
|
17216
|
+
});
|
|
17217
|
+
for (const [assetId, asset] of Object.entries(assets)) {
|
|
17218
|
+
ctx.manifest.assets[assetId] = asset;
|
|
17219
|
+
importedAssetIds.push(assetId);
|
|
17220
|
+
characterAssetIds.push(assetId);
|
|
17221
|
+
}
|
|
17222
|
+
for (const line of skipped) log(` not imported: ${line}`);
|
|
17223
|
+
result.imported++;
|
|
17224
|
+
log(` imported character ${orphan.id} as ${Object.keys(assets).length} asset(s)`);
|
|
17225
|
+
} catch (err) {
|
|
17226
|
+
result.failed++;
|
|
17227
|
+
log(` import failed ${orphan.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
17228
|
+
}
|
|
17229
|
+
} else if (decision.action === "import") {
|
|
16798
17230
|
try {
|
|
16799
17231
|
const buf = await provider.download(orphan.previewUrl);
|
|
16800
17232
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
@@ -16858,11 +17290,12 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
16858
17290
|
result.discarded++;
|
|
16859
17291
|
}
|
|
16860
17292
|
}
|
|
16861
|
-
await applyTags(provider, decisions, existingTags, { onProgress: log });
|
|
17293
|
+
await applyTags(provider, decisions, existingTags, { onProgress: log, characters: characterMembers });
|
|
16862
17294
|
const raw = JSON.parse(await readFile26(ctx.manifestPath, "utf8"));
|
|
16863
17295
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
16864
17296
|
await writeFile13(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
16865
17297
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
17298
|
+
if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
|
|
16866
17299
|
return result;
|
|
16867
17300
|
}
|
|
16868
17301
|
});
|
|
@@ -16978,6 +17411,9 @@ export {
|
|
|
16978
17411
|
CharacterAnimationSchema,
|
|
16979
17412
|
CharacterDirectionSchema,
|
|
16980
17413
|
CharacterModeSchema,
|
|
17414
|
+
CharacterOutfitSchema,
|
|
17415
|
+
CharacterPortraitSchema,
|
|
17416
|
+
CharacterPortraitSizeSchema,
|
|
16981
17417
|
CharacterProportionsSchema,
|
|
16982
17418
|
CharacterStateSchema,
|
|
16983
17419
|
ComfyUIProvider,
|