pixelkiln 0.54.0 → 0.56.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 CHANGED
@@ -683,6 +683,23 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
683
683
  concept: spec.character.concept?.sha256,
684
684
  styleAnchor: spec.character.styleAnchor ? { of: spec.character.styleAnchor.assetId, sha256: spec.character.styleAnchor.sha256 } : void 0
685
685
  } : void 0,
686
+ // An objectPro family: same shape as character above, minus the
687
+ // fields it has none of (mode/template/proportions/isometric/concept/
688
+ // styleAnchor) — see ResolvedObjectPro.
689
+ objectPro: spec.objectPro ? {
690
+ kind: spec.objectPro.kind,
691
+ directions: spec.objectPro.directions,
692
+ parent: spec.objectPro.parentAssetId,
693
+ parentSha256: spec.objectPro.parentSha256 ?? null,
694
+ state: spec.objectPro.state,
695
+ animation: spec.objectPro.animation ? {
696
+ ...spec.objectPro.animation,
697
+ startFrame: spec.objectPro.animation.startFrame?.sha256,
698
+ endFrame: spec.objectPro.animation.endFrame?.sha256
699
+ } : void 0,
700
+ styleTraits: spec.objectPro.styleTraits,
701
+ reference: spec.objectPro.reference?.sha256
702
+ } : void 0,
686
703
  // A mirror's bytes come from its source's recorded outputs; the plan
687
704
  // compares those directly, so only the choice of source is identity.
688
705
  mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
@@ -709,7 +726,7 @@ import path2 from "path";
709
726
  // src/types.ts
710
727
  import { z } from "zod";
711
728
  var MediaTypeSchema = z.enum(["image/png", "image/gif"]);
712
- var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile"]);
729
+ var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro"]);
713
730
  var GridConfidenceSchema = z.enum(["low", "medium", "high"]);
714
731
  var RevisionModeSchema = z.enum(["image-to-image", "inpaint", "outpaint"]);
715
732
  function tileVariationCount(descriptions) {
@@ -1089,6 +1106,14 @@ var StyleObjectSchema = z.object({
1089
1106
  * (~50%, the API default).
1090
1107
  */
1091
1108
  isometricTileShape: z.enum(["thin tile", "thick tile", "block"]).optional(),
1109
+ /**
1110
+ * `objectPro` generator only. Rotations captured for a base: 1 (the API
1111
+ * still draws and stores a single facing) or the full 8. The API default
1112
+ * is 8. Unlike `character`, `objectPro` has no skeleton/template modes
1113
+ * and so no `mode`/`template` fields of its own — `/create-object-pro-flash`
1114
+ * is the only base-creation path.
1115
+ */
1116
+ objectDirections: z.union([z.literal(1), z.literal(8)]).optional(),
1092
1117
  /**
1093
1118
  * `pixflux` only. Whether to strip the generated background.
1094
1119
  *
@@ -1293,25 +1318,41 @@ var AssetSchema = z.object({
1293
1318
  revision: RevisionSchema.optional(),
1294
1319
  /**
1295
1320
  * The provider's own id for art that already exists on the account, so
1296
- * `adopt` can map it without matching bytes: an object id, a character
1297
- * id, or `<character id>#<animation group id>` for a character animation.
1298
- * Not part of the spec's identity.
1321
+ * `adopt` can map it without matching bytes: an object id (an
1322
+ * `objectPro` base and state included, since they share PixelLab's
1323
+ * `/objects` identity space), a character id, or `<id>#<animation group
1324
+ * id>` for a character or `objectPro` animation. Not part of the spec's
1325
+ * identity.
1299
1326
  */
1300
1327
  remoteId: z.string().min(1).optional(),
1301
- /** `character` styles: this asset is a pose or outfit of another character asset. */
1328
+ /**
1329
+ * `character` or `objectPro` styles: this asset is a pose or edit of
1330
+ * another asset in the same family. `objectPro` ignores
1331
+ * `paletteFromReference`/`canvas` — the API has no equivalent, and
1332
+ * always inherits the parent's exact canvas.
1333
+ */
1302
1334
  state: CharacterStateSchema.optional(),
1303
- /** `character` styles: this asset is a loop of another character asset in one direction. */
1335
+ /**
1336
+ * `character` or `objectPro` styles: this asset is a loop of another
1337
+ * asset in the same family, in one direction. `objectPro` has no
1338
+ * skeleton/template concept, so `template`/`subject`/`outline`/
1339
+ * `shading`/`detail` are rejected on an `objectPro` animation rather
1340
+ * than silently ignored — describe the motion in the asset's own
1341
+ * `prompt` instead.
1342
+ */
1304
1343
  animation: CharacterAnimationSchema.optional(),
1305
1344
  /** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
1306
1345
  proportions: CharacterProportionsSchema.optional(),
1307
1346
  /**
1308
- * `character` styles, bases only: the character's own sprite, which
1309
- * PixelLab rotates into the other directions instead of drawing from
1310
- * the prompt. A manifest-relative PNG or JPEG of the south-facing
1311
- * sprite, or an object keyed by direction (`{ "south": ..., "east":
1312
- * ... }`); quadrupeds in `standard` mode need south and east, and only
1313
- * `standard` takes more than south. `standard` wants each image at the
1314
- * style's size; v3 accepts up to 256px, pro up to 168px. The prompt
1347
+ * `character` or `objectPro` styles, bases only: the subject's own
1348
+ * sprite, which PixelLab rotates into the other directions instead of
1349
+ * drawing from the prompt. A manifest-relative PNG or JPEG of the
1350
+ * south-facing sprite, or an object keyed by direction (`{ "south":
1351
+ * ..., "east": ... }`); quadrupeds in `standard` character mode need
1352
+ * south and east, and only `standard` takes more than south. `objectPro`
1353
+ * only ever reads the south image, since `/create-object-pro-flash`
1354
+ * rotates from one frame. `standard` wants each image at the style's
1355
+ * size; v3 accepts up to 256px, pro up to 168px. The prompt
1315
1356
  * still guides the result.
1316
1357
  */
1317
1358
  reference: z.union([z.string().min(1), z.record(CharacterDirectionSchema, z.string().min(1))]).optional(),
@@ -3797,6 +3838,7 @@ async function inspectRevisionReadiness(spec, lock) {
3797
3838
  if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
3798
3839
  if (spec.character?.parentSpec) return inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
3799
3840
  if (spec.character?.styleAnchor) return inspectStyleAnchor(spec, lock, /* @__PURE__ */ new Set());
3841
+ if (spec.objectPro?.parentSpec) return inspectObjectProParent(spec, lock, /* @__PURE__ */ new Set());
3800
3842
  if (!spec.revision) return null;
3801
3843
  return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
3802
3844
  }
@@ -3838,6 +3880,24 @@ async function inspectCharacterParent(spec, lock, seen) {
3838
3880
  }
3839
3881
  return { ready: true, reason: `${label} is current` };
3840
3882
  }
3883
+ async function inspectObjectProParent(spec, lock, seen) {
3884
+ const object = spec.objectPro;
3885
+ const parentSpec = object.parentSpec;
3886
+ const label = `${object.kind} parent ${spec.styleId}/${object.parentAssetId}`;
3887
+ const dependency = await inspectParent(parentSpec, lock, seen);
3888
+ if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
3889
+ if (!object.parentSha256 || !object.parentFile || !existsSync4(object.parentFile)) {
3890
+ return { ready: false, reason: `${label} has no generated south-facing file yet` };
3891
+ }
3892
+ if (await sha256File(object.parentFile) !== object.parentSha256) {
3893
+ return { ready: false, reason: `${label} changed after the manifest was resolved` };
3894
+ }
3895
+ const parentEntry = lock.entries[lockKey(parentSpec.styleId, parentSpec.assetId)];
3896
+ if (!parentEntry?.objectId) {
3897
+ return { ready: false, reason: `${label} has no PixelLab object id recorded` };
3898
+ }
3899
+ return { ready: true, reason: `${label} is current` };
3900
+ }
3841
3901
  async function inspectRevision(spec, lock, seen) {
3842
3902
  const revision = spec.revision;
3843
3903
  if (!revision) return { ready: true, reason: "asset has no revision inputs" };
@@ -3889,6 +3949,10 @@ async function inspectParent(spec, lock, seen) {
3889
3949
  const inputs = await inspectStyleAnchor(spec, lock, nextSeen);
3890
3950
  if (!inputs.ready) return inputs;
3891
3951
  }
3952
+ if (spec.objectPro?.parentSpec) {
3953
+ const inputs = await inspectObjectProParent(spec, lock, nextSeen);
3954
+ if (!inputs.ready) return inputs;
3955
+ }
3892
3956
  if (spec.quality) {
3893
3957
  const quality = await inspectQualityProfile(spec, lock);
3894
3958
  return quality?.state === "approved" ? { ready: true, reason: "approved quality output is current" } : {
@@ -3933,7 +3997,8 @@ function dependencies(spec) {
3933
3997
  spec.mirror?.sourceSpec,
3934
3998
  spec.revision?.sourceSpec,
3935
3999
  spec.character?.parentSpec,
3936
- spec.character?.styleAnchor?.spec
4000
+ spec.character?.styleAnchor?.spec,
4001
+ spec.objectPro?.parentSpec
3937
4002
  ].filter((parent) => Boolean(parent));
3938
4003
  }
3939
4004
  function resumeCommandForStatus(status) {
@@ -5281,9 +5346,23 @@ var RevisionJobSubmitSchema = z2.object({
5281
5346
  background_job_id: z2.string().min(1),
5282
5347
  status: z2.string().default("processing")
5283
5348
  }).passthrough();
5349
+ var ObjectAnimationGroupSchema = z2.object({
5350
+ animation_group_id: z2.string().min(1),
5351
+ display_name: z2.string().nullable().default(null),
5352
+ description: z2.string().default(""),
5353
+ frame_count: z2.number().int().default(0),
5354
+ directions: z2.array(
5355
+ z2.object({
5356
+ direction: z2.string(),
5357
+ storage_urls: z2.record(z2.unknown()).default({}),
5358
+ created_at: z2.string()
5359
+ }).passthrough()
5360
+ ).default([])
5361
+ }).passthrough();
5284
5362
  var PixelLabObjectSchema = z2.object({
5285
5363
  id: z2.string().min(1),
5286
5364
  name: z2.string().nullable().default(null),
5365
+ state_name: z2.string().nullable().optional(),
5287
5366
  prompt: z2.string().default(""),
5288
5367
  size: z2.object({ width: z2.number().int().positive(), height: z2.number().int().positive() }),
5289
5368
  directions: z2.number().default(0),
@@ -5295,7 +5374,9 @@ var PixelLabObjectSchema = z2.object({
5295
5374
  tags: z2.array(z2.string()).default([]),
5296
5375
  status: z2.string().nullable().default(null),
5297
5376
  progress_percent: z2.number().nullable().optional(),
5298
- eta_seconds: z2.number().nullable().optional()
5377
+ eta_seconds: z2.number().nullable().optional(),
5378
+ group_id: z2.string().nullable().optional(),
5379
+ animations: z2.array(ObjectAnimationGroupSchema).default([])
5299
5380
  }).passthrough();
5300
5381
  var MapObjectSchema = z2.object({
5301
5382
  object_id: z2.string().min(1),
@@ -5320,6 +5401,28 @@ var AnimateSubmitSchema = z2.object({
5320
5401
  status: z2.string().default("processing"),
5321
5402
  usage: UsageSchema
5322
5403
  }).passthrough();
5404
+ var ObjectProSubmitSchema = z2.object({
5405
+ object_id: z2.string().min(1),
5406
+ background_job_id: z2.string().min(1),
5407
+ status: z2.string().default("processing"),
5408
+ usage: UsageSchema
5409
+ }).passthrough();
5410
+ var DirectionSubmissionSchema = z2.object({
5411
+ direction: z2.string(),
5412
+ status: z2.string(),
5413
+ background_job_id: z2.string().nullable().default(null),
5414
+ animation_id: z2.string().nullable().default(null)
5415
+ }).passthrough();
5416
+ var AnimateObjectSubmitSchema = z2.object({
5417
+ animation_group_id: z2.string().min(1),
5418
+ object_id: z2.string().min(1),
5419
+ mode: z2.string(),
5420
+ frame_count: z2.number().int(),
5421
+ display_name: z2.string().nullable().default(null),
5422
+ description: z2.string().default(""),
5423
+ submissions: z2.array(DirectionSubmissionSchema),
5424
+ usage: UsageSchema
5425
+ }).passthrough();
5323
5426
  var CharacterAnimationDirectionSchema = z2.object({
5324
5427
  direction: z2.string(),
5325
5428
  frame_count: z2.number().int().min(0),
@@ -5622,6 +5725,81 @@ var PixelLabClient = class {
5622
5725
  "get object"
5623
5726
  );
5624
5727
  }
5728
+ /**
5729
+ * A standalone object with no skeleton, one call draws the south sprite
5730
+ * (or takes the author's) and rotates it with v3 — the character
5731
+ * pro-flash pattern, minus `template_id` (objects have no skeleton) and
5732
+ * with a real `n_directions` choice (1 or 8, character pro-flash is
5733
+ * always 8). Async only: `/create-object-pro-flash` has no sync response.
5734
+ */
5735
+ async createObjectProFlash(args) {
5736
+ const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
5737
+ const traits = args.styleTraits;
5738
+ const body = {
5739
+ description: args.description,
5740
+ n_directions: args.directions,
5741
+ ...args.reference ? { first_frame: encode(args.reference) } : { image_size: { width: args.size, height: args.size } },
5742
+ ...args.view ? { view: args.view } : {},
5743
+ ...args.seed != null ? { seed: args.seed } : {},
5744
+ ...args.styleReference && args.styleReferenceSize ? {
5745
+ style_image: { image: encode(args.styleReference), size: args.styleReferenceSize },
5746
+ ...traits ? {
5747
+ style_options: {
5748
+ ...traits.palette !== void 0 ? { color_palette: traits.palette } : {},
5749
+ ...traits.outline !== void 0 ? { outline: traits.outline } : {},
5750
+ ...traits.detail !== void 0 ? { detail: traits.detail } : {},
5751
+ ...traits.shading !== void 0 ? { shading: traits.shading } : {}
5752
+ }
5753
+ } : {}
5754
+ } : {}
5755
+ };
5756
+ return validateResponse(
5757
+ ObjectProSubmitSchema,
5758
+ await this.request("/create-object-pro-flash", { method: "POST", body: JSON.stringify(body) }),
5759
+ "create-object-pro-flash"
5760
+ );
5761
+ }
5762
+ /** A text edit of an existing object, applied to its whole rotation set and stored as a sibling. Always inherits the parent's exact canvas — no size override exists on this endpoint. */
5763
+ async createObjectProState(args) {
5764
+ const raw = await this.request(`/objects/${encodeURIComponent(args.objectId)}/states`, {
5765
+ method: "POST",
5766
+ body: JSON.stringify({
5767
+ edit_description: args.editDescription,
5768
+ ...args.stateName ? { state_name: args.stateName } : {},
5769
+ ...args.seed != null ? { seed: args.seed } : {}
5770
+ })
5771
+ });
5772
+ return validateResponse(ObjectProSubmitSchema, raw, "create object state");
5773
+ }
5774
+ /**
5775
+ * One direction of one object's animation. Unlike character animation,
5776
+ * this endpoint's own `replace_existing` flag regenerates an
5777
+ * already-animated direction directly (character has no such flag and
5778
+ * pixelkiln instead deletes the prior take itself before resubmitting;
5779
+ * see `submitCharacter`). A 1-direction object must not receive
5780
+ * `directions` at all -- the API 400s if it does.
5781
+ */
5782
+ async animateObject(args) {
5783
+ const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
5784
+ const body = {
5785
+ mode: args.mode,
5786
+ ...args.directions === 8 ? { directions: [args.direction] } : {},
5787
+ ...args.animationDescription ? { animation_description: args.animationDescription } : {},
5788
+ ...args.animationGroupId ? { animation_group_id: args.animationGroupId } : {},
5789
+ ...args.displayName ? { display_name: args.displayName } : {},
5790
+ ...args.frameCount != null ? { frame_count: args.frameCount } : {},
5791
+ ...args.keepFirstFrame === false ? { keep_first_frame: false } : {},
5792
+ ...args.startFrame ? { custom_start_frame: encode(args.startFrame) } : {},
5793
+ ...args.endFrame ? { end_frame: encode(args.endFrame) } : {},
5794
+ ...args.enhancePrompt ? { enhance_prompt: true } : {},
5795
+ ...args.replaceExisting ? { replace_existing: true } : {}
5796
+ };
5797
+ const raw = await this.request(`/objects/${encodeURIComponent(args.objectId)}/animations`, {
5798
+ method: "POST",
5799
+ body: JSON.stringify(body)
5800
+ });
5801
+ return validateResponse(AnimateObjectSubmitSchema, raw, "animate object");
5802
+ }
5625
5803
  async getMapObject(objectId) {
5626
5804
  return validateResponse(
5627
5805
  MapObjectSchema,
@@ -6021,6 +6199,29 @@ function proFlashCharacterCost(width, height, fromReference) {
6021
6199
  function characterAnimationName(spec) {
6022
6200
  return `pixelkiln:${spec.styleId}/${spec.assetId}`;
6023
6201
  }
6202
+ function objectAnimationName(spec) {
6203
+ return `pixelkiln:${spec.styleId}/${spec.assetId}`;
6204
+ }
6205
+ function proFlashObjectCost(width, height, directions, fromReference) {
6206
+ const side = Math.max(32, Math.ceil(Math.max(width, height) / 4) * 4);
6207
+ const rotations = Math.max(1, Math.ceil(side * side * directions / 65536));
6208
+ if (fromReference) return rotations;
6209
+ const image = side <= 96 ? 5 : side <= 208 ? 6 : 9;
6210
+ return image + rotations;
6211
+ }
6212
+ function objectProCost(spec) {
6213
+ const object = spec.objectPro;
6214
+ if (!object) return generationCost(spec.width, spec.height, "1dir");
6215
+ const px = spec.width * spec.height;
6216
+ if (object.kind === "animation") {
6217
+ const animation = object.animation;
6218
+ if (animation.mode === "pro") return generationCost(spec.width, spec.height, "1dir");
6219
+ return Math.max(1, Math.ceil(px * animation.frames / 65536));
6220
+ }
6221
+ if (object.kind === "state") return generationCost(spec.width, spec.height, "1dir");
6222
+ const south = object.reference;
6223
+ return south ? proFlashObjectCost(south.width, south.height, object.directions, true) : proFlashObjectCost(spec.width, spec.height, object.directions, false);
6224
+ }
6024
6225
  var CHARACTER_TEMPLATES = ["mannequin", "bear", "cat", "dog", "horse", "lion"];
6025
6226
  var PRO_FLASH_TEMPLATES = [...CHARACTER_TEMPLATES, "custom"];
6026
6227
  function encodeAnimationJob(job) {
@@ -6037,6 +6238,20 @@ function decodeAnimationJob(jobId) {
6037
6238
  }
6038
6239
  return null;
6039
6240
  }
6241
+ function encodeObjectAnimationJob(job) {
6242
+ return `object-animation:${Buffer.from(JSON.stringify(job)).toString("base64url")}`;
6243
+ }
6244
+ function decodeObjectAnimationJob(jobId) {
6245
+ if (!jobId.startsWith("object-animation:")) return null;
6246
+ try {
6247
+ const value = JSON.parse(Buffer.from(jobId.slice("object-animation:".length), "base64url").toString("utf8"));
6248
+ if (typeof value?.objectId === "string" && typeof value.name === "string" && typeof value.direction === "string") {
6249
+ return { objectId: value.objectId, name: value.name, direction: value.direction, jobId: typeof value.jobId === "string" ? value.jobId : null };
6250
+ }
6251
+ } catch {
6252
+ }
6253
+ return null;
6254
+ }
6040
6255
  var PixelLabProvider = class _PixelLabProvider {
6041
6256
  constructor(client2) {
6042
6257
  this.client = client2;
@@ -6055,7 +6270,7 @@ var PixelLabProvider = class _PixelLabProvider {
6055
6270
  return new _PixelLabProvider(new PixelLabClient("download-only"));
6056
6271
  }
6057
6272
  supports(generator) {
6058
- return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character" || generator === "isometricTile";
6273
+ return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character" || generator === "isometricTile" || generator === "objectPro";
6059
6274
  }
6060
6275
  /**
6061
6276
  * `inpaint` (`/inpaint-v3`, a mask) and `image-to-image` (`/edit-images-v2`,
@@ -6097,6 +6312,9 @@ var PixelLabProvider = class _PixelLabProvider {
6097
6312
  if (spec.generator === "character") {
6098
6313
  return { unit: "generations", amount: characterCost(spec), candidates: 1 };
6099
6314
  }
6315
+ if (spec.generator === "objectPro") {
6316
+ return { unit: "generations", amount: objectProCost(spec), candidates: 1 };
6317
+ }
6100
6318
  return {
6101
6319
  unit: "generations",
6102
6320
  amount: generationCost(spec.width, spec.height, spec.generator),
@@ -6205,6 +6423,7 @@ var PixelLabProvider = class _PixelLabProvider {
6205
6423
  }
6206
6424
  }
6207
6425
  if (spec.generator === "character") this.validateCharacter(spec, styleImages);
6426
+ if (spec.generator === "objectPro") this.validateObjectPro(spec, styleImages);
6208
6427
  if ((spec.generator === "map" || spec.generator === "pixflux") && styleImages.length) {
6209
6428
  throw new Error(`PixelLab ${spec.generator} does not support style images`);
6210
6429
  }
@@ -6365,6 +6584,63 @@ var PixelLabProvider = class _PixelLabProvider {
6365
6584
  }
6366
6585
  }
6367
6586
  }
6587
+ validateObjectPro(spec, styleImages = []) {
6588
+ const object = spec.objectPro;
6589
+ if (!object) throw new Error(`${spec.styleId}/${spec.assetId} has no objectPro shape`);
6590
+ const label = `${spec.styleId}/${spec.assetId}`;
6591
+ if (object.kind === "base" && object.reference && styleImages.length) {
6592
+ throw new Error(`${label}: a base rotating its own reference sprite takes no style image`);
6593
+ }
6594
+ if (object.reference && (object.reference.width > 256 || object.reference.height > 256)) {
6595
+ throw new Error(`${label}: reference is ${object.reference.width}x${object.reference.height}; PixelLab objectPro takes up to 256px`);
6596
+ }
6597
+ if (styleImages.length) {
6598
+ if (styleImages.length > 1) throw new Error(`PixelLab objectPro takes one style image`);
6599
+ const image = styleImages[0];
6600
+ if (image.width > 256 || image.height > 256) {
6601
+ throw new Error(`PixelLab objectPro style image is ${image.width}x${image.height}; the limit is 256px`);
6602
+ }
6603
+ if (image.width > spec.width || image.height > spec.height) {
6604
+ throw new Error(
6605
+ `PixelLab objectPro style image is ${image.width}x${image.height} but the object is ${spec.width}x${spec.height}; crop the image to its subject or raise the style's size`
6606
+ );
6607
+ }
6608
+ }
6609
+ if (object.kind === "base" && (spec.width < 16 || spec.height < 16 || spec.width > 256 || spec.height > 256)) {
6610
+ throw new Error(`PixelLab objectPro bases must be between 16 and 256 pixels`);
6611
+ }
6612
+ if (object.kind === "base" && (spec.width % 4 || spec.height % 4)) {
6613
+ throw new Error(`PixelLab objectPro bases are sized in multiples of 4 pixels; ${spec.width}x${spec.height} is not`);
6614
+ }
6615
+ if (object.kind !== "base" && !object.parentAssetId) {
6616
+ throw new Error(`${spec.styleId}/${spec.assetId} is a ${object.kind} with no parent`);
6617
+ }
6618
+ if (object.animation) {
6619
+ const allowed = object.directions === 1 ? ["south"] : CHARACTER_DIRECTIONS_8;
6620
+ if (!allowed.includes(object.animation.direction)) {
6621
+ throw new Error(
6622
+ `${label} animates "${object.animation.direction}", but its object has ${object.directions} direction${object.directions === 1 ? "" : "s"}: ${allowed.join(", ")}`
6623
+ );
6624
+ }
6625
+ if (!spec.prompt.trim()) {
6626
+ throw new Error(`${label} needs a prompt describing the motion`);
6627
+ }
6628
+ const { startFrame, endFrame } = object.animation;
6629
+ for (const [name, image] of [["start frame", startFrame], ["end frame", endFrame]]) {
6630
+ if (image && (image.width > 256 || image.height > 256)) {
6631
+ throw new Error(`${label}: ${name} is ${image.width}x${image.height}; PixelLab v3 takes up to 256px`);
6632
+ }
6633
+ }
6634
+ if (endFrame) {
6635
+ const start = startFrame ?? (object.parentFile && existsSync7(object.parentFile) ? imageMetadata(readFileSync2(object.parentFile)) : null);
6636
+ if (start && (start.width !== endFrame.width || start.height !== endFrame.height)) {
6637
+ throw new Error(
6638
+ `${label}: end frame is ${endFrame.width}x${endFrame.height} but the ${startFrame ? "start frame" : "object's rotation"} is ${start.width}x${start.height}; PixelLab interpolates between frames of one size`
6639
+ );
6640
+ }
6641
+ }
6642
+ }
6643
+ }
6368
6644
  /**
6369
6645
  * A base is one request that answers with the character id; a state is
6370
6646
  * the same with the parent's id; an animation names itself after the
@@ -6458,6 +6734,78 @@ var PixelLabProvider = class _PixelLabProvider {
6458
6734
  metadata: { character: { kind: "animation", characterId: parentId, animationName: name, direction: animation.direction, mode: animation.mode, backgroundJobIds: res.background_job_ids } }
6459
6735
  };
6460
6736
  }
6737
+ /**
6738
+ * Same shape as `submitCharacter`, minus the mode branching (objectPro has
6739
+ * exactly one base-creation path) and minus the client-side "find and
6740
+ * delete the prior take" dance for animation: `/objects/{id}/animations`
6741
+ * has its own `replace_existing` flag, so this always passes it rather
6742
+ * than fetching the object first to check.
6743
+ */
6744
+ async submitObjectPro(spec, styleImages, context) {
6745
+ const object = spec.objectPro;
6746
+ if (object.kind === "base") {
6747
+ const styleImage = styleImages[0];
6748
+ const res2 = await this.client.createObjectProFlash({
6749
+ description: spec.prompt,
6750
+ directions: object.directions,
6751
+ size: spec.width,
6752
+ view: spec.view,
6753
+ seed: spec.seed,
6754
+ reference: object.reference ? readPose(spec, "reference sprite", object.reference) : void 0,
6755
+ styleReference: styleImage ? { base64: styleImage.base64, format: styleImage.format } : void 0,
6756
+ styleReferenceSize: styleImage ? { width: styleImage.width, height: styleImage.height } : void 0,
6757
+ styleTraits: object.styleTraits
6758
+ });
6759
+ return { jobId: res2.object_id, metadata: { objectPro: { kind: "base", objectId: res2.object_id, directions: object.directions, backgroundJobId: res2.background_job_id } } };
6760
+ }
6761
+ const parentId = context?.parentObjectId;
6762
+ if (!parentId) {
6763
+ throw new Error(
6764
+ `${spec.styleId}/${spec.assetId} is a ${object.kind} of ${object.parentAssetId}, which has no PixelLab object id in the lockfile yet; generate the parent first`
6765
+ );
6766
+ }
6767
+ if (object.kind === "state") {
6768
+ const res2 = await this.client.createObjectProState({
6769
+ objectId: parentId,
6770
+ editDescription: spec.prompt,
6771
+ stateName: spec.assetId.slice(0, 50),
6772
+ seed: spec.seed
6773
+ });
6774
+ return { jobId: res2.object_id, metadata: { objectPro: { kind: "state", objectId: res2.object_id, parentObjectId: parentId, directions: object.directions, backgroundJobId: res2.background_job_id } } };
6775
+ }
6776
+ const animation = object.animation;
6777
+ const name = objectAnimationName(spec);
6778
+ const res = await this.client.animateObject({
6779
+ objectId: parentId,
6780
+ directions: object.directions,
6781
+ direction: animation.direction,
6782
+ animationDescription: spec.prompt.trim() || void 0,
6783
+ displayName: name,
6784
+ mode: animation.mode === "pro" ? "pro" : "v3",
6785
+ frameCount: animation.frames,
6786
+ keepFirstFrame: animation.keepFirstFrame,
6787
+ startFrame: animation.startFrame ? readPose(spec, "start frame", animation.startFrame) : void 0,
6788
+ endFrame: animation.endFrame ? readPose(spec, "end frame", animation.endFrame) : void 0,
6789
+ enhancePrompt: animation.enhancePrompt,
6790
+ replaceExisting: true
6791
+ });
6792
+ const submission = res.submissions.find((s) => s.direction === animation.direction) ?? res.submissions[0];
6793
+ const job = { objectId: parentId, name, direction: animation.direction, jobId: submission?.background_job_id ?? null };
6794
+ return {
6795
+ jobId: encodeObjectAnimationJob(job),
6796
+ metadata: {
6797
+ objectPro: {
6798
+ kind: "animation",
6799
+ objectId: parentId,
6800
+ animationName: name,
6801
+ direction: animation.direction,
6802
+ mode: animation.mode,
6803
+ animationGroupId: res.animation_group_id,
6804
+ animationId: submission?.animation_id ?? null
6805
+ }
6806
+ }
6807
+ };
6808
+ }
6461
6809
  async pollCharacter(jobId, context) {
6462
6810
  const animation = decodeAnimationJob(jobId);
6463
6811
  if (animation) return this.pollCharacterAnimation(animation, context);
@@ -6541,10 +6889,93 @@ var PixelLabProvider = class _PixelLabProvider {
6541
6889
  if (found) return review(found.frames, found.animationId, found.groupId, null);
6542
6890
  return cleanedUp ? { status: "failed", error: `animation job is gone upstream and no "${job.name}" ${job.direction} animation exists on the character` } : { status: "processing" };
6543
6891
  }
6892
+ async pollObjectPro(jobId, context) {
6893
+ const animation = decodeObjectAnimationJob(jobId);
6894
+ if (animation) return this.pollObjectProAnimation(animation, context);
6895
+ const object = await this.client.getObject(jobId);
6896
+ if (object.status === "failed") return { status: "failed", error: "object generation failed upstream" };
6897
+ if (object.status !== "completed" || !object.rotation_urls) return { status: "processing" };
6898
+ const sources = objectRotationSources(object);
6899
+ if (!sources.length) return { status: "failed", error: "object completed with no rotation images" };
6900
+ const backgroundJobId = context?.metadata?.objectPro?.backgroundJobId;
6901
+ return {
6902
+ status: "ready",
6903
+ objectId: object.id,
6904
+ sourceUrl: sources[0].url,
6905
+ sources,
6906
+ metadata: {
6907
+ objectPro: {
6908
+ objectId: object.id,
6909
+ groupId: object.group_id ?? null,
6910
+ name: object.name,
6911
+ stateName: object.state_name ?? null,
6912
+ directions: object.directions,
6913
+ size: object.size
6914
+ }
6915
+ },
6916
+ billed: await this.billedForJob(backgroundJobId)
6917
+ };
6918
+ }
6919
+ /**
6920
+ * Same resilience shape as `pollCharacterAnimation`: the background job is
6921
+ * the authoritative record while it lasts, the object's own `animations`
6922
+ * list (matched by the `pixelkiln:` display name this asset submitted, or
6923
+ * a recorded animation id) is the fallback once PixelLab cleans the job
6924
+ * up. Never declares failure from absence alone -- a `rate_limited`
6925
+ * `DirectionSubmission` has no background job id at all, so this keeps
6926
+ * reporting "processing" rather than guessing a job that never existed
6927
+ * has failed.
6928
+ */
6929
+ async pollObjectProAnimation(job, context) {
6930
+ const fps = context?.spec?.objectPro?.animation?.fps ?? 8;
6931
+ const review = (frames, animationId, groupId, billed) => ({
6932
+ status: "review-set",
6933
+ objectId: `${job.objectId}#${groupId ?? animationId ?? job.name}`,
6934
+ frameUrls: frames,
6935
+ sources: frames.map((url, index) => ({ url, role: `frame-${String(index).padStart(2, "0")}` })),
6936
+ fps,
6937
+ metadata: {
6938
+ frameSet: { fps, count: frames.length },
6939
+ objectPro: {
6940
+ kind: "animation",
6941
+ objectId: job.objectId,
6942
+ animationId,
6943
+ animationGroupId: groupId,
6944
+ animationName: job.name,
6945
+ direction: job.direction
6946
+ }
6947
+ },
6948
+ billed
6949
+ });
6950
+ if (job.jobId) {
6951
+ const status = await this.client.getBackgroundJob(job.jobId).catch((err) => {
6952
+ if (err instanceof PixelLabError && err.status === 404) return null;
6953
+ throw err;
6954
+ });
6955
+ if (status) {
6956
+ if (status.status === "failed") return { status: "failed", error: "animation generation failed upstream" };
6957
+ if (status.status !== "completed") return { status: "processing" };
6958
+ const done = status.last_response ?? {};
6959
+ const frames = done.storage_urls?.frames;
6960
+ const animationId = typeof done.animation_id === "string" ? done.animation_id : null;
6961
+ if (Array.isArray(frames) && frames.length && frames.every((f) => typeof f === "string")) {
6962
+ const object2 = await this.client.getObject(job.objectId);
6963
+ const group = findObjectAnimation(object2, job.name, job.direction, animationId);
6964
+ return review(frames, animationId, group?.groupId ?? null, billedFromUsage(status.usage));
6965
+ }
6966
+ }
6967
+ }
6968
+ const recorded = context?.metadata?.objectPro?.animationId ?? null;
6969
+ const object = await this.client.getObject(job.objectId);
6970
+ const found = findObjectAnimation(object, job.name, job.direction, recorded);
6971
+ if (found) return review(found.frames, found.animationId, found.groupId, null);
6972
+ return { status: "processing" };
6973
+ }
6544
6974
  async submit(spec, styleImages, context) {
6545
6975
  this.validate(spec, styleImages);
6546
6976
  if (spec.revision) return this.submitRevision(spec);
6547
6977
  if (spec.generator === "character") return this.submitCharacter(spec, styleImages, context);
6978
+ if (spec.generator === "objectPro") return this.submitObjectPro(spec, styleImages, context);
6548
6979
  if (spec.generator === "pixflux") {
6549
6980
  const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
6550
6981
  const { png } = await this.client.createImagePixflux({
@@ -6738,6 +7169,7 @@ var PixelLabProvider = class _PixelLabProvider {
6738
7169
  if (generator === "isometricTile") return this.pollIsometricTile(jobId, context);
6739
7170
  if (generator === "imagePro") return this.pollImagePro(jobId);
6740
7171
  if (generator === "character") return this.pollCharacter(jobId, context);
7172
+ if (generator === "objectPro") return this.pollObjectPro(jobId, context);
6741
7173
  const backgroundJobId = context?.metadata?.backgroundJobId;
6742
7174
  const obj = await this.client.getObject(jobId);
6743
7175
  if (obj.status === "review") {
@@ -7155,6 +7587,28 @@ function findAnimation(character, name, direction, animationId) {
7155
7587
  }
7156
7588
  return null;
7157
7589
  }
7590
+ function objectRotationSources(object) {
7591
+ const order = object.directions === 1 ? ["south"] : CHARACTER_DIRECTIONS_8;
7592
+ const sources = [];
7593
+ for (const direction of order) {
7594
+ const url = object.rotation_urls?.[direction];
7595
+ if (url) sources.push({ url, role: direction });
7596
+ }
7597
+ return sources;
7598
+ }
7599
+ function findObjectAnimation(object, name, direction, animationId) {
7600
+ for (const group of object.animations) {
7601
+ const match = group.directions.find((d) => d.direction === direction);
7602
+ const frames = match ? match.storage_urls.frames : void 0;
7603
+ if (!match || !Array.isArray(frames) || !frames.length || !frames.every((f) => typeof f === "string")) continue;
7604
+ const byName = group.display_name === name;
7605
+ const byId = animationId !== null && frames.some((url) => url.includes(`/animations/${animationId}/`));
7606
+ if (!byName && !byId) continue;
7607
+ const fromUrl = /\/animations\/([^/]+)\//.exec(frames[0])?.[1] ?? null;
7608
+ return { frames, groupId: group.animation_group_id, animationId: animationId ?? fromUrl };
7609
+ }
7610
+ return null;
7611
+ }
7158
7612
 
7159
7613
  // src/providers/retrodiffusion.ts
7160
7614
  var DEFAULT_BASE_URL2 = "https://api.retrodiffusion.ai/v1";
@@ -8334,14 +8788,18 @@ async function resolveSpecs(loaded, filter) {
8334
8788
  size = asset.size ?? style.size ?? 32;
8335
8789
  width = size;
8336
8790
  height = size;
8791
+ } else if (generator === "objectPro") {
8792
+ size = asset.size ?? style.size ?? 64;
8793
+ width = size;
8794
+ height = size;
8337
8795
  } else {
8338
8796
  width = asset.width ?? style.size ?? 64;
8339
8797
  height = asset.height ?? style.size ?? 64;
8340
8798
  size = Math.max(width, height);
8341
8799
  }
8342
- if ((asset.state || asset.animation) && generator !== "character") {
8800
+ if ((asset.state || asset.animation) && generator !== "character" && generator !== "objectPro") {
8343
8801
  throw new Error(
8344
- `assets.${assetId}: ${asset.state ? "state" : "animation"} needs a character style; "${styleId}" generates ${generator}`
8802
+ `assets.${assetId}: ${asset.state ? "state" : "animation"} needs a character or objectPro style; "${styleId}" generates ${generator}`
8345
8803
  );
8346
8804
  }
8347
8805
  const characterKind = asset.animation ? "animation" : asset.state ? "state" : "base";
@@ -8362,7 +8820,7 @@ async function resolveSpecs(loaded, filter) {
8362
8820
  terrainUpperDescription = wrap(parsed.upper);
8363
8821
  terrainTransitionDescription = parsed.transition ? wrap(parsed.transition) : void 0;
8364
8822
  prompt = subject.trim();
8365
- } else if (generator === "character" && characterKind !== "base") {
8823
+ } else if ((generator === "character" || generator === "objectPro") && characterKind !== "base") {
8366
8824
  prompt = subject.trim();
8367
8825
  } else {
8368
8826
  prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
@@ -8430,6 +8888,7 @@ async function resolveSpecs(loaded, filter) {
8430
8888
  isometricTileSize: generator === "isometricTile" ? style.isometricTileSize : void 0,
8431
8889
  isometricTileShape: generator === "isometricTile" ? style.isometricTileShape : void 0,
8432
8890
  ...generator === "character" ? { character: await resolveCharacterShape(asset, style, characterKind, { root, load: loadStyleImage }) } : {},
8891
+ ...generator === "objectPro" ? { objectPro: await resolveObjectProShape(assetId, asset, style, characterKind, { root, load: loadStyleImage }) } : {},
8433
8892
  cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? tilesCost(size, terrainTiles) : generator === "isometricTile" ? isometricTileCost() : generationCost(width, height, generator),
8434
8893
  costUnit: "generations",
8435
8894
  candidates: generator === "tiles" ? tileVariations : generator === "terrain" ? terrainTiles : generator === "1dir" || generator === "imagePro" ? candidateCount(size) : 1
@@ -8495,6 +8954,18 @@ async function resolveSpecs(loaded, filter) {
8495
8954
  } else if (sourceSpec.character) {
8496
8955
  resolved.character = { ...sourceSpec.character };
8497
8956
  }
8957
+ if (sourceSpec.objectPro?.kind === "animation") {
8958
+ const facing = sourceSpec.objectPro.animation.direction;
8959
+ const mirrored = MIRRORED_DIRECTION[facing];
8960
+ if (mirrored === facing) {
8961
+ throw new Error(
8962
+ `assets.${assetId}: mirroring ${asset.mirror} gives another ${facing}-facing loop; mirror a loop facing east, west, or a diagonal`
8963
+ );
8964
+ }
8965
+ resolved.objectPro = { ...sourceSpec.objectPro, animation: { ...sourceSpec.objectPro.animation, direction: mirrored } };
8966
+ } else if (sourceSpec.objectPro) {
8967
+ resolved.objectPro = { ...sourceSpec.objectPro };
8968
+ }
8498
8969
  resolved.mirror = { sourceAssetId: asset.mirror, sourceSpec };
8499
8970
  resolved.generator = sourceSpec.generator;
8500
8971
  resolved.width = sourceSpec.width;
@@ -8536,6 +9007,23 @@ async function resolveSpecs(loaded, filter) {
8536
9007
  directions: resolved.character.kind === "base" ? resolved.character.directions : parentSpec.character?.directions ?? resolved.character.directions
8537
9008
  };
8538
9009
  }
9010
+ if (resolved.objectPro && characterParent) {
9011
+ const parentSpec = await finalize(characterParent);
9012
+ if (!parentSpec.objectPro) {
9013
+ throw new Error(`assets.${assetId}: ${characterParent} is not an objectPro asset`);
9014
+ }
9015
+ const parentDirections = parentSpec.objectPro.directions;
9016
+ const parentFile = memberPath(parentSpec.outFile, "south", 0, parentDirections, MediaType.PNG);
9017
+ resolved.objectPro = {
9018
+ ...resolved.objectPro,
9019
+ parentAssetId: characterParent,
9020
+ parentSpec,
9021
+ parentFile,
9022
+ parentSha256: existsSync8(parentFile) ? await sha256File(parentFile) : null,
9023
+ // A state and its animations keep the base's own rotation count.
9024
+ directions: resolved.objectPro.kind === "base" ? resolved.objectPro.directions : parentSpec.objectPro.directions
9025
+ };
9026
+ }
8539
9027
  if (asset.revision) {
8540
9028
  if (!activeProvider.supportsRevision?.(asset.revision.mode)) {
8541
9029
  throw new Error(
@@ -8718,6 +9206,60 @@ async function resolveCharacterShape(asset, style, kind, files) {
8718
9206
  }
8719
9207
  return shape;
8720
9208
  }
9209
+ async function resolveObjectProShape(assetId, asset, style, kind, files) {
9210
+ const directions = style.objectDirections ?? 8;
9211
+ const shape = {
9212
+ kind,
9213
+ directions,
9214
+ ...style.styleTraits ? { styleTraits: style.styleTraits } : {}
9215
+ };
9216
+ if (asset.reference) {
9217
+ const south = typeof asset.reference === "string" ? asset.reference : asset.reference.south;
9218
+ if (south) {
9219
+ const image = await files.load(south, "Reference sprite");
9220
+ shape.reference = { path: path13.resolve(files.root, south), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9221
+ }
9222
+ }
9223
+ if (asset.state) {
9224
+ shape.state = {
9225
+ paletteFromReference: asset.state.paletteFromReference,
9226
+ ...asset.state.canvas ? { canvas: asset.state.canvas } : {}
9227
+ };
9228
+ }
9229
+ if (asset.animation) {
9230
+ for (const [field, value] of Object.entries({
9231
+ template: asset.animation.template,
9232
+ subject: asset.animation.subject,
9233
+ outline: asset.animation.outline,
9234
+ shading: asset.animation.shading,
9235
+ detail: asset.animation.detail
9236
+ })) {
9237
+ if (value !== void 0) {
9238
+ throw new Error(
9239
+ `assets.${assetId}: objectPro has no skeleton/template concept, so animation.${field} does not apply; describe the motion in the asset's own prompt instead`
9240
+ );
9241
+ }
9242
+ }
9243
+ if (asset.animation.mode === "template") {
9244
+ throw new Error(`assets.${assetId}: objectPro animations have no "template" mode; use "v3" or "pro"`);
9245
+ }
9246
+ const pose = async (rel, what) => {
9247
+ const image = await files.load(rel, what);
9248
+ return { path: path13.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9249
+ };
9250
+ shape.animation = {
9251
+ mode: asset.animation.mode ?? "v3",
9252
+ direction: asset.animation.direction,
9253
+ frames: asset.animation.frames ?? 8,
9254
+ fps: asset.animation.fps,
9255
+ keepFirstFrame: asset.animation.keepFirstFrame,
9256
+ ...asset.animation.startFrame ? { startFrame: await pose(asset.animation.startFrame, "Animation start frame") } : {},
9257
+ ...asset.animation.endFrame ? { endFrame: await pose(asset.animation.endFrame, "Animation end frame") } : {},
9258
+ ...asset.animation.enhancePrompt !== void 0 ? { enhancePrompt: asset.animation.enhancePrompt } : {}
9259
+ };
9260
+ }
9261
+ return shape;
9262
+ }
8721
9263
 
8722
9264
  // src/pipeline/fetch.ts
8723
9265
  import { existsSync as existsSync9, readFileSync as readFileSync3 } from "fs";
@@ -10884,6 +11426,25 @@ var AssetPatchSchema = z5.object({
10884
11426
  size: z5.number().int().nullable().optional(),
10885
11427
  tags: z5.array(z5.string()).optional()
10886
11428
  }).strict();
11429
+ var NewStateSchema = z5.object({
11430
+ /** Asset id of the parent this states; must already exist. */
11431
+ of: z5.string().min(1),
11432
+ paletteFromReference: z5.boolean().optional(),
11433
+ canvas: z5.object({ width: z5.number().int().min(16).max(256), height: z5.number().int().min(16).max(256) }).strict().optional()
11434
+ }).strict();
11435
+ var NewAnimationSchema = z5.object({
11436
+ /** Asset id of the parent this animates; must already exist. */
11437
+ of: z5.string().min(1),
11438
+ /** A PixelLab template id (`character` only; `objectPro` has no template concept). */
11439
+ template: z5.string().min(1).optional(),
11440
+ direction: CharacterDirectionSchema.optional(),
11441
+ frames: z5.number().int().min(4).max(16).optional(),
11442
+ fps: z5.number().int().min(1).max(60).optional(),
11443
+ mode: CharacterAnimationModeSchema.optional(),
11444
+ /** What is being animated, when the parent's own description would mislead the model (`character` only). */
11445
+ subject: z5.string().min(1).optional(),
11446
+ enhancePrompt: z5.boolean().optional()
11447
+ }).strict();
10887
11448
  var NewAssetSchema = z5.object({
10888
11449
  prompt: z5.string(),
10889
11450
  category: z5.string().optional(),
@@ -10904,8 +11465,16 @@ var NewAssetSchema = z5.object({
10904
11465
  /** Asset id of the parent this revises; must already exist. */
10905
11466
  from: z5.string().min(1),
10906
11467
  strength: z5.number().min(0).max(1).optional()
10907
- }).strict().optional()
10908
- }).strict();
11468
+ }).strict().optional(),
11469
+ /** A pose or outfit of an existing `character`/`objectPro` base or state. */
11470
+ state: NewStateSchema.optional(),
11471
+ /** A loop of an existing `character`/`objectPro` base or state. */
11472
+ animation: NewAnimationSchema.optional()
11473
+ }).strict().refine((asset) => !(asset.revision && (asset.state || asset.animation)), {
11474
+ message: "an asset is a revision, a state, or an animation \u2014 not more than one"
11475
+ }).refine((asset) => !(asset.state && asset.animation), {
11476
+ message: "an asset is a state or an animation \u2014 not both"
11477
+ });
10909
11478
  var CANDIDATE_OPTION = {
10910
11479
  retrodiffusion: "numImages",
10911
11480
  comfyui: "numImages",
@@ -11073,6 +11642,12 @@ function applyEdit(raw, edit) {
11073
11642
  if (edit.asset.revision && !Object.hasOwn(raw.assets, edit.asset.revision.from)) {
11074
11643
  throw new ManifestEditError(`revision parent "${edit.asset.revision.from}" is not declared by the manifest`);
11075
11644
  }
11645
+ if (edit.asset.state && !Object.hasOwn(raw.assets, edit.asset.state.of)) {
11646
+ throw new ManifestEditError(`state parent "${edit.asset.state.of}" is not declared by the manifest`);
11647
+ }
11648
+ if (edit.asset.animation && !Object.hasOwn(raw.assets, edit.asset.animation.of)) {
11649
+ throw new ManifestEditError(`animation parent "${edit.asset.animation.of}" is not declared by the manifest`);
11650
+ }
11076
11651
  raw.assets[edit.assetId] = asset2;
11077
11652
  return;
11078
11653
  }
@@ -11723,27 +12298,54 @@ async function samePixels(a, b) {
11723
12298
  }
11724
12299
  var metadataFps = (entry) => entry ? frameSetFps(entry) : null;
11725
12300
  function describeCharacter(spec, entry) {
11726
- const recorded = entry?.providerMetadata?.[entry.provider]?.character;
12301
+ const recordedCharacter = entry?.providerMetadata?.[entry.provider]?.character;
11727
12302
  if (spec?.character) {
11728
12303
  const character = spec.character;
11729
12304
  return {
12305
+ generator: "character",
11730
12306
  kind: character.kind,
11731
12307
  parentKey: character.parentAssetId ? lockKey(spec.styleId, character.parentAssetId) : null,
11732
12308
  mode: character.kind === "animation" ? character.animation.mode : character.mode,
11733
12309
  directions: character.directions,
11734
12310
  direction: character.animation?.direction ?? null,
11735
- characterId: recorded?.characterId ?? entry?.objectId?.split("#")[0] ?? null
12311
+ characterId: recordedCharacter?.characterId ?? entry?.objectId?.split("#")[0] ?? null
12312
+ };
12313
+ }
12314
+ if (spec?.objectPro) {
12315
+ const objectPro = spec.objectPro;
12316
+ return {
12317
+ generator: "objectPro",
12318
+ kind: objectPro.kind,
12319
+ parentKey: objectPro.parentAssetId ? lockKey(spec.styleId, objectPro.parentAssetId) : null,
12320
+ mode: objectPro.kind === "animation" ? objectPro.animation.mode : "pro-flash",
12321
+ directions: objectPro.directions,
12322
+ direction: objectPro.animation?.direction ?? null,
12323
+ characterId: entry?.objectId?.split("#")[0] ?? null
11736
12324
  };
11737
12325
  }
11738
12326
  if (entry?.generator === "character") {
11739
- const kind = recorded?.kind === "animation" || entry.outputs.some((o) => o.role?.startsWith("frame-")) ? "animation" : "base";
12327
+ const kind = recordedCharacter?.kind === "animation" || entry.outputs.some((o) => o.role?.startsWith("frame-")) ? "animation" : "base";
12328
+ return {
12329
+ generator: "character",
12330
+ kind,
12331
+ parentKey: null,
12332
+ mode: recordedCharacter?.mode ?? "standard",
12333
+ directions: recordedCharacter?.directions ?? entry.outputs.length,
12334
+ direction: recordedCharacter?.direction ?? null,
12335
+ characterId: recordedCharacter?.characterId ?? entry.objectId?.split("#")[0] ?? null
12336
+ };
12337
+ }
12338
+ if (entry?.generator === "objectPro") {
12339
+ const recordedObject = entry.providerMetadata?.[entry.provider]?.objectPro;
12340
+ const kind = recordedObject?.kind === "animation" || entry.outputs.some((o) => o.role?.startsWith("frame-")) ? "animation" : "base";
11740
12341
  return {
12342
+ generator: "objectPro",
11741
12343
  kind,
11742
12344
  parentKey: null,
11743
- mode: recorded?.mode ?? "standard",
11744
- directions: recorded?.directions ?? entry.outputs.length,
11745
- direction: recorded?.direction ?? null,
11746
- characterId: recorded?.characterId ?? entry.objectId?.split("#")[0] ?? null
12345
+ mode: "pro-flash",
12346
+ directions: recordedObject?.directions ?? entry.outputs.length,
12347
+ direction: recordedObject?.direction ?? null,
12348
+ characterId: entry.objectId?.split("#")[0] ?? null
11747
12349
  };
11748
12350
  }
11749
12351
  return null;
@@ -12079,7 +12681,7 @@ async function buildGallerySnapshot(opts) {
12079
12681
  view: style?.view ?? null,
12080
12682
  noBackground: style?.noBackground ?? true,
12081
12683
  quality: Boolean(style?.quality),
12082
- characters: style?.generator === "character" ? {
12684
+ characters: style?.generator === "character" || style?.generator === "objectPro" ? {
12083
12685
  bases: styleItems.filter((item) => item.character?.kind === "base").length,
12084
12686
  states: styleItems.filter((item) => item.character?.kind === "state").length,
12085
12687
  animations: styleItems.filter((item) => item.character?.kind === "animation").length
@@ -12840,7 +13442,8 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
12840
13442
  lastSubmitAt = Date.now();
12841
13443
  try {
12842
13444
  const refs = styleImages.get(spec.styleId) ?? [];
12843
- const parentEntry = spec.character?.parentSpec ? lock.entries[lockKey(spec.character.parentSpec.styleId, spec.character.parentSpec.assetId)] : void 0;
13445
+ const parentSpec = spec.character?.parentSpec ?? spec.objectPro?.parentSpec;
13446
+ const parentEntry = parentSpec ? lock.entries[lockKey(parentSpec.styleId, parentSpec.assetId)] : void 0;
12844
13447
  const anchor = spec.character?.styleAnchor;
12845
13448
  const anchorEntry = anchor ? lock.entries[lockKey(anchor.spec.styleId, anchor.spec.assetId)] : void 0;
12846
13449
  const replacedMetadata = previousEntry?.providerMetadata?.[provider.id];