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.js
CHANGED
|
@@ -310,16 +310,30 @@ var CharacterAnimationSchema = z.object({
|
|
|
310
310
|
* `src/loop-directions.ts`.
|
|
311
311
|
*/
|
|
312
312
|
directions: z.array(CharacterDirectionSchema).min(1).optional(),
|
|
313
|
-
/**
|
|
314
|
-
|
|
313
|
+
/**
|
|
314
|
+
* A sentence loop (`mode: "skeleton-v3"`, no `template`) of another
|
|
315
|
+
* direction whose motion this one shares: the asset id of the loop that
|
|
316
|
+
* leads. This loop is then submitted after it, into its animation group,
|
|
317
|
+
* so PixelLab draws it from the same 3D motion. Written for you when
|
|
318
|
+
* `directions` expands (the first direction leads); set by hand only when
|
|
319
|
+
* declaring the loops explicitly.
|
|
320
|
+
*/
|
|
321
|
+
motionFrom: z.string().min(1).optional(),
|
|
322
|
+
/**
|
|
323
|
+
* Frames to generate without a template. A v3 loop takes an even 4 to 16;
|
|
324
|
+
* a sentence loop (`mode: "skeleton-v3"` with no `template`) takes 3 to 15.
|
|
325
|
+
*/
|
|
326
|
+
frames: z.number().int().min(3).max(16).optional(),
|
|
315
327
|
/** Playback rate recorded with the frames; PixelLab does not store one. */
|
|
316
328
|
fps: z.number().int().min(1).max(60).default(8),
|
|
317
329
|
/** v3 only: keep the resting pose as frame 0, so `frames` generated frames land as `frames + 1` files. */
|
|
318
330
|
keepFirstFrame: z.boolean().default(true),
|
|
319
331
|
/**
|
|
320
332
|
* `template` when a template is named, otherwise `v3`; `skeleton-v3`
|
|
321
|
-
* poses a template with PixelLab's skeleton model instead
|
|
322
|
-
*
|
|
333
|
+
* poses a template with PixelLab's skeleton model instead, or, with no
|
|
334
|
+
* template, animates the asset's prompt as a sentence written into one 3D
|
|
335
|
+
* motion (a v3 base; 3-15 `frames`); `pro` for the sequential
|
|
336
|
+
* high-quality engine.
|
|
323
337
|
*/
|
|
324
338
|
mode: CharacterAnimationModeSchema.optional(),
|
|
325
339
|
/**
|
|
@@ -349,15 +363,23 @@ var CharacterAnimationSchema = z.object({
|
|
|
349
363
|
path: ["directions"]
|
|
350
364
|
});
|
|
351
365
|
}
|
|
352
|
-
|
|
353
|
-
|
|
366
|
+
const sentence = animation.mode === "skeleton-v3" && !animation.template;
|
|
367
|
+
if (animation.frames !== void 0 && !sentence) {
|
|
368
|
+
if (animation.frames % 2 !== 0) {
|
|
369
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "frames must be even", path: ["frames"] });
|
|
370
|
+
} else if (animation.frames < 4) {
|
|
371
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "frames must be 4 to 16", path: ["frames"] });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (animation.frames !== void 0 && sentence && animation.frames > 15) {
|
|
375
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "a skeleton-v3 loop from a sentence takes 3 to 15 frames", path: ["frames"] });
|
|
354
376
|
}
|
|
355
377
|
const mode2 = animation.mode ?? (animation.template ? "template" : "v3");
|
|
356
378
|
for (const key of ["startFrame", "endFrame", "enhancePrompt"]) {
|
|
357
379
|
if (animation[key] !== void 0 && mode2 !== "v3") {
|
|
358
380
|
context.addIssue({
|
|
359
381
|
code: z.ZodIssueCode.custom,
|
|
360
|
-
message: `${key} is for v3 loops; a ${mode2} loop ${mode2 === "template" || mode2 === "skeleton-v3" ?
|
|
382
|
+
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"}`,
|
|
361
383
|
path: [key]
|
|
362
384
|
});
|
|
363
385
|
}
|
|
@@ -381,8 +403,14 @@ var CharacterAnimationSchema = z.object({
|
|
|
381
403
|
path: ["frames"]
|
|
382
404
|
});
|
|
383
405
|
}
|
|
384
|
-
if (!animation.template &&
|
|
385
|
-
context.addIssue({ code: z.ZodIssueCode.custom, message:
|
|
406
|
+
if (!animation.template && animation.mode === "template") {
|
|
407
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "template mode needs a template", path: ["template"] });
|
|
408
|
+
}
|
|
409
|
+
if (animation.motionFrom !== void 0 && !sentence) {
|
|
410
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "motionFrom applies to a skeleton-v3 loop from a sentence (no template)", path: ["motionFrom"] });
|
|
411
|
+
}
|
|
412
|
+
if (sentence && animation.subject !== void 0) {
|
|
413
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "subject does not apply to a skeleton-v3 loop from a sentence; describe the motion in the prompt", path: ["subject"] });
|
|
386
414
|
}
|
|
387
415
|
});
|
|
388
416
|
var RevisionDitheringSchema = z.enum(["none", "2x2", "4x4", "8x8"]);
|
|
@@ -1313,6 +1341,7 @@ var LockTerrainConnectSchema = z.record(
|
|
|
1313
1341
|
TerrainSideSchema,
|
|
1314
1342
|
z.object({ sourceAssetId: z.string().min(1), sourceObjectId: z.string().min(1) }).strict()
|
|
1315
1343
|
);
|
|
1344
|
+
var LockMotionShareSchema = z.object({ sourceAssetId: z.string().min(1), animationGroupId: z.string().min(1).nullable() }).strict();
|
|
1316
1345
|
var LockTerrainSettingsSchema = z.object({
|
|
1317
1346
|
terrainMode: z.string().optional(),
|
|
1318
1347
|
terrainShapeStyle: z.string().optional(),
|
|
@@ -1349,6 +1378,8 @@ var LockHistoryEntrySchema = z.object({
|
|
|
1349
1378
|
postprocess: PostprocessSchema.optional(),
|
|
1350
1379
|
/** A connected terrain set's other sets; see LockEntrySchema.terrainConnect. */
|
|
1351
1380
|
terrainConnect: LockTerrainConnectSchema.optional(),
|
|
1381
|
+
/** A sentence loop that shares another direction's motion; see LockEntrySchema.motionShare. */
|
|
1382
|
+
motionShare: LockMotionShareSchema.optional(),
|
|
1352
1383
|
/** A terrain set's settings outside specHash; see LockEntrySchema.terrainSettings. */
|
|
1353
1384
|
terrainSettings: LockTerrainSettingsSchema.optional(),
|
|
1354
1385
|
/** When the replacement was submitted. */
|
|
@@ -1425,6 +1456,16 @@ var LockEntrySchema = z.object({
|
|
|
1425
1456
|
* Absent (not null) otherwise, so other entries do not grow the key.
|
|
1426
1457
|
*/
|
|
1427
1458
|
terrainConnect: LockTerrainConnectSchema.optional(),
|
|
1459
|
+
/**
|
|
1460
|
+
* For a sentence loop of a later direction: the loop whose motion it
|
|
1461
|
+
* shares and the animation group of that loop's generation this was
|
|
1462
|
+
* drawn into when submitted. Tracked the way `terrainConnect` is, not
|
|
1463
|
+
* through `specHash`: the group lives in the lockfile. A regenerated leader
|
|
1464
|
+
* has a new group, so the plan marks this stale. `animationGroupId` is
|
|
1465
|
+
* null when the leader had no group recorded and this was drawn on its own.
|
|
1466
|
+
* Absent otherwise.
|
|
1467
|
+
*/
|
|
1468
|
+
motionShare: LockMotionShareSchema.optional(),
|
|
1428
1469
|
/**
|
|
1429
1470
|
* For a terrain set: the settings it was submitted with that `specHash`
|
|
1430
1471
|
* leaves out (see LockTerrainSettingsSchema). The plan marks the entry
|
|
@@ -1890,6 +1931,8 @@ var CharacterSubmitSchema = z3.object({
|
|
|
1890
1931
|
var AnimateSubmitSchema = z3.object({
|
|
1891
1932
|
background_job_ids: z3.array(z3.string()),
|
|
1892
1933
|
directions: z3.array(z3.string()),
|
|
1934
|
+
/** skeleton-v3 only: the group the directions were drawn into. */
|
|
1935
|
+
animation_group_id: z3.string().nullable().optional(),
|
|
1893
1936
|
status: z3.string().default("processing"),
|
|
1894
1937
|
usage: UsageSchema
|
|
1895
1938
|
}).passthrough();
|
|
@@ -2610,7 +2653,13 @@ var PixelLabClient = class {
|
|
|
2610
2653
|
* Every mode but skeleton-v3 goes to `/animate-character`, the path whose
|
|
2611
2654
|
* shapes were measured live. skeleton-v3 is documented only on
|
|
2612
2655
|
* `/characters/animations`, which takes the same request and answers with
|
|
2613
|
-
* the same job list (plus an `animation_group_id`).
|
|
2656
|
+
* the same job list (plus an `animation_group_id`). With no `template` it
|
|
2657
|
+
* animates `actionDescription` as a sentence, 3 to 15 `frameCount` frames
|
|
2658
|
+
* written into one 3D motion; measured on a v3 character (8 frames, 64px:
|
|
2659
|
+
* 3 generations, frames back at 96x96). `animationGroupId` (skeleton-v3
|
|
2660
|
+
* only) adds the directions to an existing animation of the character,
|
|
2661
|
+
* drawn from its motion; the group's own name must be sent again as
|
|
2662
|
+
* `animationName`, or the group appears renamed.
|
|
2614
2663
|
*/
|
|
2615
2664
|
async animateCharacter(args) {
|
|
2616
2665
|
const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
|
|
@@ -2622,10 +2671,12 @@ var PixelLabClient = class {
|
|
|
2622
2671
|
animation_name: args.animationName,
|
|
2623
2672
|
mode: args.mode,
|
|
2624
2673
|
directions: args.directions,
|
|
2674
|
+
...args.mode === "skeleton-v3" && args.animationGroupId ? { animation_group_id: args.animationGroupId } : {},
|
|
2625
2675
|
...args.template ? { template_animation_id: args.template } : {},
|
|
2626
2676
|
...args.actionDescription ? { action_description: args.actionDescription } : {},
|
|
2627
2677
|
...args.description ? { description: args.description } : {},
|
|
2628
|
-
|
|
2678
|
+
// A template fixes its own frame count; v3 and a skeleton-v3 sentence take one.
|
|
2679
|
+
...(args.mode === "v3" || args.mode === "skeleton-v3" && !args.template) && args.frameCount ? { frame_count: args.frameCount } : {},
|
|
2629
2680
|
...args.mode === "v3" && args.keepFirstFrame === false ? { keep_first_frame: false } : {},
|
|
2630
2681
|
...args.mode === "v3" && args.startFrame ? { custom_start_frame: encode(args.startFrame) } : {},
|
|
2631
2682
|
...args.mode === "v3" && args.endFrame ? { end_frame: encode(args.endFrame) } : {},
|
|
@@ -3993,6 +4044,10 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
3993
4044
|
template: spec.character.template,
|
|
3994
4045
|
parent: spec.character.parentAssetId,
|
|
3995
4046
|
parentSha256: spec.character.parentSha256 ?? null,
|
|
4047
|
+
// Which loop leads, not its group: the group is in the lockfile,
|
|
4048
|
+
// and the plan compares it there. Absent for every loop that
|
|
4049
|
+
// shares nothing, so no existing hash moves.
|
|
4050
|
+
motionFrom: spec.character.motionLeader?.assetId,
|
|
3996
4051
|
state: spec.character.state,
|
|
3997
4052
|
// Pose images enter as their hashes, never their paths.
|
|
3998
4053
|
animation: spec.character.animation ? {
|
|
@@ -5037,7 +5092,13 @@ function decodeAnimationJob(jobId) {
|
|
|
5037
5092
|
try {
|
|
5038
5093
|
const value = JSON.parse(Buffer.from(jobId.slice("character-animation:".length), "base64url").toString("utf8"));
|
|
5039
5094
|
if (typeof value?.characterId === "string" && typeof value.name === "string" && typeof value.direction === "string") {
|
|
5040
|
-
return {
|
|
5095
|
+
return {
|
|
5096
|
+
characterId: value.characterId,
|
|
5097
|
+
name: value.name,
|
|
5098
|
+
direction: value.direction,
|
|
5099
|
+
jobIds: Array.isArray(value.jobIds) ? value.jobIds : [],
|
|
5100
|
+
...typeof value.groupId === "string" && value.groupId ? { groupId: value.groupId } : {}
|
|
5101
|
+
};
|
|
5041
5102
|
}
|
|
5042
5103
|
} catch {
|
|
5043
5104
|
}
|
|
@@ -5277,6 +5338,16 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5277
5338
|
if (skeletonTemplate && !CHARACTER_TEMPLATES.includes(skeletonTemplate)) {
|
|
5278
5339
|
throw new Error(`PixelLab animate-skeleton template "${skeletonTemplate}" is not one of ${CHARACTER_TEMPLATES.join(", ")}`);
|
|
5279
5340
|
}
|
|
5341
|
+
if (!spec.revision.description?.trim()) {
|
|
5342
|
+
throw new Error(
|
|
5343
|
+
`${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`
|
|
5344
|
+
);
|
|
5345
|
+
}
|
|
5346
|
+
if (!spec.prompt.trim()) {
|
|
5347
|
+
throw new Error(
|
|
5348
|
+
`${spec.styleId}/${spec.assetId}: an animate-skeleton revision needs a prompt, a short motion label (for example "swinging the sword")`
|
|
5349
|
+
);
|
|
5350
|
+
}
|
|
5280
5351
|
}
|
|
5281
5352
|
if (spec.revision?.mode === "animate" || spec.revision?.mode === "animate-pixminimax") {
|
|
5282
5353
|
const { sourceWidth: width, sourceHeight: height, mode: mode2, frames, lastFrameWidth, lastFrameHeight } = spec.revision;
|
|
@@ -5680,12 +5751,18 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5680
5751
|
`${spec.styleId}/${spec.assetId} animates "${character.animation.direction}", but its character has ${character.directions} directions: ${allowed.join(", ")}`
|
|
5681
5752
|
);
|
|
5682
5753
|
}
|
|
5683
|
-
|
|
5684
|
-
|
|
5754
|
+
const sentence = character.animation.mode === "skeleton-v3" && !character.animation.template;
|
|
5755
|
+
if (character.animation.mode !== "template" && (character.animation.mode !== "skeleton-v3" || sentence) && !spec.prompt.trim()) {
|
|
5756
|
+
throw new Error(
|
|
5757
|
+
`${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")' : "")
|
|
5758
|
+
);
|
|
5759
|
+
}
|
|
5760
|
+
if (sentence && (character.animation.frames < 3 || character.animation.frames > 15)) {
|
|
5761
|
+
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`);
|
|
5685
5762
|
}
|
|
5686
5763
|
if (character.animation.mode === "skeleton-v3" && character.mode === "standard") {
|
|
5687
5764
|
throw new Error(
|
|
5688
|
-
`${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`
|
|
5765
|
+
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`
|
|
5689
5766
|
);
|
|
5690
5767
|
}
|
|
5691
5768
|
}
|
|
@@ -5859,11 +5936,18 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5859
5936
|
};
|
|
5860
5937
|
}
|
|
5861
5938
|
const animation = character.animation;
|
|
5862
|
-
const
|
|
5939
|
+
const ownName = characterAnimationName(spec);
|
|
5940
|
+
const shared = context?.motionGroup;
|
|
5941
|
+
const name = shared?.animationName ?? ownName;
|
|
5863
5942
|
const replaced = context?.replacedMetadata?.character ?? {};
|
|
5864
5943
|
const existing = await this.client.getCharacter(parentId);
|
|
5944
|
+
if (shared && !existing.animations.some((group) => group.animation_group_id === shared.animationGroupId && group.directions.some((d) => d.direction !== animation.direction))) {
|
|
5945
|
+
throw new Error(
|
|
5946
|
+
`${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`
|
|
5947
|
+
);
|
|
5948
|
+
}
|
|
5865
5949
|
for (const group of existing.animations) {
|
|
5866
|
-
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}/`))));
|
|
5950
|
+
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}/`))));
|
|
5867
5951
|
if (!ours || !group.animation_group_id) continue;
|
|
5868
5952
|
if (group.directions.some((d) => d.direction === animation.direction)) {
|
|
5869
5953
|
await this.client.deleteCharacterAnimations(parentId, { animationGroupId: group.animation_group_id }, animation.direction);
|
|
@@ -5878,6 +5962,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5878
5962
|
frameCount: animation.frames,
|
|
5879
5963
|
keepFirstFrame: animation.keepFirstFrame,
|
|
5880
5964
|
directions: [animation.direction],
|
|
5965
|
+
...shared ? { animationGroupId: shared.animationGroupId } : {},
|
|
5881
5966
|
seed: spec.seed,
|
|
5882
5967
|
textGuidanceScale: character.textGuidanceScale,
|
|
5883
5968
|
isometric: character.isometric,
|
|
@@ -5890,10 +5975,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5890
5975
|
enhancePrompt: animation.enhancePrompt,
|
|
5891
5976
|
paletteSwatchBase64: spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0
|
|
5892
5977
|
});
|
|
5893
|
-
const
|
|
5978
|
+
const groupId = res.animation_group_id ?? shared?.animationGroupId;
|
|
5979
|
+
const job = { characterId: parentId, name, direction: animation.direction, jobIds: res.background_job_ids, ...groupId ? { groupId } : {} };
|
|
5894
5980
|
return {
|
|
5895
5981
|
jobId: encodeAnimationJob(job),
|
|
5896
|
-
metadata: { character: { kind: "animation", characterId: parentId, animationName: name, direction: animation.direction, mode: animation.mode, backgroundJobIds: res.background_job_ids } }
|
|
5982
|
+
metadata: { character: { kind: "animation", characterId: parentId, animationName: name, direction: animation.direction, mode: animation.mode, backgroundJobIds: res.background_job_ids, ...groupId ? { animationGroupId: groupId } : {} } }
|
|
5897
5983
|
};
|
|
5898
5984
|
}
|
|
5899
5985
|
/**
|
|
@@ -6108,13 +6194,13 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6108
6194
|
const animationId = typeof done.animation_id === "string" ? done.animation_id : null;
|
|
6109
6195
|
if (Array.isArray(frames) && frames.length && frames.every((f) => typeof f === "string")) {
|
|
6110
6196
|
const character2 = await this.client.getCharacter(job.characterId);
|
|
6111
|
-
const group = findAnimation(character2, job.name, job.direction, animationId);
|
|
6112
|
-
return review(frames, animationId, group?.groupId ?? null, billedFromUsage(status.usage));
|
|
6197
|
+
const group = findAnimation(character2, job.name, job.direction, animationId, job.groupId);
|
|
6198
|
+
return review(frames, animationId, group?.groupId ?? job.groupId ?? null, billedFromUsage(status.usage));
|
|
6113
6199
|
}
|
|
6114
6200
|
}
|
|
6115
6201
|
const recorded = context?.metadata?.character?.animationId ?? null;
|
|
6116
6202
|
const character = await this.client.getCharacter(job.characterId);
|
|
6117
|
-
const found = findAnimation(character, job.name, job.direction, recorded);
|
|
6203
|
+
const found = findAnimation(character, job.name, job.direction, recorded, job.groupId);
|
|
6118
6204
|
if (found) return review(found.frames, found.animationId, found.groupId, null);
|
|
6119
6205
|
return cleanedUp ? { status: "failed", error: `animation job is gone upstream and no "${job.name}" ${job.direction} animation exists on the character` } : { status: "processing" };
|
|
6120
6206
|
}
|
|
@@ -7548,10 +7634,11 @@ function rotationSources(character) {
|
|
|
7548
7634
|
}
|
|
7549
7635
|
return sources;
|
|
7550
7636
|
}
|
|
7551
|
-
function findAnimation(character, name, direction, animationId) {
|
|
7637
|
+
function findAnimation(character, name, direction, animationId, groupId) {
|
|
7552
7638
|
for (const group of character.animations) {
|
|
7553
7639
|
const match = group.directions.find((d) => d.direction === direction && d.frames.length > 0);
|
|
7554
7640
|
if (!match) continue;
|
|
7641
|
+
if (groupId && group.animation_group_id !== groupId) continue;
|
|
7555
7642
|
const byName = group.display_name === name || group.animation_type === name;
|
|
7556
7643
|
const byId = animationId !== null && match.frames.some((url) => url.includes(`/animations/${animationId}/`));
|
|
7557
7644
|
if (!byName && !byId) continue;
|
|
@@ -9785,8 +9872,16 @@ function expandLoopDirections(raw) {
|
|
|
9785
9872
|
continue;
|
|
9786
9873
|
}
|
|
9787
9874
|
for (const expandedId of ids) claimed.add(expandedId);
|
|
9788
|
-
|
|
9789
|
-
|
|
9875
|
+
const sentence = animation.mode === "skeleton-v3" && !animation.template;
|
|
9876
|
+
for (const [index, loop] of loops.entries()) {
|
|
9877
|
+
expanded[loop.id] = {
|
|
9878
|
+
...asset,
|
|
9879
|
+
animation: {
|
|
9880
|
+
...animation,
|
|
9881
|
+
direction: loop.direction,
|
|
9882
|
+
...sentence && index > 0 ? { motionFrom: loops[0].id } : {}
|
|
9883
|
+
}
|
|
9884
|
+
};
|
|
9790
9885
|
}
|
|
9791
9886
|
for (const mirror of mirrors) {
|
|
9792
9887
|
const inherited = {};
|
|
@@ -10721,7 +10816,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
10721
10816
|
`assets.${assetId}: mirroring ${asset.mirror} gives another ${facing}-facing loop; mirror a loop facing east, west, or a diagonal`
|
|
10722
10817
|
);
|
|
10723
10818
|
}
|
|
10724
|
-
|
|
10819
|
+
const { motionLeader: _motionLeader, ...flipped } = sourceSpec.character;
|
|
10820
|
+
resolved.character = { ...flipped, animation: { ...sourceSpec.character.animation, direction: mirrored } };
|
|
10725
10821
|
} else if (sourceSpec.character) {
|
|
10726
10822
|
resolved.character = { ...sourceSpec.character };
|
|
10727
10823
|
}
|
|
@@ -10778,6 +10874,26 @@ async function resolveSpecs(loaded, filter) {
|
|
|
10778
10874
|
directions: resolved.character.kind === "base" ? resolved.character.directions : parentSpec.character?.directions ?? resolved.character.directions
|
|
10779
10875
|
};
|
|
10780
10876
|
}
|
|
10877
|
+
if (resolved.character?.animation && asset.animation?.motionFrom) {
|
|
10878
|
+
const leaderId = asset.animation.motionFrom;
|
|
10879
|
+
const where = `assets.${assetId}.animation.motionFrom`;
|
|
10880
|
+
if (leaderId === assetId) throw new Error(`${where}: an asset cannot share its own motion`);
|
|
10881
|
+
const leader = await finalize(leaderId);
|
|
10882
|
+
const led = leader.character?.animation;
|
|
10883
|
+
if (leader.character?.kind !== "animation" || leader.mirror || !led || led.mode !== "skeleton-v3" || led.template) {
|
|
10884
|
+
throw new Error(`${where}: ${leaderId} is not a skeleton-v3 loop from a sentence`);
|
|
10885
|
+
}
|
|
10886
|
+
if (leader.character.parentAssetId !== resolved.character.parentAssetId) {
|
|
10887
|
+
throw new Error(`${where}: ${leaderId} animates ${leader.character.parentAssetId}, not ${resolved.character.parentAssetId}`);
|
|
10888
|
+
}
|
|
10889
|
+
if (leader.character.motionLeader) {
|
|
10890
|
+
throw new Error(`${where}: ${leaderId} itself shares ${leader.character.motionLeader.assetId}'s motion; point at the loop that leads`);
|
|
10891
|
+
}
|
|
10892
|
+
if (led.direction === resolved.character.animation.direction) {
|
|
10893
|
+
throw new Error(`${where}: ${leaderId} is drawn in the same direction; sharing a motion needs another direction`);
|
|
10894
|
+
}
|
|
10895
|
+
resolved.character = { ...resolved.character, motionLeader: { assetId: leaderId, spec: leader } };
|
|
10896
|
+
}
|
|
10781
10897
|
if (resolved.objectPro && characterParent) {
|
|
10782
10898
|
const parentSpec = await finalize(characterParent);
|
|
10783
10899
|
if (!parentSpec.objectPro) {
|
|
@@ -12772,7 +12888,10 @@ import path14 from "path";
|
|
|
12772
12888
|
async function inspectRevisionReadiness(spec, lock) {
|
|
12773
12889
|
if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
|
|
12774
12890
|
if (spec.character?.kind === "outfit") return inspectOutfit(spec, lock, /* @__PURE__ */ new Set());
|
|
12775
|
-
if (spec.character?.parentSpec)
|
|
12891
|
+
if (spec.character?.parentSpec) {
|
|
12892
|
+
const parent = await inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
|
|
12893
|
+
return parent.ready && spec.character.motionLeader ? inspectMotionLeader(spec, lock, /* @__PURE__ */ new Set()) : parent;
|
|
12894
|
+
}
|
|
12776
12895
|
if (spec.character?.styleAnchor) return inspectStyleAnchor(spec, lock, /* @__PURE__ */ new Set());
|
|
12777
12896
|
if (spec.objectPro?.parentSpec) return inspectObjectProParent(spec, lock, /* @__PURE__ */ new Set());
|
|
12778
12897
|
if (spec.scene) return inspectScene(spec);
|
|
@@ -12807,6 +12926,17 @@ async function inspectTerrainConnect(spec, lock, seen) {
|
|
|
12807
12926
|
}
|
|
12808
12927
|
return { ready: true, reason: "connected sets are current" };
|
|
12809
12928
|
}
|
|
12929
|
+
function recordedAnimationGroup(entry) {
|
|
12930
|
+
const character = entry?.providerMetadata?.[entry.provider]?.character;
|
|
12931
|
+
const text = (value) => typeof value === "string" && value ? value : null;
|
|
12932
|
+
return { animationGroupId: text(character?.animationGroupId), animationName: text(character?.animationName) };
|
|
12933
|
+
}
|
|
12934
|
+
async function inspectMotionLeader(spec, lock, seen) {
|
|
12935
|
+
const leader = spec.character.motionLeader;
|
|
12936
|
+
const label = `motion leader ${spec.styleId}/${leader.assetId}`;
|
|
12937
|
+
const dependency = await inspectParent(leader.spec, lock, seen);
|
|
12938
|
+
return dependency.ready ? { ready: true, reason: `${label} is current` } : { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
|
|
12939
|
+
}
|
|
12810
12940
|
async function inspectScene(spec) {
|
|
12811
12941
|
const scene = spec.scene;
|
|
12812
12942
|
const files = [{ label: "scene image", file: scene.file, sha256: scene.sha256 }];
|
|
@@ -12965,6 +13095,10 @@ async function inspectParent(spec, lock, seen) {
|
|
|
12965
13095
|
const inputs = await inspectCharacterParent(spec, lock, nextSeen);
|
|
12966
13096
|
if (!inputs.ready) return inputs;
|
|
12967
13097
|
}
|
|
13098
|
+
if (spec.character?.motionLeader) {
|
|
13099
|
+
const inputs = await inspectMotionLeader(spec, lock, nextSeen);
|
|
13100
|
+
if (!inputs.ready) return inputs;
|
|
13101
|
+
}
|
|
12968
13102
|
if (spec.character?.styleAnchor) {
|
|
12969
13103
|
const inputs = await inspectStyleAnchor(spec, lock, nextSeen);
|
|
12970
13104
|
if (!inputs.ready) return inputs;
|
|
@@ -13021,6 +13155,7 @@ function dependencies(spec) {
|
|
|
13021
13155
|
spec.mirror?.sourceSpec,
|
|
13022
13156
|
spec.revision?.sourceSpec,
|
|
13023
13157
|
spec.character?.parentSpec,
|
|
13158
|
+
spec.character?.motionLeader?.spec,
|
|
13024
13159
|
spec.character?.styleAnchor?.spec,
|
|
13025
13160
|
spec.objectPro?.parentSpec,
|
|
13026
13161
|
spec.terrainConnect?.lower?.spec,
|
|
@@ -13037,6 +13172,12 @@ function changedTerrainConnection(spec, entry, lock) {
|
|
|
13037
13172
|
}
|
|
13038
13173
|
return null;
|
|
13039
13174
|
}
|
|
13175
|
+
function changedMotionLeader(spec, entry, lock) {
|
|
13176
|
+
const leader = spec.character?.motionLeader;
|
|
13177
|
+
if (!leader) return null;
|
|
13178
|
+
const current = recordedAnimationGroup(lock.entries[lockKey(leader.spec.styleId, leader.spec.assetId)]).animationGroupId;
|
|
13179
|
+
return (entry.motionShare?.animationGroupId ?? null) === current ? null : leader.assetId;
|
|
13180
|
+
}
|
|
13040
13181
|
function changedTerrainSettings(spec, entry) {
|
|
13041
13182
|
const recorded = entry.terrainSettings;
|
|
13042
13183
|
const current = terrainSettings(spec);
|
|
@@ -13078,6 +13219,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
13078
13219
|
let state;
|
|
13079
13220
|
let reason;
|
|
13080
13221
|
const changedConnection = entry ? changedTerrainConnection(spec, entry, lock) : null;
|
|
13222
|
+
const changedMotion = entry ? changedMotionLeader(spec, entry, lock) : null;
|
|
13081
13223
|
const changedSettings = entry ? changedTerrainSettings(spec, entry) : null;
|
|
13082
13224
|
const revision = await inspectRevisionReadiness(spec, lock);
|
|
13083
13225
|
if (revision && !revision.ready) {
|
|
@@ -13117,6 +13259,9 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
13117
13259
|
} else if (changedConnection) {
|
|
13118
13260
|
state = "stale";
|
|
13119
13261
|
reason = `connected set ${changedConnection} was regenerated; draw this set from its new terrain`;
|
|
13262
|
+
} else if (changedMotion) {
|
|
13263
|
+
state = "stale";
|
|
13264
|
+
reason = `shares motion with ${changedMotion}, which was regenerated; draw this direction into its new motion`;
|
|
13120
13265
|
} else if (changedSettings) {
|
|
13121
13266
|
state = "stale";
|
|
13122
13267
|
reason = changedSettings;
|
|
@@ -13180,6 +13325,14 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
13180
13325
|
}
|
|
13181
13326
|
}
|
|
13182
13327
|
}
|
|
13328
|
+
for (const item of items) {
|
|
13329
|
+
const leader = item.spec.character?.motionLeader;
|
|
13330
|
+
if (!leader || !isActionable(item)) continue;
|
|
13331
|
+
const leaderEntry = lock.entries[lockKey(leader.spec.styleId, leader.spec.assetId)];
|
|
13332
|
+
if (leaderEntry?.status === "downloaded" && !recordedAnimationGroup(leaderEntry).animationGroupId) {
|
|
13333
|
+
item.reason += `; ${leader.assetId} has no animation group recorded, so this is drawn on its own and its motion will differ`;
|
|
13334
|
+
}
|
|
13335
|
+
}
|
|
13183
13336
|
const actionable = items.filter(isActionable);
|
|
13184
13337
|
const groupsByProvider = /* @__PURE__ */ new Map();
|
|
13185
13338
|
for (const item of actionable) {
|
|
@@ -14589,6 +14742,7 @@ function retireGeneration(entry, retiredAt) {
|
|
|
14589
14742
|
provider: entry.provider,
|
|
14590
14743
|
postprocess: entry.postprocess,
|
|
14591
14744
|
...entry.terrainConnect ? { terrainConnect: entry.terrainConnect } : {},
|
|
14745
|
+
...entry.motionShare ? { motionShare: entry.motionShare } : {},
|
|
14592
14746
|
...entry.terrainSettings ? { terrainSettings: entry.terrainSettings } : {},
|
|
14593
14747
|
retiredAt
|
|
14594
14748
|
};
|
|
@@ -14684,6 +14838,8 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
14684
14838
|
// A connected set comes back with the connection it was drawn from, so
|
|
14685
14839
|
// the plan compares the restored generation, not the one it replaced.
|
|
14686
14840
|
terrainConnect: generation.terrainConnect,
|
|
14841
|
+
// Likewise a loop that shared another direction's motion.
|
|
14842
|
+
motionShare: generation.motionShare,
|
|
14687
14843
|
// Likewise its settings; a generation from before they were recorded
|
|
14688
14844
|
// comes back with none and is taken as current.
|
|
14689
14845
|
terrainSettings: generation.terrainSettings,
|
|
@@ -15723,6 +15879,12 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
15723
15879
|
};
|
|
15724
15880
|
terrainConnect[side] = { sourceAssetId: link.assetId, sourceObjectId: objectId };
|
|
15725
15881
|
}
|
|
15882
|
+
const motionLeader = spec.character?.motionLeader;
|
|
15883
|
+
const leaderGroup = motionLeader ? recordedAnimationGroup(lock.entries[lockKey(motionLeader.spec.styleId, motionLeader.spec.assetId)]) : void 0;
|
|
15884
|
+
const motionGroup = leaderGroup?.animationGroupId && leaderGroup.animationName ? { animationGroupId: leaderGroup.animationGroupId, animationName: leaderGroup.animationName } : void 0;
|
|
15885
|
+
if (motionLeader && !motionGroup) {
|
|
15886
|
+
log(` ${key}: ${motionLeader.assetId} has no animation group recorded; drawing this direction on its own`);
|
|
15887
|
+
}
|
|
15726
15888
|
upsert(lock, key, {
|
|
15727
15889
|
styleId: spec.styleId,
|
|
15728
15890
|
assetId: spec.assetId,
|
|
@@ -15764,6 +15926,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
15764
15926
|
referenceSha256: spec.character.outfit.reference.sha256
|
|
15765
15927
|
} : null,
|
|
15766
15928
|
terrainConnect: spec.terrainConnect ? terrainConnect : void 0,
|
|
15929
|
+
motionShare: motionLeader ? { sourceAssetId: motionLeader.assetId, animationGroupId: motionGroup?.animationGroupId ?? null } : void 0,
|
|
15767
15930
|
// The terrain knobs specHash leaves out, so the plan can tell a
|
|
15768
15931
|
// changed one; undefined (no key) for every other generator.
|
|
15769
15932
|
terrainSettings: terrainSettings(spec),
|
|
@@ -15814,6 +15977,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
15814
15977
|
...anchorEntry?.objectId ? { styleObjectId: anchorEntry.objectId } : {},
|
|
15815
15978
|
...parentOutputs ? { parentOutputs } : {},
|
|
15816
15979
|
...spec.terrainConnect ? { terrainLinks } : {},
|
|
15980
|
+
...motionGroup ? { motionGroup } : {},
|
|
15817
15981
|
...replacedMetadata ? { replacedMetadata } : {},
|
|
15818
15982
|
...lastGenerationMetadata ? { lastGenerationMetadata } : {},
|
|
15819
15983
|
...previousMetadata ? { previousMetadata } : {},
|
|
@@ -18066,7 +18230,8 @@ var SkeletonAnimationRequestSchema = z12.object({
|
|
|
18066
18230
|
/** The motion, sent as PixelLab's `action`. */
|
|
18067
18231
|
prompt: z12.string().min(1),
|
|
18068
18232
|
direction: CharacterDirectionSchema,
|
|
18069
|
-
|
|
18233
|
+
/** What the character looks like; PixelLab refuses the request without it. */
|
|
18234
|
+
description: z12.string().min(1),
|
|
18070
18235
|
skeletonTemplate: z12.string().min(1).optional(),
|
|
18071
18236
|
/** Manifest-relative JSON path inside the project. */
|
|
18072
18237
|
keypointsFile: z12.string().min(1),
|
|
@@ -18116,7 +18281,7 @@ async function createSkeletonAnimation(manifestPath, request) {
|
|
|
18116
18281
|
from: request.from,
|
|
18117
18282
|
keypointsFile: relative.split(path32.sep).join("/"),
|
|
18118
18283
|
direction: request.direction,
|
|
18119
|
-
|
|
18284
|
+
description: request.description,
|
|
18120
18285
|
...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
|
|
18121
18286
|
}
|
|
18122
18287
|
}
|
|
@@ -21004,6 +21169,7 @@ export {
|
|
|
21004
21169
|
readVfxConversation,
|
|
21005
21170
|
readVfxRecipe,
|
|
21006
21171
|
recipeDigest,
|
|
21172
|
+
recordedAnimationGroup,
|
|
21007
21173
|
referencedHashes,
|
|
21008
21174
|
refineAsset,
|
|
21009
21175
|
refineFrameSet,
|