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/cli.js
CHANGED
|
@@ -807,6 +807,10 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
807
807
|
template: spec.character.template,
|
|
808
808
|
parent: spec.character.parentAssetId,
|
|
809
809
|
parentSha256: spec.character.parentSha256 ?? null,
|
|
810
|
+
// Which loop leads, not its group: the group is in the lockfile,
|
|
811
|
+
// and the plan compares it there. Absent for every loop that
|
|
812
|
+
// shares nothing, so no existing hash moves.
|
|
813
|
+
motionFrom: spec.character.motionLeader?.assetId,
|
|
810
814
|
state: spec.character.state,
|
|
811
815
|
// Pose images enter as their hashes, never their paths.
|
|
812
816
|
animation: spec.character.animation ? {
|
|
@@ -1238,16 +1242,30 @@ var CharacterAnimationSchema = z.object({
|
|
|
1238
1242
|
* `src/loop-directions.ts`.
|
|
1239
1243
|
*/
|
|
1240
1244
|
directions: z.array(CharacterDirectionSchema).min(1).optional(),
|
|
1241
|
-
/**
|
|
1242
|
-
|
|
1245
|
+
/**
|
|
1246
|
+
* A sentence loop (`mode: "skeleton-v3"`, no `template`) of another
|
|
1247
|
+
* direction whose motion this one shares: the asset id of the loop that
|
|
1248
|
+
* leads. This loop is then submitted after it, into its animation group,
|
|
1249
|
+
* so PixelLab draws it from the same 3D motion. Written for you when
|
|
1250
|
+
* `directions` expands (the first direction leads); set by hand only when
|
|
1251
|
+
* declaring the loops explicitly.
|
|
1252
|
+
*/
|
|
1253
|
+
motionFrom: z.string().min(1).optional(),
|
|
1254
|
+
/**
|
|
1255
|
+
* Frames to generate without a template. A v3 loop takes an even 4 to 16;
|
|
1256
|
+
* a sentence loop (`mode: "skeleton-v3"` with no `template`) takes 3 to 15.
|
|
1257
|
+
*/
|
|
1258
|
+
frames: z.number().int().min(3).max(16).optional(),
|
|
1243
1259
|
/** Playback rate recorded with the frames; PixelLab does not store one. */
|
|
1244
1260
|
fps: z.number().int().min(1).max(60).default(8),
|
|
1245
1261
|
/** v3 only: keep the resting pose as frame 0, so `frames` generated frames land as `frames + 1` files. */
|
|
1246
1262
|
keepFirstFrame: z.boolean().default(true),
|
|
1247
1263
|
/**
|
|
1248
1264
|
* `template` when a template is named, otherwise `v3`; `skeleton-v3`
|
|
1249
|
-
* poses a template with PixelLab's skeleton model instead
|
|
1250
|
-
*
|
|
1265
|
+
* poses a template with PixelLab's skeleton model instead, or, with no
|
|
1266
|
+
* template, animates the asset's prompt as a sentence written into one 3D
|
|
1267
|
+
* motion (a v3 base; 3-15 `frames`); `pro` for the sequential
|
|
1268
|
+
* high-quality engine.
|
|
1251
1269
|
*/
|
|
1252
1270
|
mode: CharacterAnimationModeSchema.optional(),
|
|
1253
1271
|
/**
|
|
@@ -1277,15 +1295,23 @@ var CharacterAnimationSchema = z.object({
|
|
|
1277
1295
|
path: ["directions"]
|
|
1278
1296
|
});
|
|
1279
1297
|
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1298
|
+
const sentence = animation.mode === "skeleton-v3" && !animation.template;
|
|
1299
|
+
if (animation.frames !== void 0 && !sentence) {
|
|
1300
|
+
if (animation.frames % 2 !== 0) {
|
|
1301
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "frames must be even", path: ["frames"] });
|
|
1302
|
+
} else if (animation.frames < 4) {
|
|
1303
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "frames must be 4 to 16", path: ["frames"] });
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
if (animation.frames !== void 0 && sentence && animation.frames > 15) {
|
|
1307
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "a skeleton-v3 loop from a sentence takes 3 to 15 frames", path: ["frames"] });
|
|
1282
1308
|
}
|
|
1283
1309
|
const mode2 = animation.mode ?? (animation.template ? "template" : "v3");
|
|
1284
1310
|
for (const key of ["startFrame", "endFrame", "enhancePrompt"]) {
|
|
1285
1311
|
if (animation[key] !== void 0 && mode2 !== "v3") {
|
|
1286
1312
|
context.addIssue({
|
|
1287
1313
|
code: z.ZodIssueCode.custom,
|
|
1288
|
-
message: `${key} is for v3 loops; a ${mode2} loop ${mode2 === "template" || mode2 === "skeleton-v3" ?
|
|
1314
|
+
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"}`,
|
|
1289
1315
|
path: [key]
|
|
1290
1316
|
});
|
|
1291
1317
|
}
|
|
@@ -1309,8 +1335,14 @@ var CharacterAnimationSchema = z.object({
|
|
|
1309
1335
|
path: ["frames"]
|
|
1310
1336
|
});
|
|
1311
1337
|
}
|
|
1312
|
-
if (!animation.template &&
|
|
1313
|
-
context.addIssue({ code: z.ZodIssueCode.custom, message:
|
|
1338
|
+
if (!animation.template && animation.mode === "template") {
|
|
1339
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "template mode needs a template", path: ["template"] });
|
|
1340
|
+
}
|
|
1341
|
+
if (animation.motionFrom !== void 0 && !sentence) {
|
|
1342
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "motionFrom applies to a skeleton-v3 loop from a sentence (no template)", path: ["motionFrom"] });
|
|
1343
|
+
}
|
|
1344
|
+
if (sentence && animation.subject !== void 0) {
|
|
1345
|
+
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"] });
|
|
1314
1346
|
}
|
|
1315
1347
|
});
|
|
1316
1348
|
var RevisionDitheringSchema = z.enum(["none", "2x2", "4x4", "8x8"]);
|
|
@@ -2241,6 +2273,7 @@ var LockTerrainConnectSchema = z.record(
|
|
|
2241
2273
|
TerrainSideSchema,
|
|
2242
2274
|
z.object({ sourceAssetId: z.string().min(1), sourceObjectId: z.string().min(1) }).strict()
|
|
2243
2275
|
);
|
|
2276
|
+
var LockMotionShareSchema = z.object({ sourceAssetId: z.string().min(1), animationGroupId: z.string().min(1).nullable() }).strict();
|
|
2244
2277
|
var LockTerrainSettingsSchema = z.object({
|
|
2245
2278
|
terrainMode: z.string().optional(),
|
|
2246
2279
|
terrainShapeStyle: z.string().optional(),
|
|
@@ -2277,6 +2310,8 @@ var LockHistoryEntrySchema = z.object({
|
|
|
2277
2310
|
postprocess: PostprocessSchema.optional(),
|
|
2278
2311
|
/** A connected terrain set's other sets; see LockEntrySchema.terrainConnect. */
|
|
2279
2312
|
terrainConnect: LockTerrainConnectSchema.optional(),
|
|
2313
|
+
/** A sentence loop that shares another direction's motion; see LockEntrySchema.motionShare. */
|
|
2314
|
+
motionShare: LockMotionShareSchema.optional(),
|
|
2280
2315
|
/** A terrain set's settings outside specHash; see LockEntrySchema.terrainSettings. */
|
|
2281
2316
|
terrainSettings: LockTerrainSettingsSchema.optional(),
|
|
2282
2317
|
/** When the replacement was submitted. */
|
|
@@ -2353,6 +2388,16 @@ var LockEntrySchema = z.object({
|
|
|
2353
2388
|
* Absent (not null) otherwise, so other entries do not grow the key.
|
|
2354
2389
|
*/
|
|
2355
2390
|
terrainConnect: LockTerrainConnectSchema.optional(),
|
|
2391
|
+
/**
|
|
2392
|
+
* For a sentence loop of a later direction: the loop whose motion it
|
|
2393
|
+
* shares and the animation group of that loop's generation this was
|
|
2394
|
+
* drawn into when submitted. Tracked the way `terrainConnect` is, not
|
|
2395
|
+
* through `specHash`: the group lives in the lockfile. A regenerated leader
|
|
2396
|
+
* has a new group, so the plan marks this stale. `animationGroupId` is
|
|
2397
|
+
* null when the leader had no group recorded and this was drawn on its own.
|
|
2398
|
+
* Absent otherwise.
|
|
2399
|
+
*/
|
|
2400
|
+
motionShare: LockMotionShareSchema.optional(),
|
|
2356
2401
|
/**
|
|
2357
2402
|
* For a terrain set: the settings it was submitted with that `specHash`
|
|
2358
2403
|
* leaves out (see LockTerrainSettingsSchema). The plan marks the entry
|
|
@@ -4679,7 +4724,10 @@ import path8 from "path";
|
|
|
4679
4724
|
async function inspectRevisionReadiness(spec, lock) {
|
|
4680
4725
|
if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
|
|
4681
4726
|
if (spec.character?.kind === "outfit") return inspectOutfit(spec, lock, /* @__PURE__ */ new Set());
|
|
4682
|
-
if (spec.character?.parentSpec)
|
|
4727
|
+
if (spec.character?.parentSpec) {
|
|
4728
|
+
const parent = await inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
|
|
4729
|
+
return parent.ready && spec.character.motionLeader ? inspectMotionLeader(spec, lock, /* @__PURE__ */ new Set()) : parent;
|
|
4730
|
+
}
|
|
4683
4731
|
if (spec.character?.styleAnchor) return inspectStyleAnchor(spec, lock, /* @__PURE__ */ new Set());
|
|
4684
4732
|
if (spec.objectPro?.parentSpec) return inspectObjectProParent(spec, lock, /* @__PURE__ */ new Set());
|
|
4685
4733
|
if (spec.scene) return inspectScene(spec);
|
|
@@ -4714,6 +4762,17 @@ async function inspectTerrainConnect(spec, lock, seen) {
|
|
|
4714
4762
|
}
|
|
4715
4763
|
return { ready: true, reason: "connected sets are current" };
|
|
4716
4764
|
}
|
|
4765
|
+
function recordedAnimationGroup(entry) {
|
|
4766
|
+
const character = entry?.providerMetadata?.[entry.provider]?.character;
|
|
4767
|
+
const text = (value) => typeof value === "string" && value ? value : null;
|
|
4768
|
+
return { animationGroupId: text(character?.animationGroupId), animationName: text(character?.animationName) };
|
|
4769
|
+
}
|
|
4770
|
+
async function inspectMotionLeader(spec, lock, seen) {
|
|
4771
|
+
const leader = spec.character.motionLeader;
|
|
4772
|
+
const label = `motion leader ${spec.styleId}/${leader.assetId}`;
|
|
4773
|
+
const dependency = await inspectParent(leader.spec, lock, seen);
|
|
4774
|
+
return dependency.ready ? { ready: true, reason: `${label} is current` } : { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
|
|
4775
|
+
}
|
|
4717
4776
|
async function inspectScene(spec) {
|
|
4718
4777
|
const scene = spec.scene;
|
|
4719
4778
|
const files = [{ label: "scene image", file: scene.file, sha256: scene.sha256 }];
|
|
@@ -4872,6 +4931,10 @@ async function inspectParent(spec, lock, seen) {
|
|
|
4872
4931
|
const inputs = await inspectCharacterParent(spec, lock, nextSeen);
|
|
4873
4932
|
if (!inputs.ready) return inputs;
|
|
4874
4933
|
}
|
|
4934
|
+
if (spec.character?.motionLeader) {
|
|
4935
|
+
const inputs = await inspectMotionLeader(spec, lock, nextSeen);
|
|
4936
|
+
if (!inputs.ready) return inputs;
|
|
4937
|
+
}
|
|
4875
4938
|
if (spec.character?.styleAnchor) {
|
|
4876
4939
|
const inputs = await inspectStyleAnchor(spec, lock, nextSeen);
|
|
4877
4940
|
if (!inputs.ready) return inputs;
|
|
@@ -4928,6 +4991,7 @@ function dependencies(spec) {
|
|
|
4928
4991
|
spec.mirror?.sourceSpec,
|
|
4929
4992
|
spec.revision?.sourceSpec,
|
|
4930
4993
|
spec.character?.parentSpec,
|
|
4994
|
+
spec.character?.motionLeader?.spec,
|
|
4931
4995
|
spec.character?.styleAnchor?.spec,
|
|
4932
4996
|
spec.objectPro?.parentSpec,
|
|
4933
4997
|
spec.terrainConnect?.lower?.spec,
|
|
@@ -4944,6 +5008,12 @@ function changedTerrainConnection(spec, entry, lock) {
|
|
|
4944
5008
|
}
|
|
4945
5009
|
return null;
|
|
4946
5010
|
}
|
|
5011
|
+
function changedMotionLeader(spec, entry, lock) {
|
|
5012
|
+
const leader = spec.character?.motionLeader;
|
|
5013
|
+
if (!leader) return null;
|
|
5014
|
+
const current = recordedAnimationGroup(lock.entries[lockKey(leader.spec.styleId, leader.spec.assetId)]).animationGroupId;
|
|
5015
|
+
return (entry.motionShare?.animationGroupId ?? null) === current ? null : leader.assetId;
|
|
5016
|
+
}
|
|
4947
5017
|
function changedTerrainSettings(spec, entry) {
|
|
4948
5018
|
const recorded = entry.terrainSettings;
|
|
4949
5019
|
const current = terrainSettings(spec);
|
|
@@ -4985,6 +5055,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
4985
5055
|
let state;
|
|
4986
5056
|
let reason;
|
|
4987
5057
|
const changedConnection = entry ? changedTerrainConnection(spec, entry, lock) : null;
|
|
5058
|
+
const changedMotion = entry ? changedMotionLeader(spec, entry, lock) : null;
|
|
4988
5059
|
const changedSettings = entry ? changedTerrainSettings(spec, entry) : null;
|
|
4989
5060
|
const revision = await inspectRevisionReadiness(spec, lock);
|
|
4990
5061
|
if (revision && !revision.ready) {
|
|
@@ -5024,6 +5095,9 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
5024
5095
|
} else if (changedConnection) {
|
|
5025
5096
|
state = "stale";
|
|
5026
5097
|
reason = `connected set ${changedConnection} was regenerated; draw this set from its new terrain`;
|
|
5098
|
+
} else if (changedMotion) {
|
|
5099
|
+
state = "stale";
|
|
5100
|
+
reason = `shares motion with ${changedMotion}, which was regenerated; draw this direction into its new motion`;
|
|
5027
5101
|
} else if (changedSettings) {
|
|
5028
5102
|
state = "stale";
|
|
5029
5103
|
reason = changedSettings;
|
|
@@ -5087,6 +5161,14 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
5087
5161
|
}
|
|
5088
5162
|
}
|
|
5089
5163
|
}
|
|
5164
|
+
for (const item of items) {
|
|
5165
|
+
const leader = item.spec.character?.motionLeader;
|
|
5166
|
+
if (!leader || !isActionable(item)) continue;
|
|
5167
|
+
const leaderEntry = lock.entries[lockKey(leader.spec.styleId, leader.spec.assetId)];
|
|
5168
|
+
if (leaderEntry?.status === "downloaded" && !recordedAnimationGroup(leaderEntry).animationGroupId) {
|
|
5169
|
+
item.reason += `; ${leader.assetId} has no animation group recorded, so this is drawn on its own and its motion will differ`;
|
|
5170
|
+
}
|
|
5171
|
+
}
|
|
5090
5172
|
const actionable = items.filter(isActionable);
|
|
5091
5173
|
const groupsByProvider = /* @__PURE__ */ new Map();
|
|
5092
5174
|
for (const item of actionable) {
|
|
@@ -6538,6 +6620,8 @@ var CharacterSubmitSchema = z3.object({
|
|
|
6538
6620
|
var AnimateSubmitSchema = z3.object({
|
|
6539
6621
|
background_job_ids: z3.array(z3.string()),
|
|
6540
6622
|
directions: z3.array(z3.string()),
|
|
6623
|
+
/** skeleton-v3 only: the group the directions were drawn into. */
|
|
6624
|
+
animation_group_id: z3.string().nullable().optional(),
|
|
6541
6625
|
status: z3.string().default("processing"),
|
|
6542
6626
|
usage: UsageSchema
|
|
6543
6627
|
}).passthrough();
|
|
@@ -7258,7 +7342,13 @@ var PixelLabClient = class {
|
|
|
7258
7342
|
* Every mode but skeleton-v3 goes to `/animate-character`, the path whose
|
|
7259
7343
|
* shapes were measured live. skeleton-v3 is documented only on
|
|
7260
7344
|
* `/characters/animations`, which takes the same request and answers with
|
|
7261
|
-
* the same job list (plus an `animation_group_id`).
|
|
7345
|
+
* the same job list (plus an `animation_group_id`). With no `template` it
|
|
7346
|
+
* animates `actionDescription` as a sentence, 3 to 15 `frameCount` frames
|
|
7347
|
+
* written into one 3D motion; measured on a v3 character (8 frames, 64px:
|
|
7348
|
+
* 3 generations, frames back at 96x96). `animationGroupId` (skeleton-v3
|
|
7349
|
+
* only) adds the directions to an existing animation of the character,
|
|
7350
|
+
* drawn from its motion; the group's own name must be sent again as
|
|
7351
|
+
* `animationName`, or the group appears renamed.
|
|
7262
7352
|
*/
|
|
7263
7353
|
async animateCharacter(args) {
|
|
7264
7354
|
const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
|
|
@@ -7270,10 +7360,12 @@ var PixelLabClient = class {
|
|
|
7270
7360
|
animation_name: args.animationName,
|
|
7271
7361
|
mode: args.mode,
|
|
7272
7362
|
directions: args.directions,
|
|
7363
|
+
...args.mode === "skeleton-v3" && args.animationGroupId ? { animation_group_id: args.animationGroupId } : {},
|
|
7273
7364
|
...args.template ? { template_animation_id: args.template } : {},
|
|
7274
7365
|
...args.actionDescription ? { action_description: args.actionDescription } : {},
|
|
7275
7366
|
...args.description ? { description: args.description } : {},
|
|
7276
|
-
|
|
7367
|
+
// A template fixes its own frame count; v3 and a skeleton-v3 sentence take one.
|
|
7368
|
+
...(args.mode === "v3" || args.mode === "skeleton-v3" && !args.template) && args.frameCount ? { frame_count: args.frameCount } : {},
|
|
7277
7369
|
...args.mode === "v3" && args.keepFirstFrame === false ? { keep_first_frame: false } : {},
|
|
7278
7370
|
...args.mode === "v3" && args.startFrame ? { custom_start_frame: encode(args.startFrame) } : {},
|
|
7279
7371
|
...args.mode === "v3" && args.endFrame ? { end_frame: encode(args.endFrame) } : {},
|
|
@@ -8953,7 +9045,13 @@ function decodeAnimationJob(jobId) {
|
|
|
8953
9045
|
try {
|
|
8954
9046
|
const value = JSON.parse(Buffer.from(jobId.slice("character-animation:".length), "base64url").toString("utf8"));
|
|
8955
9047
|
if (typeof value?.characterId === "string" && typeof value.name === "string" && typeof value.direction === "string") {
|
|
8956
|
-
return {
|
|
9048
|
+
return {
|
|
9049
|
+
characterId: value.characterId,
|
|
9050
|
+
name: value.name,
|
|
9051
|
+
direction: value.direction,
|
|
9052
|
+
jobIds: Array.isArray(value.jobIds) ? value.jobIds : [],
|
|
9053
|
+
...typeof value.groupId === "string" && value.groupId ? { groupId: value.groupId } : {}
|
|
9054
|
+
};
|
|
8957
9055
|
}
|
|
8958
9056
|
} catch {
|
|
8959
9057
|
}
|
|
@@ -9193,6 +9291,16 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9193
9291
|
if (skeletonTemplate && !CHARACTER_TEMPLATES.includes(skeletonTemplate)) {
|
|
9194
9292
|
throw new Error(`PixelLab animate-skeleton template "${skeletonTemplate}" is not one of ${CHARACTER_TEMPLATES.join(", ")}`);
|
|
9195
9293
|
}
|
|
9294
|
+
if (!spec.revision.description?.trim()) {
|
|
9295
|
+
throw new Error(
|
|
9296
|
+
`${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`
|
|
9297
|
+
);
|
|
9298
|
+
}
|
|
9299
|
+
if (!spec.prompt.trim()) {
|
|
9300
|
+
throw new Error(
|
|
9301
|
+
`${spec.styleId}/${spec.assetId}: an animate-skeleton revision needs a prompt, a short motion label (for example "swinging the sword")`
|
|
9302
|
+
);
|
|
9303
|
+
}
|
|
9196
9304
|
}
|
|
9197
9305
|
if (spec.revision?.mode === "animate" || spec.revision?.mode === "animate-pixminimax") {
|
|
9198
9306
|
const { sourceWidth: width, sourceHeight: height, mode: mode2, frames, lastFrameWidth, lastFrameHeight } = spec.revision;
|
|
@@ -9596,12 +9704,18 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9596
9704
|
`${spec.styleId}/${spec.assetId} animates "${character.animation.direction}", but its character has ${character.directions} directions: ${allowed.join(", ")}`
|
|
9597
9705
|
);
|
|
9598
9706
|
}
|
|
9599
|
-
|
|
9600
|
-
|
|
9707
|
+
const sentence = character.animation.mode === "skeleton-v3" && !character.animation.template;
|
|
9708
|
+
if (character.animation.mode !== "template" && (character.animation.mode !== "skeleton-v3" || sentence) && !spec.prompt.trim()) {
|
|
9709
|
+
throw new Error(
|
|
9710
|
+
`${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")' : "")
|
|
9711
|
+
);
|
|
9712
|
+
}
|
|
9713
|
+
if (sentence && (character.animation.frames < 3 || character.animation.frames > 15)) {
|
|
9714
|
+
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`);
|
|
9601
9715
|
}
|
|
9602
9716
|
if (character.animation.mode === "skeleton-v3" && character.mode === "standard") {
|
|
9603
9717
|
throw new Error(
|
|
9604
|
-
`${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`
|
|
9718
|
+
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`
|
|
9605
9719
|
);
|
|
9606
9720
|
}
|
|
9607
9721
|
}
|
|
@@ -9775,11 +9889,18 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9775
9889
|
};
|
|
9776
9890
|
}
|
|
9777
9891
|
const animation = character.animation;
|
|
9778
|
-
const
|
|
9892
|
+
const ownName = characterAnimationName(spec);
|
|
9893
|
+
const shared = context?.motionGroup;
|
|
9894
|
+
const name = shared?.animationName ?? ownName;
|
|
9779
9895
|
const replaced = context?.replacedMetadata?.character ?? {};
|
|
9780
9896
|
const existing = await this.client.getCharacter(parentId);
|
|
9897
|
+
if (shared && !existing.animations.some((group) => group.animation_group_id === shared.animationGroupId && group.directions.some((d) => d.direction !== animation.direction))) {
|
|
9898
|
+
throw new Error(
|
|
9899
|
+
`${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`
|
|
9900
|
+
);
|
|
9901
|
+
}
|
|
9781
9902
|
for (const group of existing.animations) {
|
|
9782
|
-
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}/`))));
|
|
9903
|
+
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}/`))));
|
|
9783
9904
|
if (!ours || !group.animation_group_id) continue;
|
|
9784
9905
|
if (group.directions.some((d) => d.direction === animation.direction)) {
|
|
9785
9906
|
await this.client.deleteCharacterAnimations(parentId, { animationGroupId: group.animation_group_id }, animation.direction);
|
|
@@ -9794,6 +9915,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9794
9915
|
frameCount: animation.frames,
|
|
9795
9916
|
keepFirstFrame: animation.keepFirstFrame,
|
|
9796
9917
|
directions: [animation.direction],
|
|
9918
|
+
...shared ? { animationGroupId: shared.animationGroupId } : {},
|
|
9797
9919
|
seed: spec.seed,
|
|
9798
9920
|
textGuidanceScale: character.textGuidanceScale,
|
|
9799
9921
|
isometric: character.isometric,
|
|
@@ -9806,10 +9928,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9806
9928
|
enhancePrompt: animation.enhancePrompt,
|
|
9807
9929
|
paletteSwatchBase64: spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0
|
|
9808
9930
|
});
|
|
9809
|
-
const
|
|
9931
|
+
const groupId = res.animation_group_id ?? shared?.animationGroupId;
|
|
9932
|
+
const job = { characterId: parentId, name, direction: animation.direction, jobIds: res.background_job_ids, ...groupId ? { groupId } : {} };
|
|
9810
9933
|
return {
|
|
9811
9934
|
jobId: encodeAnimationJob(job),
|
|
9812
|
-
metadata: { character: { kind: "animation", characterId: parentId, animationName: name, direction: animation.direction, mode: animation.mode, backgroundJobIds: res.background_job_ids } }
|
|
9935
|
+
metadata: { character: { kind: "animation", characterId: parentId, animationName: name, direction: animation.direction, mode: animation.mode, backgroundJobIds: res.background_job_ids, ...groupId ? { animationGroupId: groupId } : {} } }
|
|
9813
9936
|
};
|
|
9814
9937
|
}
|
|
9815
9938
|
/**
|
|
@@ -10024,13 +10147,13 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
10024
10147
|
const animationId = typeof done.animation_id === "string" ? done.animation_id : null;
|
|
10025
10148
|
if (Array.isArray(frames) && frames.length && frames.every((f) => typeof f === "string")) {
|
|
10026
10149
|
const character2 = await this.client.getCharacter(job.characterId);
|
|
10027
|
-
const group = findAnimation(character2, job.name, job.direction, animationId);
|
|
10028
|
-
return review(frames, animationId, group?.groupId ?? null, billedFromUsage(status.usage));
|
|
10150
|
+
const group = findAnimation(character2, job.name, job.direction, animationId, job.groupId);
|
|
10151
|
+
return review(frames, animationId, group?.groupId ?? job.groupId ?? null, billedFromUsage(status.usage));
|
|
10029
10152
|
}
|
|
10030
10153
|
}
|
|
10031
10154
|
const recorded = context?.metadata?.character?.animationId ?? null;
|
|
10032
10155
|
const character = await this.client.getCharacter(job.characterId);
|
|
10033
|
-
const found = findAnimation(character, job.name, job.direction, recorded);
|
|
10156
|
+
const found = findAnimation(character, job.name, job.direction, recorded, job.groupId);
|
|
10034
10157
|
if (found) return review(found.frames, found.animationId, found.groupId, null);
|
|
10035
10158
|
return cleanedUp ? { status: "failed", error: `animation job is gone upstream and no "${job.name}" ${job.direction} animation exists on the character` } : { status: "processing" };
|
|
10036
10159
|
}
|
|
@@ -11464,10 +11587,11 @@ function rotationSources(character) {
|
|
|
11464
11587
|
}
|
|
11465
11588
|
return sources;
|
|
11466
11589
|
}
|
|
11467
|
-
function findAnimation(character, name, direction, animationId) {
|
|
11590
|
+
function findAnimation(character, name, direction, animationId, groupId) {
|
|
11468
11591
|
for (const group of character.animations) {
|
|
11469
11592
|
const match = group.directions.find((d) => d.direction === direction && d.frames.length > 0);
|
|
11470
11593
|
if (!match) continue;
|
|
11594
|
+
if (groupId && group.animation_group_id !== groupId) continue;
|
|
11471
11595
|
const byName = group.display_name === name || group.animation_type === name;
|
|
11472
11596
|
const byId = animationId !== null && match.frames.some((url) => url.includes(`/animations/${animationId}/`));
|
|
11473
11597
|
if (!byName && !byId) continue;
|
|
@@ -12602,8 +12726,16 @@ function expandLoopDirections(raw) {
|
|
|
12602
12726
|
continue;
|
|
12603
12727
|
}
|
|
12604
12728
|
for (const expandedId of ids) claimed.add(expandedId);
|
|
12605
|
-
|
|
12606
|
-
|
|
12729
|
+
const sentence = animation.mode === "skeleton-v3" && !animation.template;
|
|
12730
|
+
for (const [index, loop] of loops.entries()) {
|
|
12731
|
+
expanded[loop.id] = {
|
|
12732
|
+
...asset,
|
|
12733
|
+
animation: {
|
|
12734
|
+
...animation,
|
|
12735
|
+
direction: loop.direction,
|
|
12736
|
+
...sentence && index > 0 ? { motionFrom: loops[0].id } : {}
|
|
12737
|
+
}
|
|
12738
|
+
};
|
|
12607
12739
|
}
|
|
12608
12740
|
for (const mirror of mirrors) {
|
|
12609
12741
|
const inherited = {};
|
|
@@ -13219,7 +13351,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
13219
13351
|
`assets.${assetId}: mirroring ${asset.mirror} gives another ${facing}-facing loop; mirror a loop facing east, west, or a diagonal`
|
|
13220
13352
|
);
|
|
13221
13353
|
}
|
|
13222
|
-
|
|
13354
|
+
const { motionLeader: _motionLeader, ...flipped } = sourceSpec.character;
|
|
13355
|
+
resolved.character = { ...flipped, animation: { ...sourceSpec.character.animation, direction: mirrored } };
|
|
13223
13356
|
} else if (sourceSpec.character) {
|
|
13224
13357
|
resolved.character = { ...sourceSpec.character };
|
|
13225
13358
|
}
|
|
@@ -13276,6 +13409,26 @@ async function resolveSpecs(loaded, filter) {
|
|
|
13276
13409
|
directions: resolved.character.kind === "base" ? resolved.character.directions : parentSpec.character?.directions ?? resolved.character.directions
|
|
13277
13410
|
};
|
|
13278
13411
|
}
|
|
13412
|
+
if (resolved.character?.animation && asset.animation?.motionFrom) {
|
|
13413
|
+
const leaderId = asset.animation.motionFrom;
|
|
13414
|
+
const where = `assets.${assetId}.animation.motionFrom`;
|
|
13415
|
+
if (leaderId === assetId) throw new Error(`${where}: an asset cannot share its own motion`);
|
|
13416
|
+
const leader = await finalize(leaderId);
|
|
13417
|
+
const led = leader.character?.animation;
|
|
13418
|
+
if (leader.character?.kind !== "animation" || leader.mirror || !led || led.mode !== "skeleton-v3" || led.template) {
|
|
13419
|
+
throw new Error(`${where}: ${leaderId} is not a skeleton-v3 loop from a sentence`);
|
|
13420
|
+
}
|
|
13421
|
+
if (leader.character.parentAssetId !== resolved.character.parentAssetId) {
|
|
13422
|
+
throw new Error(`${where}: ${leaderId} animates ${leader.character.parentAssetId}, not ${resolved.character.parentAssetId}`);
|
|
13423
|
+
}
|
|
13424
|
+
if (leader.character.motionLeader) {
|
|
13425
|
+
throw new Error(`${where}: ${leaderId} itself shares ${leader.character.motionLeader.assetId}'s motion; point at the loop that leads`);
|
|
13426
|
+
}
|
|
13427
|
+
if (led.direction === resolved.character.animation.direction) {
|
|
13428
|
+
throw new Error(`${where}: ${leaderId} is drawn in the same direction; sharing a motion needs another direction`);
|
|
13429
|
+
}
|
|
13430
|
+
resolved.character = { ...resolved.character, motionLeader: { assetId: leaderId, spec: leader } };
|
|
13431
|
+
}
|
|
13279
13432
|
if (resolved.objectPro && characterParent) {
|
|
13280
13433
|
const parentSpec = await finalize(characterParent);
|
|
13281
13434
|
if (!parentSpec.objectPro) {
|
|
@@ -17029,7 +17182,8 @@ var SkeletonAnimationRequestSchema = z10.object({
|
|
|
17029
17182
|
/** The motion, sent as PixelLab's `action`. */
|
|
17030
17183
|
prompt: z10.string().min(1),
|
|
17031
17184
|
direction: CharacterDirectionSchema,
|
|
17032
|
-
|
|
17185
|
+
/** What the character looks like; PixelLab refuses the request without it. */
|
|
17186
|
+
description: z10.string().min(1),
|
|
17033
17187
|
skeletonTemplate: z10.string().min(1).optional(),
|
|
17034
17188
|
/** Manifest-relative JSON path inside the project. */
|
|
17035
17189
|
keypointsFile: z10.string().min(1),
|
|
@@ -17136,7 +17290,7 @@ async function createSkeletonAnimation(manifestPath, request) {
|
|
|
17136
17290
|
from: request.from,
|
|
17137
17291
|
keypointsFile: relative.split(path31.sep).join("/"),
|
|
17138
17292
|
direction: request.direction,
|
|
17139
|
-
|
|
17293
|
+
description: request.description,
|
|
17140
17294
|
...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
|
|
17141
17295
|
}
|
|
17142
17296
|
}
|
|
@@ -17460,6 +17614,7 @@ function retireGeneration(entry, retiredAt) {
|
|
|
17460
17614
|
provider: entry.provider,
|
|
17461
17615
|
postprocess: entry.postprocess,
|
|
17462
17616
|
...entry.terrainConnect ? { terrainConnect: entry.terrainConnect } : {},
|
|
17617
|
+
...entry.motionShare ? { motionShare: entry.motionShare } : {},
|
|
17463
17618
|
...entry.terrainSettings ? { terrainSettings: entry.terrainSettings } : {},
|
|
17464
17619
|
retiredAt
|
|
17465
17620
|
};
|
|
@@ -17555,6 +17710,8 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
17555
17710
|
// A connected set comes back with the connection it was drawn from, so
|
|
17556
17711
|
// the plan compares the restored generation, not the one it replaced.
|
|
17557
17712
|
terrainConnect: generation.terrainConnect,
|
|
17713
|
+
// Likewise a loop that shared another direction's motion.
|
|
17714
|
+
motionShare: generation.motionShare,
|
|
17558
17715
|
// Likewise its settings; a generation from before they were recorded
|
|
17559
17716
|
// comes back with none and is taken as current.
|
|
17560
17717
|
terrainSettings: generation.terrainSettings,
|
|
@@ -18919,6 +19076,12 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
18919
19076
|
};
|
|
18920
19077
|
terrainConnect[side] = { sourceAssetId: link.assetId, sourceObjectId: objectId };
|
|
18921
19078
|
}
|
|
19079
|
+
const motionLeader = spec.character?.motionLeader;
|
|
19080
|
+
const leaderGroup = motionLeader ? recordedAnimationGroup(lock.entries[lockKey(motionLeader.spec.styleId, motionLeader.spec.assetId)]) : void 0;
|
|
19081
|
+
const motionGroup = leaderGroup?.animationGroupId && leaderGroup.animationName ? { animationGroupId: leaderGroup.animationGroupId, animationName: leaderGroup.animationName } : void 0;
|
|
19082
|
+
if (motionLeader && !motionGroup) {
|
|
19083
|
+
log2(` ${key}: ${motionLeader.assetId} has no animation group recorded; drawing this direction on its own`);
|
|
19084
|
+
}
|
|
18922
19085
|
upsert(lock, key, {
|
|
18923
19086
|
styleId: spec.styleId,
|
|
18924
19087
|
assetId: spec.assetId,
|
|
@@ -18960,6 +19123,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
18960
19123
|
referenceSha256: spec.character.outfit.reference.sha256
|
|
18961
19124
|
} : null,
|
|
18962
19125
|
terrainConnect: spec.terrainConnect ? terrainConnect : void 0,
|
|
19126
|
+
motionShare: motionLeader ? { sourceAssetId: motionLeader.assetId, animationGroupId: motionGroup?.animationGroupId ?? null } : void 0,
|
|
18963
19127
|
// The terrain knobs specHash leaves out, so the plan can tell a
|
|
18964
19128
|
// changed one; undefined (no key) for every other generator.
|
|
18965
19129
|
terrainSettings: terrainSettings(spec),
|
|
@@ -19010,6 +19174,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
19010
19174
|
...anchorEntry?.objectId ? { styleObjectId: anchorEntry.objectId } : {},
|
|
19011
19175
|
...parentOutputs ? { parentOutputs } : {},
|
|
19012
19176
|
...spec.terrainConnect ? { terrainLinks } : {},
|
|
19177
|
+
...motionGroup ? { motionGroup } : {},
|
|
19013
19178
|
...replacedMetadata ? { replacedMetadata } : {},
|
|
19014
19179
|
...lastGenerationMetadata ? { lastGenerationMetadata } : {},
|
|
19015
19180
|
...previousMetadata ? { previousMetadata } : {},
|