pixelkiln 0.89.6 → 0.91.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/dist/cli.js +194 -29
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.css +1 -0
- package/dist/client/gallery.js +54 -17
- package/dist/index.cjs +196 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +282 -14
- package/dist/index.d.ts +282 -14
- package/dist/index.js +195 -29
- package/dist/index.js.map +1 -1
- package/docs/CHARACTERS.md +73 -3
- package/docs/ENDPOINTS.md +12 -1
- package/docs/MANIFEST.md +2 -2
- package/docs/PIXELLAB.md +46 -0
- package/docs/REVISIONS.md +8 -5
- package/package.json +1 -1
- package/schema/manifest.schema.json +5 -1
- package/skills/pixelkiln/references/pixellab-roadmap.md +16 -15
- package/skills/pixelkiln/references/pixellab.md +11 -0
package/dist/index.cjs
CHANGED
|
@@ -284,6 +284,7 @@ __export(index_exports, {
|
|
|
284
284
|
readVfxConversation: () => readVfxConversation,
|
|
285
285
|
readVfxRecipe: () => readVfxRecipe,
|
|
286
286
|
recipeDigest: () => recipeDigest,
|
|
287
|
+
recordedAnimationGroup: () => recordedAnimationGroup,
|
|
287
288
|
referencedHashes: () => referencedHashes,
|
|
288
289
|
refineAsset: () => refineAsset,
|
|
289
290
|
refineFrameSet: () => refineFrameSet,
|
|
@@ -692,16 +693,30 @@ var CharacterAnimationSchema = import_zod.z.object({
|
|
|
692
693
|
* `src/loop-directions.ts`.
|
|
693
694
|
*/
|
|
694
695
|
directions: import_zod.z.array(CharacterDirectionSchema).min(1).optional(),
|
|
695
|
-
/**
|
|
696
|
-
|
|
696
|
+
/**
|
|
697
|
+
* A sentence loop (`mode: "skeleton-v3"`, no `template`) of another
|
|
698
|
+
* direction whose motion this one shares: the asset id of the loop that
|
|
699
|
+
* leads. This loop is then submitted after it, into its animation group,
|
|
700
|
+
* so PixelLab draws it from the same 3D motion. Written for you when
|
|
701
|
+
* `directions` expands (the first direction leads); set by hand only when
|
|
702
|
+
* declaring the loops explicitly.
|
|
703
|
+
*/
|
|
704
|
+
motionFrom: import_zod.z.string().min(1).optional(),
|
|
705
|
+
/**
|
|
706
|
+
* Frames to generate without a template. A v3 loop takes an even 4 to 16;
|
|
707
|
+
* a sentence loop (`mode: "skeleton-v3"` with no `template`) takes 3 to 15.
|
|
708
|
+
*/
|
|
709
|
+
frames: import_zod.z.number().int().min(3).max(16).optional(),
|
|
697
710
|
/** Playback rate recorded with the frames; PixelLab does not store one. */
|
|
698
711
|
fps: import_zod.z.number().int().min(1).max(60).default(8),
|
|
699
712
|
/** v3 only: keep the resting pose as frame 0, so `frames` generated frames land as `frames + 1` files. */
|
|
700
713
|
keepFirstFrame: import_zod.z.boolean().default(true),
|
|
701
714
|
/**
|
|
702
715
|
* `template` when a template is named, otherwise `v3`; `skeleton-v3`
|
|
703
|
-
* poses a template with PixelLab's skeleton model instead
|
|
704
|
-
*
|
|
716
|
+
* poses a template with PixelLab's skeleton model instead, or, with no
|
|
717
|
+
* template, animates the asset's prompt as a sentence written into one 3D
|
|
718
|
+
* motion (a v3 base; 3-15 `frames`); `pro` for the sequential
|
|
719
|
+
* high-quality engine.
|
|
705
720
|
*/
|
|
706
721
|
mode: CharacterAnimationModeSchema.optional(),
|
|
707
722
|
/**
|
|
@@ -731,15 +746,23 @@ var CharacterAnimationSchema = import_zod.z.object({
|
|
|
731
746
|
path: ["directions"]
|
|
732
747
|
});
|
|
733
748
|
}
|
|
734
|
-
|
|
735
|
-
|
|
749
|
+
const sentence = animation.mode === "skeleton-v3" && !animation.template;
|
|
750
|
+
if (animation.frames !== void 0 && !sentence) {
|
|
751
|
+
if (animation.frames % 2 !== 0) {
|
|
752
|
+
context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "frames must be even", path: ["frames"] });
|
|
753
|
+
} else if (animation.frames < 4) {
|
|
754
|
+
context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "frames must be 4 to 16", path: ["frames"] });
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (animation.frames !== void 0 && sentence && animation.frames > 15) {
|
|
758
|
+
context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "a skeleton-v3 loop from a sentence takes 3 to 15 frames", path: ["frames"] });
|
|
736
759
|
}
|
|
737
760
|
const mode2 = animation.mode ?? (animation.template ? "template" : "v3");
|
|
738
761
|
for (const key of ["startFrame", "endFrame", "enhancePrompt"]) {
|
|
739
762
|
if (animation[key] !== void 0 && mode2 !== "v3") {
|
|
740
763
|
context.addIssue({
|
|
741
764
|
code: import_zod.z.ZodIssueCode.custom,
|
|
742
|
-
message: `${key} is for v3 loops; a ${mode2} loop ${mode2 === "template" || mode2 === "skeleton-v3" ?
|
|
765
|
+
message: `${key} is for v3 loops; a ${mode2} loop ${mode2 === "template" || mode2 === "skeleton-v3" ? `starts from the character's rotation and ${animation.template ? "follows its template" : "animates its sentence"}` : "takes neither"}`,
|
|
743
766
|
path: [key]
|
|
744
767
|
});
|
|
745
768
|
}
|
|
@@ -763,8 +786,14 @@ var CharacterAnimationSchema = import_zod.z.object({
|
|
|
763
786
|
path: ["frames"]
|
|
764
787
|
});
|
|
765
788
|
}
|
|
766
|
-
if (!animation.template &&
|
|
767
|
-
context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message:
|
|
789
|
+
if (!animation.template && animation.mode === "template") {
|
|
790
|
+
context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "template mode needs a template", path: ["template"] });
|
|
791
|
+
}
|
|
792
|
+
if (animation.motionFrom !== void 0 && !sentence) {
|
|
793
|
+
context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "motionFrom applies to a skeleton-v3 loop from a sentence (no template)", path: ["motionFrom"] });
|
|
794
|
+
}
|
|
795
|
+
if (sentence && animation.subject !== void 0) {
|
|
796
|
+
context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "subject does not apply to a skeleton-v3 loop from a sentence; describe the motion in the prompt", path: ["subject"] });
|
|
768
797
|
}
|
|
769
798
|
});
|
|
770
799
|
var RevisionDitheringSchema = import_zod.z.enum(["none", "2x2", "4x4", "8x8"]);
|
|
@@ -1695,6 +1724,7 @@ var LockTerrainConnectSchema = import_zod.z.record(
|
|
|
1695
1724
|
TerrainSideSchema,
|
|
1696
1725
|
import_zod.z.object({ sourceAssetId: import_zod.z.string().min(1), sourceObjectId: import_zod.z.string().min(1) }).strict()
|
|
1697
1726
|
);
|
|
1727
|
+
var LockMotionShareSchema = import_zod.z.object({ sourceAssetId: import_zod.z.string().min(1), animationGroupId: import_zod.z.string().min(1).nullable() }).strict();
|
|
1698
1728
|
var LockTerrainSettingsSchema = import_zod.z.object({
|
|
1699
1729
|
terrainMode: import_zod.z.string().optional(),
|
|
1700
1730
|
terrainShapeStyle: import_zod.z.string().optional(),
|
|
@@ -1731,6 +1761,8 @@ var LockHistoryEntrySchema = import_zod.z.object({
|
|
|
1731
1761
|
postprocess: PostprocessSchema.optional(),
|
|
1732
1762
|
/** A connected terrain set's other sets; see LockEntrySchema.terrainConnect. */
|
|
1733
1763
|
terrainConnect: LockTerrainConnectSchema.optional(),
|
|
1764
|
+
/** A sentence loop that shares another direction's motion; see LockEntrySchema.motionShare. */
|
|
1765
|
+
motionShare: LockMotionShareSchema.optional(),
|
|
1734
1766
|
/** A terrain set's settings outside specHash; see LockEntrySchema.terrainSettings. */
|
|
1735
1767
|
terrainSettings: LockTerrainSettingsSchema.optional(),
|
|
1736
1768
|
/** When the replacement was submitted. */
|
|
@@ -1807,6 +1839,16 @@ var LockEntrySchema = import_zod.z.object({
|
|
|
1807
1839
|
* Absent (not null) otherwise, so other entries do not grow the key.
|
|
1808
1840
|
*/
|
|
1809
1841
|
terrainConnect: LockTerrainConnectSchema.optional(),
|
|
1842
|
+
/**
|
|
1843
|
+
* For a sentence loop of a later direction: the loop whose motion it
|
|
1844
|
+
* shares and the animation group of that loop's generation this was
|
|
1845
|
+
* drawn into when submitted. Tracked the way `terrainConnect` is, not
|
|
1846
|
+
* through `specHash`: the group lives in the lockfile. A regenerated leader
|
|
1847
|
+
* has a new group, so the plan marks this stale. `animationGroupId` is
|
|
1848
|
+
* null when the leader had no group recorded and this was drawn on its own.
|
|
1849
|
+
* Absent otherwise.
|
|
1850
|
+
*/
|
|
1851
|
+
motionShare: LockMotionShareSchema.optional(),
|
|
1810
1852
|
/**
|
|
1811
1853
|
* For a terrain set: the settings it was submitted with that `specHash`
|
|
1812
1854
|
* leaves out (see LockTerrainSettingsSchema). The plan marks the entry
|
|
@@ -2272,6 +2314,8 @@ var CharacterSubmitSchema = import_zod3.z.object({
|
|
|
2272
2314
|
var AnimateSubmitSchema = import_zod3.z.object({
|
|
2273
2315
|
background_job_ids: import_zod3.z.array(import_zod3.z.string()),
|
|
2274
2316
|
directions: import_zod3.z.array(import_zod3.z.string()),
|
|
2317
|
+
/** skeleton-v3 only: the group the directions were drawn into. */
|
|
2318
|
+
animation_group_id: import_zod3.z.string().nullable().optional(),
|
|
2275
2319
|
status: import_zod3.z.string().default("processing"),
|
|
2276
2320
|
usage: UsageSchema
|
|
2277
2321
|
}).passthrough();
|
|
@@ -2992,7 +3036,13 @@ var PixelLabClient = class {
|
|
|
2992
3036
|
* Every mode but skeleton-v3 goes to `/animate-character`, the path whose
|
|
2993
3037
|
* shapes were measured live. skeleton-v3 is documented only on
|
|
2994
3038
|
* `/characters/animations`, which takes the same request and answers with
|
|
2995
|
-
* the same job list (plus an `animation_group_id`).
|
|
3039
|
+
* the same job list (plus an `animation_group_id`). With no `template` it
|
|
3040
|
+
* animates `actionDescription` as a sentence, 3 to 15 `frameCount` frames
|
|
3041
|
+
* written into one 3D motion; measured on a v3 character (8 frames, 64px:
|
|
3042
|
+
* 3 generations, frames back at 96x96). `animationGroupId` (skeleton-v3
|
|
3043
|
+
* only) adds the directions to an existing animation of the character,
|
|
3044
|
+
* drawn from its motion; the group's own name must be sent again as
|
|
3045
|
+
* `animationName`, or the group appears renamed.
|
|
2996
3046
|
*/
|
|
2997
3047
|
async animateCharacter(args) {
|
|
2998
3048
|
const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
|
|
@@ -3004,10 +3054,12 @@ var PixelLabClient = class {
|
|
|
3004
3054
|
animation_name: args.animationName,
|
|
3005
3055
|
mode: args.mode,
|
|
3006
3056
|
directions: args.directions,
|
|
3057
|
+
...args.mode === "skeleton-v3" && args.animationGroupId ? { animation_group_id: args.animationGroupId } : {},
|
|
3007
3058
|
...args.template ? { template_animation_id: args.template } : {},
|
|
3008
3059
|
...args.actionDescription ? { action_description: args.actionDescription } : {},
|
|
3009
3060
|
...args.description ? { description: args.description } : {},
|
|
3010
|
-
|
|
3061
|
+
// A template fixes its own frame count; v3 and a skeleton-v3 sentence take one.
|
|
3062
|
+
...(args.mode === "v3" || args.mode === "skeleton-v3" && !args.template) && args.frameCount ? { frame_count: args.frameCount } : {},
|
|
3011
3063
|
...args.mode === "v3" && args.keepFirstFrame === false ? { keep_first_frame: false } : {},
|
|
3012
3064
|
...args.mode === "v3" && args.startFrame ? { custom_start_frame: encode(args.startFrame) } : {},
|
|
3013
3065
|
...args.mode === "v3" && args.endFrame ? { end_frame: encode(args.endFrame) } : {},
|
|
@@ -4375,6 +4427,10 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
4375
4427
|
template: spec.character.template,
|
|
4376
4428
|
parent: spec.character.parentAssetId,
|
|
4377
4429
|
parentSha256: spec.character.parentSha256 ?? null,
|
|
4430
|
+
// Which loop leads, not its group: the group is in the lockfile,
|
|
4431
|
+
// and the plan compares it there. Absent for every loop that
|
|
4432
|
+
// shares nothing, so no existing hash moves.
|
|
4433
|
+
motionFrom: spec.character.motionLeader?.assetId,
|
|
4378
4434
|
state: spec.character.state,
|
|
4379
4435
|
// Pose images enter as their hashes, never their paths.
|
|
4380
4436
|
animation: spec.character.animation ? {
|
|
@@ -5419,7 +5475,13 @@ function decodeAnimationJob(jobId) {
|
|
|
5419
5475
|
try {
|
|
5420
5476
|
const value = JSON.parse(Buffer.from(jobId.slice("character-animation:".length), "base64url").toString("utf8"));
|
|
5421
5477
|
if (typeof value?.characterId === "string" && typeof value.name === "string" && typeof value.direction === "string") {
|
|
5422
|
-
return {
|
|
5478
|
+
return {
|
|
5479
|
+
characterId: value.characterId,
|
|
5480
|
+
name: value.name,
|
|
5481
|
+
direction: value.direction,
|
|
5482
|
+
jobIds: Array.isArray(value.jobIds) ? value.jobIds : [],
|
|
5483
|
+
...typeof value.groupId === "string" && value.groupId ? { groupId: value.groupId } : {}
|
|
5484
|
+
};
|
|
5423
5485
|
}
|
|
5424
5486
|
} catch {
|
|
5425
5487
|
}
|
|
@@ -5659,6 +5721,16 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5659
5721
|
if (skeletonTemplate && !CHARACTER_TEMPLATES.includes(skeletonTemplate)) {
|
|
5660
5722
|
throw new Error(`PixelLab animate-skeleton template "${skeletonTemplate}" is not one of ${CHARACTER_TEMPLATES.join(", ")}`);
|
|
5661
5723
|
}
|
|
5724
|
+
if (!spec.revision.description?.trim()) {
|
|
5725
|
+
throw new Error(
|
|
5726
|
+
`${spec.styleId}/${spec.assetId}: an animate-skeleton revision needs revision.description, what the character looks like (for example "a knight in steel armour holding a sword"); PixelLab refuses the request without it`
|
|
5727
|
+
);
|
|
5728
|
+
}
|
|
5729
|
+
if (!spec.prompt.trim()) {
|
|
5730
|
+
throw new Error(
|
|
5731
|
+
`${spec.styleId}/${spec.assetId}: an animate-skeleton revision needs a prompt, a short motion label (for example "swinging the sword")`
|
|
5732
|
+
);
|
|
5733
|
+
}
|
|
5662
5734
|
}
|
|
5663
5735
|
if (spec.revision?.mode === "animate" || spec.revision?.mode === "animate-pixminimax") {
|
|
5664
5736
|
const { sourceWidth: width, sourceHeight: height, mode: mode2, frames, lastFrameWidth, lastFrameHeight } = spec.revision;
|
|
@@ -6062,12 +6134,18 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6062
6134
|
`${spec.styleId}/${spec.assetId} animates "${character.animation.direction}", but its character has ${character.directions} directions: ${allowed.join(", ")}`
|
|
6063
6135
|
);
|
|
6064
6136
|
}
|
|
6065
|
-
|
|
6066
|
-
|
|
6137
|
+
const sentence = character.animation.mode === "skeleton-v3" && !character.animation.template;
|
|
6138
|
+
if (character.animation.mode !== "template" && (character.animation.mode !== "skeleton-v3" || sentence) && !spec.prompt.trim()) {
|
|
6139
|
+
throw new Error(
|
|
6140
|
+
`${spec.styleId}/${spec.assetId} needs a prompt describing the motion, or a template` + (sentence ? ' (a skeleton-v3 loop with no template animates its prompt, e.g. "swinging a sword in a wide arc")' : "")
|
|
6141
|
+
);
|
|
6142
|
+
}
|
|
6143
|
+
if (sentence && (character.animation.frames < 3 || character.animation.frames > 15)) {
|
|
6144
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: a skeleton-v3 loop from a sentence takes 3 to 15 frames; ${character.animation.frames} is out of range`);
|
|
6067
6145
|
}
|
|
6068
6146
|
if (character.animation.mode === "skeleton-v3" && character.mode === "standard") {
|
|
6069
6147
|
throw new Error(
|
|
6070
|
-
`${spec.styleId}/${spec.assetId}: skeleton-v3 poses a template onto a 3D skeleton, which a ${character.mode} character has none of; use a v3 base (style mode: "v3"), or mode: "template" for this loop`
|
|
6148
|
+
sentence ? `${spec.styleId}/${spec.assetId}: a skeleton-v3 loop from a sentence writes the motion onto a 3D skeleton, which a ${character.mode} character has none of; use a v3 base (style mode: "v3"), or mode: "template" with a template for this loop` : `${spec.styleId}/${spec.assetId}: skeleton-v3 poses a template onto a 3D skeleton, which a ${character.mode} character has none of; use a v3 base (style mode: "v3"), or mode: "template" for this loop`
|
|
6071
6149
|
);
|
|
6072
6150
|
}
|
|
6073
6151
|
}
|
|
@@ -6241,11 +6319,18 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6241
6319
|
};
|
|
6242
6320
|
}
|
|
6243
6321
|
const animation = character.animation;
|
|
6244
|
-
const
|
|
6322
|
+
const ownName = characterAnimationName(spec);
|
|
6323
|
+
const shared = context?.motionGroup;
|
|
6324
|
+
const name = shared?.animationName ?? ownName;
|
|
6245
6325
|
const replaced = context?.replacedMetadata?.character ?? {};
|
|
6246
6326
|
const existing = await this.client.getCharacter(parentId);
|
|
6327
|
+
if (shared && !existing.animations.some((group) => group.animation_group_id === shared.animationGroupId && group.directions.some((d) => d.direction !== animation.direction))) {
|
|
6328
|
+
throw new Error(
|
|
6329
|
+
`${spec.styleId}/${spec.assetId} shares the motion of an animation group (${shared.animationGroupId}) that is no longer on the character; regenerate the leading direction first`
|
|
6330
|
+
);
|
|
6331
|
+
}
|
|
6247
6332
|
for (const group of existing.animations) {
|
|
6248
|
-
const ours = group.animation_group_id && (group.animation_group_id === replaced.animationGroupId || group.display_name === name || replaced.animationId && group.directions.some((d) => d.frames.some((url) => url.includes(`/animations/${replaced.animationId}/`))));
|
|
6333
|
+
const ours = group.animation_group_id && (group.animation_group_id === replaced.animationGroupId || group.display_name === name || group.display_name === ownName || replaced.animationId && group.directions.some((d) => d.frames.some((url) => url.includes(`/animations/${replaced.animationId}/`))));
|
|
6249
6334
|
if (!ours || !group.animation_group_id) continue;
|
|
6250
6335
|
if (group.directions.some((d) => d.direction === animation.direction)) {
|
|
6251
6336
|
await this.client.deleteCharacterAnimations(parentId, { animationGroupId: group.animation_group_id }, animation.direction);
|
|
@@ -6260,6 +6345,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6260
6345
|
frameCount: animation.frames,
|
|
6261
6346
|
keepFirstFrame: animation.keepFirstFrame,
|
|
6262
6347
|
directions: [animation.direction],
|
|
6348
|
+
...shared ? { animationGroupId: shared.animationGroupId } : {},
|
|
6263
6349
|
seed: spec.seed,
|
|
6264
6350
|
textGuidanceScale: character.textGuidanceScale,
|
|
6265
6351
|
isometric: character.isometric,
|
|
@@ -6272,10 +6358,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6272
6358
|
enhancePrompt: animation.enhancePrompt,
|
|
6273
6359
|
paletteSwatchBase64: spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0
|
|
6274
6360
|
});
|
|
6275
|
-
const
|
|
6361
|
+
const groupId = res.animation_group_id ?? shared?.animationGroupId;
|
|
6362
|
+
const job = { characterId: parentId, name, direction: animation.direction, jobIds: res.background_job_ids, ...groupId ? { groupId } : {} };
|
|
6276
6363
|
return {
|
|
6277
6364
|
jobId: encodeAnimationJob(job),
|
|
6278
|
-
metadata: { character: { kind: "animation", characterId: parentId, animationName: name, direction: animation.direction, mode: animation.mode, backgroundJobIds: res.background_job_ids } }
|
|
6365
|
+
metadata: { character: { kind: "animation", characterId: parentId, animationName: name, direction: animation.direction, mode: animation.mode, backgroundJobIds: res.background_job_ids, ...groupId ? { animationGroupId: groupId } : {} } }
|
|
6279
6366
|
};
|
|
6280
6367
|
}
|
|
6281
6368
|
/**
|
|
@@ -6490,13 +6577,13 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6490
6577
|
const animationId = typeof done.animation_id === "string" ? done.animation_id : null;
|
|
6491
6578
|
if (Array.isArray(frames) && frames.length && frames.every((f) => typeof f === "string")) {
|
|
6492
6579
|
const character2 = await this.client.getCharacter(job.characterId);
|
|
6493
|
-
const group = findAnimation(character2, job.name, job.direction, animationId);
|
|
6494
|
-
return review(frames, animationId, group?.groupId ?? null, billedFromUsage(status.usage));
|
|
6580
|
+
const group = findAnimation(character2, job.name, job.direction, animationId, job.groupId);
|
|
6581
|
+
return review(frames, animationId, group?.groupId ?? job.groupId ?? null, billedFromUsage(status.usage));
|
|
6495
6582
|
}
|
|
6496
6583
|
}
|
|
6497
6584
|
const recorded = context?.metadata?.character?.animationId ?? null;
|
|
6498
6585
|
const character = await this.client.getCharacter(job.characterId);
|
|
6499
|
-
const found = findAnimation(character, job.name, job.direction, recorded);
|
|
6586
|
+
const found = findAnimation(character, job.name, job.direction, recorded, job.groupId);
|
|
6500
6587
|
if (found) return review(found.frames, found.animationId, found.groupId, null);
|
|
6501
6588
|
return cleanedUp ? { status: "failed", error: `animation job is gone upstream and no "${job.name}" ${job.direction} animation exists on the character` } : { status: "processing" };
|
|
6502
6589
|
}
|
|
@@ -7930,10 +8017,11 @@ function rotationSources(character) {
|
|
|
7930
8017
|
}
|
|
7931
8018
|
return sources;
|
|
7932
8019
|
}
|
|
7933
|
-
function findAnimation(character, name, direction, animationId) {
|
|
8020
|
+
function findAnimation(character, name, direction, animationId, groupId) {
|
|
7934
8021
|
for (const group of character.animations) {
|
|
7935
8022
|
const match = group.directions.find((d) => d.direction === direction && d.frames.length > 0);
|
|
7936
8023
|
if (!match) continue;
|
|
8024
|
+
if (groupId && group.animation_group_id !== groupId) continue;
|
|
7937
8025
|
const byName = group.display_name === name || group.animation_type === name;
|
|
7938
8026
|
const byId = animationId !== null && match.frames.some((url) => url.includes(`/animations/${animationId}/`));
|
|
7939
8027
|
if (!byName && !byId) continue;
|
|
@@ -10167,8 +10255,16 @@ function expandLoopDirections(raw) {
|
|
|
10167
10255
|
continue;
|
|
10168
10256
|
}
|
|
10169
10257
|
for (const expandedId of ids) claimed.add(expandedId);
|
|
10170
|
-
|
|
10171
|
-
|
|
10258
|
+
const sentence = animation.mode === "skeleton-v3" && !animation.template;
|
|
10259
|
+
for (const [index, loop] of loops.entries()) {
|
|
10260
|
+
expanded[loop.id] = {
|
|
10261
|
+
...asset,
|
|
10262
|
+
animation: {
|
|
10263
|
+
...animation,
|
|
10264
|
+
direction: loop.direction,
|
|
10265
|
+
...sentence && index > 0 ? { motionFrom: loops[0].id } : {}
|
|
10266
|
+
}
|
|
10267
|
+
};
|
|
10172
10268
|
}
|
|
10173
10269
|
for (const mirror of mirrors) {
|
|
10174
10270
|
const inherited = {};
|
|
@@ -11103,7 +11199,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11103
11199
|
`assets.${assetId}: mirroring ${asset.mirror} gives another ${facing}-facing loop; mirror a loop facing east, west, or a diagonal`
|
|
11104
11200
|
);
|
|
11105
11201
|
}
|
|
11106
|
-
|
|
11202
|
+
const { motionLeader: _motionLeader, ...flipped } = sourceSpec.character;
|
|
11203
|
+
resolved.character = { ...flipped, animation: { ...sourceSpec.character.animation, direction: mirrored } };
|
|
11107
11204
|
} else if (sourceSpec.character) {
|
|
11108
11205
|
resolved.character = { ...sourceSpec.character };
|
|
11109
11206
|
}
|
|
@@ -11160,6 +11257,26 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11160
11257
|
directions: resolved.character.kind === "base" ? resolved.character.directions : parentSpec.character?.directions ?? resolved.character.directions
|
|
11161
11258
|
};
|
|
11162
11259
|
}
|
|
11260
|
+
if (resolved.character?.animation && asset.animation?.motionFrom) {
|
|
11261
|
+
const leaderId = asset.animation.motionFrom;
|
|
11262
|
+
const where = `assets.${assetId}.animation.motionFrom`;
|
|
11263
|
+
if (leaderId === assetId) throw new Error(`${where}: an asset cannot share its own motion`);
|
|
11264
|
+
const leader = await finalize(leaderId);
|
|
11265
|
+
const led = leader.character?.animation;
|
|
11266
|
+
if (leader.character?.kind !== "animation" || leader.mirror || !led || led.mode !== "skeleton-v3" || led.template) {
|
|
11267
|
+
throw new Error(`${where}: ${leaderId} is not a skeleton-v3 loop from a sentence`);
|
|
11268
|
+
}
|
|
11269
|
+
if (leader.character.parentAssetId !== resolved.character.parentAssetId) {
|
|
11270
|
+
throw new Error(`${where}: ${leaderId} animates ${leader.character.parentAssetId}, not ${resolved.character.parentAssetId}`);
|
|
11271
|
+
}
|
|
11272
|
+
if (leader.character.motionLeader) {
|
|
11273
|
+
throw new Error(`${where}: ${leaderId} itself shares ${leader.character.motionLeader.assetId}'s motion; point at the loop that leads`);
|
|
11274
|
+
}
|
|
11275
|
+
if (led.direction === resolved.character.animation.direction) {
|
|
11276
|
+
throw new Error(`${where}: ${leaderId} is drawn in the same direction; sharing a motion needs another direction`);
|
|
11277
|
+
}
|
|
11278
|
+
resolved.character = { ...resolved.character, motionLeader: { assetId: leaderId, spec: leader } };
|
|
11279
|
+
}
|
|
11163
11280
|
if (resolved.objectPro && characterParent) {
|
|
11164
11281
|
const parentSpec = await finalize(characterParent);
|
|
11165
11282
|
if (!parentSpec.objectPro) {
|
|
@@ -13154,7 +13271,10 @@ var import_node_path14 = __toESM(require("path"), 1);
|
|
|
13154
13271
|
async function inspectRevisionReadiness(spec, lock) {
|
|
13155
13272
|
if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
|
|
13156
13273
|
if (spec.character?.kind === "outfit") return inspectOutfit(spec, lock, /* @__PURE__ */ new Set());
|
|
13157
|
-
if (spec.character?.parentSpec)
|
|
13274
|
+
if (spec.character?.parentSpec) {
|
|
13275
|
+
const parent = await inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
|
|
13276
|
+
return parent.ready && spec.character.motionLeader ? inspectMotionLeader(spec, lock, /* @__PURE__ */ new Set()) : parent;
|
|
13277
|
+
}
|
|
13158
13278
|
if (spec.character?.styleAnchor) return inspectStyleAnchor(spec, lock, /* @__PURE__ */ new Set());
|
|
13159
13279
|
if (spec.objectPro?.parentSpec) return inspectObjectProParent(spec, lock, /* @__PURE__ */ new Set());
|
|
13160
13280
|
if (spec.scene) return inspectScene(spec);
|
|
@@ -13189,6 +13309,17 @@ async function inspectTerrainConnect(spec, lock, seen) {
|
|
|
13189
13309
|
}
|
|
13190
13310
|
return { ready: true, reason: "connected sets are current" };
|
|
13191
13311
|
}
|
|
13312
|
+
function recordedAnimationGroup(entry) {
|
|
13313
|
+
const character = entry?.providerMetadata?.[entry.provider]?.character;
|
|
13314
|
+
const text = (value) => typeof value === "string" && value ? value : null;
|
|
13315
|
+
return { animationGroupId: text(character?.animationGroupId), animationName: text(character?.animationName) };
|
|
13316
|
+
}
|
|
13317
|
+
async function inspectMotionLeader(spec, lock, seen) {
|
|
13318
|
+
const leader = spec.character.motionLeader;
|
|
13319
|
+
const label = `motion leader ${spec.styleId}/${leader.assetId}`;
|
|
13320
|
+
const dependency = await inspectParent(leader.spec, lock, seen);
|
|
13321
|
+
return dependency.ready ? { ready: true, reason: `${label} is current` } : { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
|
|
13322
|
+
}
|
|
13192
13323
|
async function inspectScene(spec) {
|
|
13193
13324
|
const scene = spec.scene;
|
|
13194
13325
|
const files = [{ label: "scene image", file: scene.file, sha256: scene.sha256 }];
|
|
@@ -13347,6 +13478,10 @@ async function inspectParent(spec, lock, seen) {
|
|
|
13347
13478
|
const inputs = await inspectCharacterParent(spec, lock, nextSeen);
|
|
13348
13479
|
if (!inputs.ready) return inputs;
|
|
13349
13480
|
}
|
|
13481
|
+
if (spec.character?.motionLeader) {
|
|
13482
|
+
const inputs = await inspectMotionLeader(spec, lock, nextSeen);
|
|
13483
|
+
if (!inputs.ready) return inputs;
|
|
13484
|
+
}
|
|
13350
13485
|
if (spec.character?.styleAnchor) {
|
|
13351
13486
|
const inputs = await inspectStyleAnchor(spec, lock, nextSeen);
|
|
13352
13487
|
if (!inputs.ready) return inputs;
|
|
@@ -13403,6 +13538,7 @@ function dependencies(spec) {
|
|
|
13403
13538
|
spec.mirror?.sourceSpec,
|
|
13404
13539
|
spec.revision?.sourceSpec,
|
|
13405
13540
|
spec.character?.parentSpec,
|
|
13541
|
+
spec.character?.motionLeader?.spec,
|
|
13406
13542
|
spec.character?.styleAnchor?.spec,
|
|
13407
13543
|
spec.objectPro?.parentSpec,
|
|
13408
13544
|
spec.terrainConnect?.lower?.spec,
|
|
@@ -13419,6 +13555,12 @@ function changedTerrainConnection(spec, entry, lock) {
|
|
|
13419
13555
|
}
|
|
13420
13556
|
return null;
|
|
13421
13557
|
}
|
|
13558
|
+
function changedMotionLeader(spec, entry, lock) {
|
|
13559
|
+
const leader = spec.character?.motionLeader;
|
|
13560
|
+
if (!leader) return null;
|
|
13561
|
+
const current = recordedAnimationGroup(lock.entries[lockKey(leader.spec.styleId, leader.spec.assetId)]).animationGroupId;
|
|
13562
|
+
return (entry.motionShare?.animationGroupId ?? null) === current ? null : leader.assetId;
|
|
13563
|
+
}
|
|
13422
13564
|
function changedTerrainSettings(spec, entry) {
|
|
13423
13565
|
const recorded = entry.terrainSettings;
|
|
13424
13566
|
const current = terrainSettings(spec);
|
|
@@ -13460,6 +13602,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
13460
13602
|
let state;
|
|
13461
13603
|
let reason;
|
|
13462
13604
|
const changedConnection = entry ? changedTerrainConnection(spec, entry, lock) : null;
|
|
13605
|
+
const changedMotion = entry ? changedMotionLeader(spec, entry, lock) : null;
|
|
13463
13606
|
const changedSettings = entry ? changedTerrainSettings(spec, entry) : null;
|
|
13464
13607
|
const revision = await inspectRevisionReadiness(spec, lock);
|
|
13465
13608
|
if (revision && !revision.ready) {
|
|
@@ -13499,6 +13642,9 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
13499
13642
|
} else if (changedConnection) {
|
|
13500
13643
|
state = "stale";
|
|
13501
13644
|
reason = `connected set ${changedConnection} was regenerated; draw this set from its new terrain`;
|
|
13645
|
+
} else if (changedMotion) {
|
|
13646
|
+
state = "stale";
|
|
13647
|
+
reason = `shares motion with ${changedMotion}, which was regenerated; draw this direction into its new motion`;
|
|
13502
13648
|
} else if (changedSettings) {
|
|
13503
13649
|
state = "stale";
|
|
13504
13650
|
reason = changedSettings;
|
|
@@ -13562,6 +13708,14 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
13562
13708
|
}
|
|
13563
13709
|
}
|
|
13564
13710
|
}
|
|
13711
|
+
for (const item of items) {
|
|
13712
|
+
const leader = item.spec.character?.motionLeader;
|
|
13713
|
+
if (!leader || !isActionable(item)) continue;
|
|
13714
|
+
const leaderEntry = lock.entries[lockKey(leader.spec.styleId, leader.spec.assetId)];
|
|
13715
|
+
if (leaderEntry?.status === "downloaded" && !recordedAnimationGroup(leaderEntry).animationGroupId) {
|
|
13716
|
+
item.reason += `; ${leader.assetId} has no animation group recorded, so this is drawn on its own and its motion will differ`;
|
|
13717
|
+
}
|
|
13718
|
+
}
|
|
13565
13719
|
const actionable = items.filter(isActionable);
|
|
13566
13720
|
const groupsByProvider = /* @__PURE__ */ new Map();
|
|
13567
13721
|
for (const item of actionable) {
|
|
@@ -14971,6 +15125,7 @@ function retireGeneration(entry, retiredAt) {
|
|
|
14971
15125
|
provider: entry.provider,
|
|
14972
15126
|
postprocess: entry.postprocess,
|
|
14973
15127
|
...entry.terrainConnect ? { terrainConnect: entry.terrainConnect } : {},
|
|
15128
|
+
...entry.motionShare ? { motionShare: entry.motionShare } : {},
|
|
14974
15129
|
...entry.terrainSettings ? { terrainSettings: entry.terrainSettings } : {},
|
|
14975
15130
|
retiredAt
|
|
14976
15131
|
};
|
|
@@ -15066,6 +15221,8 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
15066
15221
|
// A connected set comes back with the connection it was drawn from, so
|
|
15067
15222
|
// the plan compares the restored generation, not the one it replaced.
|
|
15068
15223
|
terrainConnect: generation.terrainConnect,
|
|
15224
|
+
// Likewise a loop that shared another direction's motion.
|
|
15225
|
+
motionShare: generation.motionShare,
|
|
15069
15226
|
// Likewise its settings; a generation from before they were recorded
|
|
15070
15227
|
// comes back with none and is taken as current.
|
|
15071
15228
|
terrainSettings: generation.terrainSettings,
|
|
@@ -16105,6 +16262,12 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
16105
16262
|
};
|
|
16106
16263
|
terrainConnect[side] = { sourceAssetId: link.assetId, sourceObjectId: objectId };
|
|
16107
16264
|
}
|
|
16265
|
+
const motionLeader = spec.character?.motionLeader;
|
|
16266
|
+
const leaderGroup = motionLeader ? recordedAnimationGroup(lock.entries[lockKey(motionLeader.spec.styleId, motionLeader.spec.assetId)]) : void 0;
|
|
16267
|
+
const motionGroup = leaderGroup?.animationGroupId && leaderGroup.animationName ? { animationGroupId: leaderGroup.animationGroupId, animationName: leaderGroup.animationName } : void 0;
|
|
16268
|
+
if (motionLeader && !motionGroup) {
|
|
16269
|
+
log(` ${key}: ${motionLeader.assetId} has no animation group recorded; drawing this direction on its own`);
|
|
16270
|
+
}
|
|
16108
16271
|
upsert(lock, key, {
|
|
16109
16272
|
styleId: spec.styleId,
|
|
16110
16273
|
assetId: spec.assetId,
|
|
@@ -16146,6 +16309,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
16146
16309
|
referenceSha256: spec.character.outfit.reference.sha256
|
|
16147
16310
|
} : null,
|
|
16148
16311
|
terrainConnect: spec.terrainConnect ? terrainConnect : void 0,
|
|
16312
|
+
motionShare: motionLeader ? { sourceAssetId: motionLeader.assetId, animationGroupId: motionGroup?.animationGroupId ?? null } : void 0,
|
|
16149
16313
|
// The terrain knobs specHash leaves out, so the plan can tell a
|
|
16150
16314
|
// changed one; undefined (no key) for every other generator.
|
|
16151
16315
|
terrainSettings: terrainSettings(spec),
|
|
@@ -16196,6 +16360,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
16196
16360
|
...anchorEntry?.objectId ? { styleObjectId: anchorEntry.objectId } : {},
|
|
16197
16361
|
...parentOutputs ? { parentOutputs } : {},
|
|
16198
16362
|
...spec.terrainConnect ? { terrainLinks } : {},
|
|
16363
|
+
...motionGroup ? { motionGroup } : {},
|
|
16199
16364
|
...replacedMetadata ? { replacedMetadata } : {},
|
|
16200
16365
|
...lastGenerationMetadata ? { lastGenerationMetadata } : {},
|
|
16201
16366
|
...previousMetadata ? { previousMetadata } : {},
|
|
@@ -18448,7 +18613,8 @@ var SkeletonAnimationRequestSchema = import_zod12.z.object({
|
|
|
18448
18613
|
/** The motion, sent as PixelLab's `action`. */
|
|
18449
18614
|
prompt: import_zod12.z.string().min(1),
|
|
18450
18615
|
direction: CharacterDirectionSchema,
|
|
18451
|
-
|
|
18616
|
+
/** What the character looks like; PixelLab refuses the request without it. */
|
|
18617
|
+
description: import_zod12.z.string().min(1),
|
|
18452
18618
|
skeletonTemplate: import_zod12.z.string().min(1).optional(),
|
|
18453
18619
|
/** Manifest-relative JSON path inside the project. */
|
|
18454
18620
|
keypointsFile: import_zod12.z.string().min(1),
|
|
@@ -18498,7 +18664,7 @@ async function createSkeletonAnimation(manifestPath, request) {
|
|
|
18498
18664
|
from: request.from,
|
|
18499
18665
|
keypointsFile: relative.split(import_node_path33.default.sep).join("/"),
|
|
18500
18666
|
direction: request.direction,
|
|
18501
|
-
|
|
18667
|
+
description: request.description,
|
|
18502
18668
|
...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
|
|
18503
18669
|
}
|
|
18504
18670
|
}
|
|
@@ -21387,6 +21553,7 @@ async function workspaceStatus(ws, dir) {
|
|
|
21387
21553
|
readVfxConversation,
|
|
21388
21554
|
readVfxRecipe,
|
|
21389
21555
|
recipeDigest,
|
|
21556
|
+
recordedAnimationGroup,
|
|
21390
21557
|
referencedHashes,
|
|
21391
21558
|
refineAsset,
|
|
21392
21559
|
refineFrameSet,
|