pixelkiln 0.43.0 → 0.45.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
@@ -665,12 +665,20 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
665
665
  parent: spec.character.parentAssetId,
666
666
  parentSha256: spec.character.parentSha256 ?? null,
667
667
  state: spec.character.state,
668
- animation: spec.character.animation,
668
+ // Pose images enter as their hashes, never their paths.
669
+ animation: spec.character.animation ? {
670
+ ...spec.character.animation,
671
+ startFrame: spec.character.animation.startFrame?.sha256,
672
+ endFrame: spec.character.animation.endFrame?.sha256
673
+ } : void 0,
669
674
  // Absent keys keep the hashes of manifests that never set these.
670
675
  proportions: spec.character.proportions,
671
676
  textGuidanceScale: spec.character.textGuidanceScale,
672
677
  isometric: spec.character.isometric,
673
- reference: spec.character.reference ? Object.fromEntries(Object.entries(spec.character.reference).map(([direction, image]) => [direction, image.sha256])) : void 0
678
+ enhancePrompt: spec.character.enhancePrompt,
679
+ reference: spec.character.reference ? Object.fromEntries(Object.entries(spec.character.reference).map(([direction, image]) => [direction, image.sha256])) : void 0,
680
+ concept: spec.character.concept?.sha256,
681
+ styleAnchor: spec.character.styleAnchor ? { of: spec.character.styleAnchor.assetId, sha256: spec.character.styleAnchor.sha256 } : void 0
674
682
  } : void 0,
675
683
  // A mirror's bytes come from its source's recorded outputs; the plan
676
684
  // compares those directly, so only the choice of source is identity.
@@ -825,11 +833,45 @@ var CharacterAnimationSchema = z.object({
825
833
  /** v3 only: keep the resting pose as frame 0, so `frames` generated frames land as `frames + 1` files. */
826
834
  keepFirstFrame: z.boolean().default(true),
827
835
  /** `template` when a template is named, otherwise `v3`; `pro` for the sequential high-quality engine. */
828
- mode: CharacterAnimationModeSchema.optional()
836
+ mode: CharacterAnimationModeSchema.optional(),
837
+ /**
838
+ * v3 only: a manifest-relative image of the pose to start from, instead
839
+ * of the character's rotation for this direction. Up to 256px.
840
+ */
841
+ startFrame: z.string().min(1).optional(),
842
+ /**
843
+ * v3 only: a pose to animate toward. The loop then interpolates from the
844
+ * start frame (the rotation, or `startFrame`) to this image, which must
845
+ * be the same size as the start frame.
846
+ */
847
+ endFrame: z.string().min(1).optional(),
848
+ /** What is being animated, when the character's own description would mislead the model. */
849
+ subject: z.string().min(1).optional(),
850
+ /** Template mode only: style hints for this loop, over the character's own. */
851
+ outline: z.string().min(1).optional(),
852
+ shading: z.string().min(1).optional(),
853
+ detail: z.string().min(1).optional(),
854
+ /** v3 text loops only: let PixelLab expand the action into a fuller motion description first. */
855
+ enhancePrompt: z.boolean().optional()
829
856
  }).strict().superRefine((animation, context) => {
830
857
  if (animation.frames !== void 0 && animation.frames % 2 !== 0) {
831
858
  context.addIssue({ code: z.ZodIssueCode.custom, message: "frames must be even", path: ["frames"] });
832
859
  }
860
+ const mode2 = animation.mode ?? (animation.template ? "template" : "v3");
861
+ for (const key of ["startFrame", "endFrame", "enhancePrompt"]) {
862
+ if (animation[key] !== void 0 && mode2 !== "v3") {
863
+ context.addIssue({
864
+ code: z.ZodIssueCode.custom,
865
+ message: `${key} is for v3 loops; a ${mode2} loop ${mode2 === "template" ? "starts from the character's rotation and follows its template" : "takes neither"}`,
866
+ path: [key]
867
+ });
868
+ }
869
+ }
870
+ for (const key of ["outline", "shading", "detail"]) {
871
+ if (animation[key] !== void 0 && mode2 !== "template") {
872
+ context.addIssue({ code: z.ZodIssueCode.custom, message: `${key} overrides apply to template loops only`, path: [key] });
873
+ }
874
+ }
833
875
  if (animation.template && animation.mode && animation.mode !== "template") {
834
876
  context.addIssue({
835
877
  code: z.ZodIssueCode.custom,
@@ -967,6 +1009,14 @@ var StyleObjectSchema = z.object({
967
1009
  textGuidanceScale: z.number().min(1).max(20).optional(),
968
1010
  /** `character` only. Draw `standard` bases and every loop in isometric view. */
969
1011
  isometric: z.boolean().optional(),
1012
+ /** `character` only, `v3` bases: let PixelLab expand the prompt into a fuller one before drawing. */
1013
+ enhancePrompt: z.boolean().optional(),
1014
+ /**
1015
+ * `character` only, `pro` bases: the asset id of a generated 8-direction
1016
+ * character in this style whose look every base drawn from text or a
1017
+ * concept follows. An asset may override it; the anchor itself ignores it.
1018
+ */
1019
+ styleCharacter: z.string().min(1).optional(),
970
1020
  /** Fixed seed for reproducibility where the endpoint supports it. */
971
1021
  seed: z.number().int().optional(),
972
1022
  /**
@@ -1139,6 +1189,15 @@ var AssetSchema = z.object({
1139
1189
  * still guides the result.
1140
1190
  */
1141
1191
  reference: z.union([z.string().min(1), z.record(CharacterDirectionSchema, z.string().min(1))]).optional(),
1192
+ /**
1193
+ * `character` styles, `pro` bases only: a manifest-relative concept
1194
+ * image (a painting, a photo, a sketch; up to 1024px) that seeds the
1195
+ * design instead of the prompt alone. A style image or `styleCharacter`
1196
+ * may still anchor the look.
1197
+ */
1198
+ concept: z.string().min(1).optional(),
1199
+ /** `character` styles, `pro` bases: this base's style anchor, over the style's. */
1200
+ styleCharacter: z.string().min(1).optional(),
1142
1201
  /**
1143
1202
  * Another asset of the same style flipped left to right, made locally
1144
1203
  * from that asset's downloaded files at no generation cost.
@@ -1220,6 +1279,20 @@ var AssetSchema = z.object({
1220
1279
  path: ["reference"]
1221
1280
  });
1222
1281
  }
1282
+ if (asset.concept && (asset.state || asset.animation || asset.mirror)) {
1283
+ context.addIssue({
1284
+ code: z.ZodIssueCode.custom,
1285
+ message: "a concept image belongs on a base; a state, animation, or mirror takes its look from its parent",
1286
+ path: ["concept"]
1287
+ });
1288
+ }
1289
+ if (asset.concept && asset.reference) {
1290
+ context.addIssue({
1291
+ code: z.ZodIssueCode.custom,
1292
+ message: "concept and reference are mutually exclusive: a concept seeds a new design, a reference is the sprite to rotate",
1293
+ path: ["concept"]
1294
+ });
1295
+ }
1223
1296
  if (typeof asset.reference === "object" && !Object.keys(asset.reference).length) {
1224
1297
  context.addIssue({ code: z.ZodIssueCode.custom, message: "a reference needs at least a south image", path: ["reference"] });
1225
1298
  }
@@ -2014,6 +2087,34 @@ function skipSubBlocks(bytes, start) {
2014
2087
  function cacheFileName(hash, mediaType = MediaType.PNG) {
2015
2088
  return `${hash}${mediaExtension(mediaType)}`;
2016
2089
  }
2090
+ function imageMetadata(buf) {
2091
+ if (buf.length >= 24 && buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834 && buf.toString("ascii", 12, 16) === "IHDR") {
2092
+ return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20), format: "png" };
2093
+ }
2094
+ if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216) return null;
2095
+ let offset = 2;
2096
+ while (offset + 3 < buf.length) {
2097
+ if (buf[offset] !== 255) return null;
2098
+ while (buf[offset] === 255) offset++;
2099
+ const marker = buf[offset++];
2100
+ if (marker == null || marker === 217 || marker === 218) break;
2101
+ if (marker === 1 || marker >= 208 && marker <= 215) continue;
2102
+ if (offset + 2 > buf.length) return null;
2103
+ const length = buf.readUInt16BE(offset);
2104
+ if (length < 2 || offset + length > buf.length) return null;
2105
+ const isStartOfFrame = marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker);
2106
+ if (isStartOfFrame) {
2107
+ if (length < 7) return null;
2108
+ return {
2109
+ width: buf.readUInt16BE(offset + 5),
2110
+ height: buf.readUInt16BE(offset + 3),
2111
+ format: "jpeg"
2112
+ };
2113
+ }
2114
+ offset += length;
2115
+ }
2116
+ return null;
2117
+ }
2017
2118
 
2018
2119
  // src/outputs.ts
2019
2120
  import { existsSync as existsSync2 } from "fs";
@@ -3559,9 +3660,24 @@ import path8 from "path";
3559
3660
  async function inspectRevisionReadiness(spec, lock) {
3560
3661
  if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
3561
3662
  if (spec.character?.parentSpec) return inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
3663
+ if (spec.character?.styleAnchor) return inspectStyleAnchor(spec, lock, /* @__PURE__ */ new Set());
3562
3664
  if (!spec.revision) return null;
3563
3665
  return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
3564
3666
  }
3667
+ async function inspectStyleAnchor(spec, lock, seen) {
3668
+ const anchor = spec.character.styleAnchor;
3669
+ const label = `style anchor ${spec.styleId}/${anchor.assetId}`;
3670
+ const dependency = await inspectParent(anchor.spec, lock, seen);
3671
+ if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
3672
+ if (!anchor.sha256 || !existsSync4(anchor.file)) return { ready: false, reason: `${label} has no generated south-facing file yet` };
3673
+ if (await sha256File(anchor.file) !== anchor.sha256) {
3674
+ return { ready: false, reason: `${label} changed after the manifest was resolved` };
3675
+ }
3676
+ if (!lock.entries[lockKey(anchor.spec.styleId, anchor.spec.assetId)]?.objectId) {
3677
+ return { ready: false, reason: `${label} has no PixelLab character id recorded` };
3678
+ }
3679
+ return { ready: true, reason: `${label} is current` };
3680
+ }
3565
3681
  async function inspectMirror(spec, lock, seen) {
3566
3682
  const source = spec.mirror.sourceSpec;
3567
3683
  const label = `mirror source ${spec.styleId}/${spec.mirror.sourceAssetId}`;
@@ -3633,6 +3749,10 @@ async function inspectParent(spec, lock, seen) {
3633
3749
  const inputs = await inspectCharacterParent(spec, lock, nextSeen);
3634
3750
  if (!inputs.ready) return inputs;
3635
3751
  }
3752
+ if (spec.character?.styleAnchor) {
3753
+ const inputs = await inspectStyleAnchor(spec, lock, nextSeen);
3754
+ if (!inputs.ready) return inputs;
3755
+ }
3636
3756
  if (spec.quality) {
3637
3757
  const quality = await inspectQualityProfile(spec, lock);
3638
3758
  return quality?.state === "approved" ? { ready: true, reason: "approved quality output is current" } : {
@@ -3672,6 +3792,14 @@ async function anyOutputModified(entry, spec) {
3672
3792
  }
3673
3793
  return false;
3674
3794
  }
3795
+ function dependencies(spec) {
3796
+ return [
3797
+ spec.mirror?.sourceSpec,
3798
+ spec.revision?.sourceSpec,
3799
+ spec.character?.parentSpec,
3800
+ spec.character?.styleAnchor?.spec
3801
+ ].filter((parent) => Boolean(parent));
3802
+ }
3675
3803
  function resumeCommandForStatus(status) {
3676
3804
  if (status === "pending" || status === "processing") return "poll";
3677
3805
  if (status === "review") return "pick";
@@ -3780,16 +3908,19 @@ async function buildPlan(specs, lock, opts = {}) {
3780
3908
  const stateOf = new Map(items.map((i) => [i.key, i.state]));
3781
3909
  for (const item of items) {
3782
3910
  if (!isActionable(item)) continue;
3783
- const parent = item.spec.mirror?.sourceSpec ?? item.spec.revision?.sourceSpec ?? item.spec.character?.parentSpec;
3784
- if (!parent) continue;
3785
- const parentKey = lockKey(parent.styleId, parent.assetId);
3786
- const parentState = stateOf.get(parentKey);
3787
- if (parentState && isActionable({ state: parentState })) {
3788
- item.state = "blocked";
3789
- item.reason = `parent ${parentKey} is being generated in this run; this follows in the next wave`;
3790
- } else if (item.spec.mirror && (parentState === "recoverable" || parentState === "in-flight")) {
3791
- item.state = "blocked";
3792
- item.reason = `mirror source ${parentKey} is ${parentState}; this follows in the next wave`;
3911
+ for (const parent of dependencies(item.spec)) {
3912
+ const parentKey = lockKey(parent.styleId, parent.assetId);
3913
+ const parentState = stateOf.get(parentKey);
3914
+ if (parentState && isActionable({ state: parentState })) {
3915
+ item.state = "blocked";
3916
+ item.reason = `parent ${parentKey} is being generated in this run; this follows in the next wave`;
3917
+ break;
3918
+ }
3919
+ if (item.spec.mirror && (parentState === "recoverable" || parentState === "in-flight")) {
3920
+ item.state = "blocked";
3921
+ item.reason = `mirror source ${parentKey} is ${parentState}; this follows in the next wave`;
3922
+ break;
3923
+ }
3793
3924
  }
3794
3925
  }
3795
3926
  const actionable = items.filter(isActionable);
@@ -5308,7 +5439,8 @@ var PixelLabClient = class {
5308
5439
  ...args.outline ? { outline: args.outline } : {},
5309
5440
  ...args.detail ? { detail: args.detail } : {},
5310
5441
  ...args.seed != null ? { seed: args.seed } : {},
5311
- ...south ? { reference_image: encode(south) } : {}
5442
+ ...south ? { reference_image: encode(south) } : {},
5443
+ ...args.enhancePrompt ? { enhance_prompt: true } : {}
5312
5444
  };
5313
5445
  } else if (args.mode === "pro") {
5314
5446
  path45 = "/create-character-pro";
@@ -5317,10 +5449,12 @@ var PixelLabClient = class {
5317
5449
  image_size: imageSize,
5318
5450
  template_id: args.template,
5319
5451
  no_background: args.noBackground ?? true,
5320
- method: south ? "rotate_character" : "create_with_style",
5452
+ method: south ? "rotate_character" : args.concept ? "create_from_concept" : "create_with_style",
5321
5453
  ...args.view ? { view: args.view } : {},
5322
5454
  ...args.seed != null ? { seed: args.seed } : {},
5323
- ...south ? { reference_image: encode(south) } : args.styleReference ? { reference_image: encode(args.styleReference) } : {}
5455
+ ...south ? { reference_image: encode(south) } : args.styleReference ? { reference_image: encode(args.styleReference) } : {},
5456
+ ...!south && args.concept ? { concept_image: encode(args.concept) } : {},
5457
+ ...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
5324
5458
  };
5325
5459
  } else {
5326
5460
  path45 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
@@ -5365,6 +5499,7 @@ var PixelLabClient = class {
5365
5499
  * the action text; pro is the sequential engine. One job per direction.
5366
5500
  */
5367
5501
  async animateCharacter(args) {
5502
+ const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
5368
5503
  const raw = await this.request("/animate-character", {
5369
5504
  method: "POST",
5370
5505
  body: JSON.stringify({
@@ -5374,10 +5509,18 @@ var PixelLabClient = class {
5374
5509
  directions: args.directions,
5375
5510
  ...args.template ? { template_animation_id: args.template } : {},
5376
5511
  ...args.actionDescription ? { action_description: args.actionDescription } : {},
5512
+ ...args.description ? { description: args.description } : {},
5377
5513
  ...args.mode === "v3" && args.frameCount ? { frame_count: args.frameCount } : {},
5378
5514
  ...args.mode === "v3" && args.keepFirstFrame === false ? { keep_first_frame: false } : {},
5515
+ ...args.mode === "v3" && args.startFrame ? { custom_start_frame: encode(args.startFrame) } : {},
5516
+ ...args.mode === "v3" && args.endFrame ? { end_frame: encode(args.endFrame) } : {},
5517
+ ...args.mode === "v3" && args.enhancePrompt ? { enhance_prompt: true } : {},
5379
5518
  ...args.mode === "template" && args.textGuidanceScale !== void 0 ? { text_guidance_scale: args.textGuidanceScale } : {},
5519
+ ...args.mode === "template" && args.outline ? { outline: args.outline } : {},
5520
+ ...args.mode === "template" && args.shading ? { shading: args.shading } : {},
5521
+ ...args.mode === "template" && args.detail ? { detail: args.detail } : {},
5380
5522
  ...args.isometric !== void 0 ? { isometric: args.isometric } : {},
5523
+ ...args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {},
5381
5524
  ...args.seed != null ? { seed: args.seed } : {}
5382
5525
  })
5383
5526
  });
@@ -5603,6 +5746,38 @@ var PixelLabProvider = class _PixelLabProvider {
5603
5746
  throw new Error(`${label}: proportions apply to the mannequin template; ${character.template} is a quadruped`);
5604
5747
  }
5605
5748
  }
5749
+ if (character.kind === "base" && character.enhancePrompt !== void 0 && character.mode !== "v3") {
5750
+ throw new Error(`${label}: enhancePrompt applies to v3 bases; the ${character.mode} engine does not take it`);
5751
+ }
5752
+ if (character.animation) {
5753
+ const { startFrame, endFrame } = character.animation;
5754
+ for (const [name, image] of [["start frame", startFrame], ["end frame", endFrame]]) {
5755
+ if (image && (image.width > 256 || image.height > 256)) {
5756
+ throw new Error(`${label}: ${name} is ${image.width}x${image.height}; PixelLab v3 takes up to 256px`);
5757
+ }
5758
+ }
5759
+ if (endFrame) {
5760
+ const start = startFrame ?? (character.parentFile && existsSync7(character.parentFile) ? imageMetadata(readFileSync2(character.parentFile)) : null);
5761
+ if (start && (start.width !== endFrame.width || start.height !== endFrame.height)) {
5762
+ throw new Error(
5763
+ `${label}: end frame is ${endFrame.width}x${endFrame.height} but the ${startFrame ? "start frame" : "character's rotation"} is ${start.width}x${start.height}; PixelLab interpolates between frames of one size`
5764
+ );
5765
+ }
5766
+ }
5767
+ }
5768
+ if (character.concept) {
5769
+ if (character.mode !== "pro") throw new Error(`${label}: a concept image is a pro input; the ${character.mode} engine draws from text or a reference`);
5770
+ if (character.concept.width > 1024 || character.concept.height > 1024) {
5771
+ throw new Error(`${label}: concept image is ${character.concept.width}x${character.concept.height}; PixelLab pro takes up to 1024px`);
5772
+ }
5773
+ }
5774
+ if (character.styleAnchor) {
5775
+ if (character.mode !== "pro") throw new Error(`${label}: styleCharacter is a pro input; the ${character.mode} engine has no style anchor`);
5776
+ if (character.reference) throw new Error(`${label}: a pro base rotating its own reference takes no styleCharacter`);
5777
+ if (character.styleAnchor.spec.character?.directions !== 8) {
5778
+ throw new Error(`${label}: styleCharacter ${character.styleAnchor.assetId} has ${character.styleAnchor.spec.character?.directions} directions; PixelLab wants an 8-direction anchor`);
5779
+ }
5780
+ }
5606
5781
  if (character.reference) {
5607
5782
  const given = Object.keys(character.reference);
5608
5783
  if (!character.reference.south) throw new Error(`${label}: a reference needs a south-facing sprite`);
@@ -5703,7 +5878,10 @@ var PixelLabProvider = class _PixelLabProvider {
5703
5878
  textGuidanceScale: character.textGuidanceScale,
5704
5879
  isometric: character.isometric,
5705
5880
  reference: character.reference ? readReference(spec, character.reference) : void 0,
5706
- styleReference: styleImage ? { base64: styleImage.base64, format: styleImage.format } : void 0
5881
+ styleReference: styleImage ? { base64: styleImage.base64, format: styleImage.format } : void 0,
5882
+ enhancePrompt: character.enhancePrompt,
5883
+ concept: character.concept ? readPose(spec, "concept image", character.concept) : void 0,
5884
+ styleCharacterId: character.styleAnchor ? requireStyleObjectId(spec, context) : void 0
5707
5885
  });
5708
5886
  return { jobId: res2.character_id, metadata: { character: { kind: "base", characterId: res2.character_id, mode: character.mode, directions: character.directions, backgroundJobId: res2.background_job_id } } };
5709
5887
  }
@@ -5747,7 +5925,15 @@ var PixelLabProvider = class _PixelLabProvider {
5747
5925
  directions: [animation.direction],
5748
5926
  seed: spec.seed,
5749
5927
  textGuidanceScale: character.textGuidanceScale,
5750
- isometric: character.isometric
5928
+ isometric: character.isometric,
5929
+ startFrame: animation.startFrame ? readPose(spec, "start frame", animation.startFrame) : void 0,
5930
+ endFrame: animation.endFrame ? readPose(spec, "end frame", animation.endFrame) : void 0,
5931
+ description: animation.subject,
5932
+ outline: animation.outline,
5933
+ shading: animation.shading,
5934
+ detail: animation.detail,
5935
+ enhancePrompt: animation.enhancePrompt,
5936
+ paletteSwatchBase64: spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0
5751
5937
  });
5752
5938
  const job = { characterId: parentId, name, direction: animation.direction, jobIds: res.background_job_ids };
5753
5939
  return {
@@ -6115,6 +6301,21 @@ function readReference(spec, reference) {
6115
6301
  }
6116
6302
  return images;
6117
6303
  }
6304
+ function requireStyleObjectId(spec, context) {
6305
+ if (!context?.styleObjectId) {
6306
+ throw new Error(
6307
+ `${spec.styleId}/${spec.assetId} anchors its style on ${spec.character.styleAnchor.assetId}, which has no PixelLab character id in the lockfile yet; generate the anchor first`
6308
+ );
6309
+ }
6310
+ return context.styleObjectId;
6311
+ }
6312
+ function readPose(spec, what, image) {
6313
+ const bytes = readFileSync2(image.path);
6314
+ if (sha256(bytes) !== image.sha256) {
6315
+ throw new Error(`${spec.styleId}/${spec.assetId}: ${what} changed after the manifest was resolved: ${image.path}`);
6316
+ }
6317
+ return { base64: bytes.toString("base64"), format: image.format };
6318
+ }
6118
6319
  function rotationSources(character) {
6119
6320
  const order = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
6120
6321
  const sources = [];
@@ -7429,6 +7630,18 @@ async function resolveSpecs(loaded, filter) {
7429
7630
  finalized.add(assetId);
7430
7631
  return resolved;
7431
7632
  }
7633
+ const anchorId = asset.styleCharacter ?? style.styleCharacter;
7634
+ if (resolved.character?.kind === "base" && anchorId && anchorId !== assetId) {
7635
+ const anchorSpec = await finalize(anchorId);
7636
+ if (!anchorSpec.character || anchorSpec.character.kind === "animation") {
7637
+ throw new Error(`assets.${assetId}: styleCharacter ${anchorId} is not a character base or state`);
7638
+ }
7639
+ const file = memberPath(anchorSpec.outFile, "south", 0, anchorSpec.character.directions, MediaType.PNG);
7640
+ resolved.character = {
7641
+ ...resolved.character,
7642
+ styleAnchor: { assetId: anchorId, spec: anchorSpec, file, sha256: existsSync8(file) ? await sha256File(file) : null }
7643
+ };
7644
+ }
7432
7645
  const characterParent = asset.state?.of ?? asset.animation?.of;
7433
7646
  if (resolved.character && characterParent) {
7434
7647
  const parentSpec = await finalize(characterParent);
@@ -7564,34 +7777,6 @@ async function resolveStyleImages(loaded, styleId) {
7564
7777
  }
7565
7778
  return out;
7566
7779
  }
7567
- function imageMetadata(buf) {
7568
- if (buf.length >= 24 && buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834 && buf.toString("ascii", 12, 16) === "IHDR") {
7569
- return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20), format: "png" };
7570
- }
7571
- if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216) return null;
7572
- let offset = 2;
7573
- while (offset + 3 < buf.length) {
7574
- if (buf[offset] !== 255) return null;
7575
- while (buf[offset] === 255) offset++;
7576
- const marker = buf[offset++];
7577
- if (marker == null || marker === 217 || marker === 218) break;
7578
- if (marker === 1 || marker >= 208 && marker <= 215) continue;
7579
- if (offset + 2 > buf.length) return null;
7580
- const length = buf.readUInt16BE(offset);
7581
- if (length < 2 || offset + length > buf.length) return null;
7582
- const isStartOfFrame = marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker);
7583
- if (isStartOfFrame) {
7584
- if (length < 7) return null;
7585
- return {
7586
- width: buf.readUInt16BE(offset + 5),
7587
- height: buf.readUInt16BE(offset + 3),
7588
- format: "jpeg"
7589
- };
7590
- }
7591
- offset += length;
7592
- }
7593
- return null;
7594
- }
7595
7780
  async function resolveCharacterShape(asset, style, kind, files) {
7596
7781
  const mode2 = style.mode ?? "standard";
7597
7782
  const directions = mode2 === "standard" ? style.directions ?? 8 : 8;
@@ -7603,8 +7788,13 @@ async function resolveCharacterShape(asset, style, kind, files) {
7603
7788
  template: style.template ?? "mannequin",
7604
7789
  ...proportions !== void 0 ? { proportions } : {},
7605
7790
  ...style.textGuidanceScale !== void 0 ? { textGuidanceScale: style.textGuidanceScale } : {},
7606
- ...style.isometric !== void 0 ? { isometric: style.isometric } : {}
7791
+ ...style.isometric !== void 0 ? { isometric: style.isometric } : {},
7792
+ ...style.enhancePrompt !== void 0 ? { enhancePrompt: style.enhancePrompt } : {}
7607
7793
  };
7794
+ if (asset.concept) {
7795
+ const image = await files.load(asset.concept, "Concept image");
7796
+ shape.concept = { path: path13.resolve(files.root, asset.concept), sha256: image.hash, width: image.width, height: image.height, format: image.format };
7797
+ }
7608
7798
  if (asset.reference) {
7609
7799
  const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
7610
7800
  shape.reference = {};
@@ -7627,13 +7817,24 @@ async function resolveCharacterShape(asset, style, kind, files) {
7627
7817
  }
7628
7818
  if (asset.animation) {
7629
7819
  const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
7820
+ const pose = async (rel, what) => {
7821
+ const image = await files.load(rel, what);
7822
+ return { path: path13.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
7823
+ };
7630
7824
  shape.animation = {
7631
7825
  mode: animationMode,
7632
7826
  ...asset.animation.template ? { template: asset.animation.template } : {},
7633
7827
  direction: asset.animation.direction,
7634
7828
  frames: asset.animation.frames ?? 8,
7635
7829
  fps: asset.animation.fps,
7636
- keepFirstFrame: asset.animation.keepFirstFrame
7830
+ keepFirstFrame: asset.animation.keepFirstFrame,
7831
+ ...asset.animation.startFrame ? { startFrame: await pose(asset.animation.startFrame, "Animation start frame") } : {},
7832
+ ...asset.animation.endFrame ? { endFrame: await pose(asset.animation.endFrame, "Animation end frame") } : {},
7833
+ ...asset.animation.subject ? { subject: asset.animation.subject } : {},
7834
+ ...asset.animation.outline ? { outline: asset.animation.outline } : {},
7835
+ ...asset.animation.shading ? { shading: asset.animation.shading } : {},
7836
+ ...asset.animation.detail ? { detail: asset.animation.detail } : {},
7837
+ ...asset.animation.enhancePrompt !== void 0 ? { enhancePrompt: asset.animation.enhancePrompt } : {}
7637
7838
  };
7638
7839
  }
7639
7840
  return shape;
@@ -11728,10 +11929,13 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
11728
11929
  try {
11729
11930
  const refs = styleImages.get(spec.styleId) ?? [];
11730
11931
  const parentEntry = spec.character?.parentSpec ? lock.entries[lockKey(spec.character.parentSpec.styleId, spec.character.parentSpec.assetId)] : void 0;
11932
+ const anchor = spec.character?.styleAnchor;
11933
+ const anchorEntry = anchor ? lock.entries[lockKey(anchor.spec.styleId, anchor.spec.assetId)] : void 0;
11731
11934
  const replacedMetadata = previousEntry?.providerMetadata?.[provider.id];
11732
11935
  const { jobId, metadata } = await provider.submit(spec, refs, {
11733
11936
  ...previousJobId ? { previousJobId } : {},
11734
11937
  ...parentEntry?.objectId ? { parentObjectId: parentEntry.objectId } : {},
11938
+ ...anchorEntry?.objectId ? { styleObjectId: anchorEntry.objectId } : {},
11735
11939
  ...replacedMetadata ? { replacedMetadata } : {},
11736
11940
  ...previousMetadata ? { previousMetadata } : {},
11737
11941
  checkpoint: async (checkpoint) => {
@@ -12811,7 +13015,7 @@ var pin_default = {
12811
13015
  godot: "4.7.2",
12812
13016
  extensionsApi: 9,
12813
13017
  protocol: 4,
12814
- release: "editor-pixelorama-v1.2.2-pk.18",
13018
+ release: "editor-pixelorama-v1.2.2-pk.26",
12815
13019
  files: {
12816
13020
  "index.apple-touch-icon.png": {
12817
13021
  sha256: "ee5434873e0fed91e9a888c3ea5a8b79beac25999308be7a6eb41bf4ce640d27",
@@ -12826,7 +13030,7 @@ var pin_default = {
12826
13030
  bytes: 7298
12827
13031
  },
12828
13032
  "index.html": {
12829
- sha256: "d9adf06036660c4b76ef5386e872bc03dc0d5b0628fd6ac660d480893ee1f47a",
13033
+ sha256: "326101ab02bb4a29a7964214c9307c7dff26e9d66b8ebaaf6e7a3987e84a05b2",
12830
13034
  bytes: 5442
12831
13035
  },
12832
13036
  "index.icon.png": {
@@ -12838,8 +13042,8 @@ var pin_default = {
12838
13042
  bytes: 279815
12839
13043
  },
12840
13044
  "index.pck": {
12841
- sha256: "a41d9c3a39aa99ea5ee49ddcedfeb8b092006aa75c0de67139f13297c17f42e4",
12842
- bytes: 6431692
13045
+ sha256: "e26292650bb996965acd1ba70e74d2b4020b063d14f1276701312c1d34cb0a83",
13046
+ bytes: 6431932
12843
13047
  },
12844
13048
  "index.png": {
12845
13049
  sha256: "6e3ca3eb4a99e4c07bdd89ebc873b440ec0ffd66ce1955160da9e7ed80d538d7",
@@ -13458,7 +13662,7 @@ async function runLifecycle(args, wave, spent) {
13458
13662
  log(` Packaging stays blocked until each refined PNG has a current human approval.`);
13459
13663
  }
13460
13664
  }
13461
- return args.command === "gen" && submitted > 0 && total.downloaded > 0 && !total.failed && specs.some((spec) => spec.character?.parentSpec || spec.revision || spec.mirror);
13665
+ return args.command === "gen" && submitted > 0 && total.downloaded > 0 && !total.failed && specs.some((spec) => spec.character?.parentSpec || spec.character?.styleAnchor || spec.revision || spec.mirror);
13462
13666
  }
13463
13667
  return false;
13464
13668
  }