pixelkiln 0.89.6 → 0.90.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.
@@ -167,6 +167,7 @@ form.edit { border:1px solid var(--accent); padding:12px 14px 14px; margin-top:1
167
167
  form.edit h3 { margin:0; font:650 12.5px/1 var(--mono); color:var(--accent-soft); }
168
168
  form.edit .row { display:grid; grid-template-columns:repeat(auto-fit, minmax(110px, 1fr)); gap:10px; }
169
169
  .field { display:grid; gap:4px; font-size:12px; color:var(--dim); min-width:0; }
170
+ .field[hidden] { display:none; }
170
171
  .field span { font-family:var(--mono); font-size:11px; }
171
172
  .field small { font-size:11px; color:var(--dim); }
172
173
  .field input, .field textarea, .field select { background:var(--panel-deep); border:1px solid var(--line);
@@ -364,12 +364,15 @@
364
364
  // src/gallery/client/src/form-kit.ts
365
365
  var skeletonV3Allowed = (engine) => engine !== "standard";
366
366
  var defaultLoopMode = (engine) => skeletonV3Allowed(engine) ? "skeleton-v3" : "template";
367
+ var SKELETON_WORDS = "skeleton-v3-words";
368
+ var SKELETON_WORDS_FRAMES = { min: 3, max: 15, fallback: 8 };
367
369
  function syncSkeletonOption(select, engine) {
368
- const option = [...select.options].find((o) => o.value === "skeleton-v3");
369
- if (!option) return;
370
370
  const allowed = skeletonV3Allowed(engine);
371
- option.disabled = !allowed;
372
- option.title = allowed ? "" : "PixelLab poses a template only onto a v3 character. A standard base has no 3D skeleton; use template.";
371
+ for (const option of select.options) {
372
+ if (option.value !== "skeleton-v3" && option.value !== SKELETON_WORDS) continue;
373
+ option.disabled = !allowed;
374
+ option.title = allowed ? "" : option.value === SKELETON_WORDS ? "PixelLab writes the motion onto a stored 3D skeleton, which a standard base has none of; use v3 or a template." : "PixelLab poses a template only onto a v3 character. A standard base has no 3D skeleton; use template.";
375
+ }
373
376
  }
374
377
  var nextStep = () => GENERATION ? "Generate it from its record when you are ready." : "Nothing is generated until you run pixelkiln gen.";
375
378
  async function addAssetAndOpen(body, where, what = "Added to the manifest.") {
@@ -460,6 +463,7 @@
460
463
  if (isCharacter) {
461
464
  mode.append(
462
465
  new Option("skeleton-v3 (a template, posed by the skeleton model; steadiest)", "skeleton-v3"),
466
+ new Option("skeleton-v3: described in words (3-15 frames; 3 per direction measured)", SKELETON_WORDS),
463
467
  new Option("template (a template, redrawn; 1 generation)", "template")
464
468
  );
465
469
  }
@@ -474,6 +478,7 @@
474
478
  "skeleton-v3": "A PixelLab template posed onto the character by the skeleton model, which moves it instead of redrawing it: steadier identity and colours. 2 to 4 generations and 3 to 5 minutes per direction; beta, Tier 1 plans and up.",
475
479
  template: "A PixelLab template, redrawn frame by frame; 1 generation regardless of frame count."
476
480
  };
481
+ const wordsHint = "Needs a v3 base. The sentence is written into one 3D motion, 3 to 15 frames; the canvas can grow around the sprite. About 3 generations per direction measured; beta, Tier 1 plans and up.";
477
482
  const templateField = field("template id", template, templateHint.template);
478
483
  const frames = el("input");
479
484
  frames.type = "number";
@@ -481,6 +486,7 @@
481
486
  frames.max = "16";
482
487
  frames.step = "2";
483
488
  frames.placeholder = "8";
489
+ const framesField = field("frames", frames);
484
490
  const fps = el("input");
485
491
  fps.type = "number";
486
492
  fps.min = "1";
@@ -504,16 +510,24 @@
504
510
  form.append(row1, field("what it does", prompt, "The style still adds its prefix and suffix."));
505
511
  if (isCharacter) form.append(templateField);
506
512
  const row2 = el("div", "row");
507
- row2.append(field("frames", frames), field("fps", fps));
513
+ row2.append(framesField, field("fps", fps));
508
514
  form.append(row2);
509
- if (subject) form.append(field("subject override", subject));
515
+ const subjectField = subject ? field("subject override", subject) : null;
516
+ if (subjectField) form.append(subjectField);
510
517
  const syncMode = () => {
511
518
  const isTemplate = isCharacter && (mode.value === "template" || mode.value === "skeleton-v3");
512
519
  templateField.style.display = isTemplate ? "" : "none";
513
520
  if (isTemplate) templateField.querySelector("small").textContent = templateHint[mode.value];
514
521
  template.required = isTemplate;
515
522
  prompt.required = !isTemplate;
523
+ const words = isCharacter && mode.value === SKELETON_WORDS;
516
524
  row2.style.display = isTemplate ? "none" : "";
525
+ frames.min = words ? String(SKELETON_WORDS_FRAMES.min) : "4";
526
+ frames.max = words ? String(SKELETON_WORDS_FRAMES.max) : "16";
527
+ frames.step = words ? "1" : "2";
528
+ if (words) frames.title = wordsHint;
529
+ else frames.removeAttribute("title");
530
+ if (subjectField) subjectField.style.display = words ? "none" : "";
517
531
  };
518
532
  if (isCharacter) {
519
533
  mode.onchange = syncMode;
@@ -544,11 +558,11 @@
544
558
  animation.template = template.value.trim();
545
559
  if (mode.value === "skeleton-v3") animation.mode = "skeleton-v3";
546
560
  } else {
547
- animation.mode = mode.value;
561
+ animation.mode = mode.value === SKELETON_WORDS ? "skeleton-v3" : mode.value;
548
562
  if (frames.value.trim() !== "") animation.frames = Number(frames.value);
549
563
  if (fps.value.trim() !== "") animation.fps = Number(fps.value);
550
564
  }
551
- if (subject && subject.value.trim()) animation.subject = subject.value.trim();
565
+ if (subject && subject.value.trim() && mode.value !== SKELETON_WORDS) animation.subject = subject.value.trim();
552
566
  const asset = { prompt: prompt.value, styles: [item.styleId], animation };
553
567
  try {
554
568
  const body = { action: "add-asset", assetId: id.value.trim(), expectedSha256: pr.manifestSha256, asset };
@@ -1216,10 +1230,11 @@
1216
1230
  for (const d of ["south", "south-east", "east", "north-east", "north", "north-west", "west", "south-west"]) direction.append(new Option(d, d));
1217
1231
  const description = el("input");
1218
1232
  description.type = "text";
1219
- description.placeholder = "optional: colours, clothing, held items";
1233
+ description.placeholder = "e.g. a knight in steel armour holding a sword";
1234
+ description.required = true;
1220
1235
  const row1 = el("div", "row");
1221
1236
  row1.append(field("new asset id", id), field("facing", direction, "Which way the sprite faces."));
1222
- form.append(row1, field("motion", prompt, "A short label for the movement; sent as PixelLab's action."), field("looks like", description));
1237
+ form.append(row1, field("motion", prompt, "A short label for the movement; sent as PixelLab's action."), field("looks like", description, "What the character looks like: colours, clothing, held items. PixelLab refuses the request without it."));
1223
1238
  const source = el("select");
1224
1239
  source.append(new Option("estimate from this sprite", "estimate"), new Option("paste poses", "paste"), new Option("use a file in the project", "file"));
1225
1240
  const file = el("input");
@@ -1368,9 +1383,9 @@
1368
1383
  assetId: id.value.trim(),
1369
1384
  prompt: prompt.value.trim(),
1370
1385
  direction: direction.value,
1371
- keypointsFile: file.value.trim()
1386
+ keypointsFile: file.value.trim(),
1387
+ description: description.value.trim()
1372
1388
  };
1373
- if (description.value.trim()) body.description = description.value.trim();
1374
1389
  if (source.value !== "file") {
1375
1390
  body.set = set;
1376
1391
  if (overwrite.checked) body.overwrite = true;
@@ -5046,6 +5061,7 @@
5046
5061
  var PORTRAIT_SIZES = [16, 32, 48, 64, 128, 160];
5047
5062
  var LOOP_MODES = [
5048
5063
  { id: "skeleton-v3", label: "skeleton-v3: a template, posed by the skeleton model (steadiest; 2-4 per direction)" },
5064
+ { id: SKELETON_WORDS, label: "skeleton-v3: described in words (3-15 frames; 3 per direction measured)" },
5049
5065
  { id: "template", label: "template: a template, redrawn (1 per direction)" },
5050
5066
  { id: "v3", label: "v3: described in words" },
5051
5067
  { id: "pro", label: "pro: described in words, sequential (10-25 per direction)" }
@@ -5262,13 +5278,17 @@
5262
5278
  const engineId = loopEngine();
5263
5279
  syncSkeletonOption(row2.mode, engineId);
5264
5280
  if (!row2.picked) row2.mode.value = defaultLoopMode(engineId);
5265
- else if (row2.mode.value === "skeleton-v3" && !skeletonV3Allowed(engineId)) row2.mode.value = "template";
5281
+ else if (!skeletonV3Allowed(engineId)) {
5282
+ if (row2.mode.value === "skeleton-v3") row2.mode.value = "template";
5283
+ else if (row2.mode.value === SKELETON_WORDS) row2.mode.value = "v3";
5284
+ }
5266
5285
  syncLoopMode(row2);
5267
5286
  }
5268
5287
  function syncLoopMode(row2) {
5269
5288
  const t = templated(row2.mode.value);
5270
5289
  row2.what.placeholder = t ? "template, e.g. walk, breathing-idle, running-8-frames" : 'the motion, e.g. "swings a sword overhead"';
5271
5290
  row2.what.title = t ? "A PixelLab template id: pick one, or type any id PixelLab has" : "What the loop does";
5291
+ row2.framesField.hidden = row2.mode.value !== SKELETON_WORDS;
5272
5292
  if (t) row2.what.setAttribute("list", templateList.id);
5273
5293
  else row2.what.removeAttribute("list");
5274
5294
  syncPreview(row2);
@@ -5332,6 +5352,14 @@
5332
5352
  what.type = "text";
5333
5353
  what.required = true;
5334
5354
  what.value = defaults.what ?? "";
5355
+ const frames = el("input");
5356
+ frames.type = "number";
5357
+ frames.min = String(SKELETON_WORDS_FRAMES.min);
5358
+ frames.max = String(SKELETON_WORDS_FRAMES.max);
5359
+ frames.step = "1";
5360
+ frames.placeholder = String(SKELETON_WORDS_FRAMES.fallback);
5361
+ frames.value = defaults.frames ?? "";
5362
+ const framesField = field("frames", frames, "3 to 15; each direction is billed separately.");
5335
5363
  const dirs = el("div", "studio-dirs");
5336
5364
  const boxes = /* @__PURE__ */ new Map();
5337
5365
  for (const d of COMPASS2) {
@@ -5347,7 +5375,7 @@
5347
5375
  remove.type = "button";
5348
5376
  const preview = el("div", "studio-tpl-preview");
5349
5377
  preview.hidden = true;
5350
- const row2 = { id, mode, picked: defaults.mode !== void 0, what, directions: boxes, wrap, preview };
5378
+ const row2 = { id, mode, picked: defaults.mode !== void 0, what, frames, framesField, directions: boxes, wrap, preview };
5351
5379
  what.addEventListener("input", () => syncPreview(row2));
5352
5380
  remove.onclick = () => {
5353
5381
  loops2.splice(loops2.indexOf(row2), 1);
@@ -5361,7 +5389,11 @@
5361
5389
  schedulePrice();
5362
5390
  };
5363
5391
  const line = el("div", "row");
5364
- line.append(field("loop id", id), field("mode", mode), field("template or motion", what));
5392
+ line.append(field("loop id", id), field("mode", mode), field("template or motion", what), framesField);
5393
+ frames.oninput = () => {
5394
+ schedulePrice();
5395
+ keepDraft();
5396
+ };
5365
5397
  wrap.append(line, preview, dirs, remove);
5366
5398
  loopList.append(wrap);
5367
5399
  loops2.push(row2);
@@ -5389,6 +5421,11 @@
5389
5421
  if (templated(row2.mode.value)) {
5390
5422
  animation.template = row2.what.value.trim();
5391
5423
  if (row2.mode.value === "skeleton-v3") animation.mode = "skeleton-v3";
5424
+ } else if (row2.mode.value === SKELETON_WORDS) {
5425
+ animation.mode = "skeleton-v3";
5426
+ asset.prompt = row2.what.value.trim();
5427
+ const count = row2.frames.value.trim();
5428
+ if (count !== "" && Number(count) !== SKELETON_WORDS_FRAMES.fallback) animation.frames = Number(count);
5392
5429
  } else {
5393
5430
  animation.mode = row2.mode.value;
5394
5431
  asset.prompt = row2.what.value.trim();
@@ -5609,7 +5646,7 @@
5609
5646
  baseId: baseId.value,
5610
5647
  prompt: prompt.value,
5611
5648
  reference,
5612
- loops: loops2.map((r) => ({ id: r.id.value, mode: r.mode.value, what: r.what.value, directions: [...r.directions].filter(([, b]) => b.checked).map(([d]) => d) })),
5649
+ loops: loops2.map((r) => ({ id: r.id.value, mode: r.mode.value, what: r.what.value, frames: r.frames.value, directions: [...r.directions].filter(([, b]) => b.checked).map(([d]) => d) })),
5613
5650
  portrait: portrait.checked,
5614
5651
  portraitSize: portraitSize.value
5615
5652
  });
@@ -5656,7 +5693,7 @@
5656
5693
  portrait.checked = Boolean(draft.portrait);
5657
5694
  set(portraitSize, draft.portraitSize);
5658
5695
  syncStyle();
5659
- for (const l of Array.isArray(draft.loops) ? draft.loops : []) addLoopRow({ id: l.id, mode: l.mode, what: l.what, directions: l.directions });
5696
+ for (const l of Array.isArray(draft.loops) ? draft.loops : []) addLoopRow({ id: l.id, mode: l.mode, what: l.what, frames: l.frames, directions: l.directions });
5660
5697
  const note = el("div", "notice studio-draft");
5661
5698
  note.append(document.createTextNode("Picked up the draft you left " + (fmtWhen(draft.savedAt) || "earlier") + ". "));
5662
5699
  const discard = el("button", "linkish", "Start over");
package/dist/index.cjs CHANGED
@@ -692,16 +692,21 @@ var CharacterAnimationSchema = import_zod.z.object({
692
692
  * `src/loop-directions.ts`.
693
693
  */
694
694
  directions: import_zod.z.array(CharacterDirectionSchema).min(1).optional(),
695
- /** Frames to generate without a template (v3). Even, 4 to 16. */
696
- frames: import_zod.z.number().int().min(4).max(16).optional(),
695
+ /**
696
+ * Frames to generate without a template. A v3 loop takes an even 4 to 16;
697
+ * a sentence loop (`mode: "skeleton-v3"` with no `template`) takes 3 to 15.
698
+ */
699
+ frames: import_zod.z.number().int().min(3).max(16).optional(),
697
700
  /** Playback rate recorded with the frames; PixelLab does not store one. */
698
701
  fps: import_zod.z.number().int().min(1).max(60).default(8),
699
702
  /** v3 only: keep the resting pose as frame 0, so `frames` generated frames land as `frames + 1` files. */
700
703
  keepFirstFrame: import_zod.z.boolean().default(true),
701
704
  /**
702
705
  * `template` when a template is named, otherwise `v3`; `skeleton-v3`
703
- * poses a template with PixelLab's skeleton model instead; `pro` for the
704
- * sequential high-quality engine.
706
+ * poses a template with PixelLab's skeleton model instead, or, with no
707
+ * template, animates the asset's prompt as a sentence written into one 3D
708
+ * motion (a v3 base; 3-15 `frames`); `pro` for the sequential
709
+ * high-quality engine.
705
710
  */
706
711
  mode: CharacterAnimationModeSchema.optional(),
707
712
  /**
@@ -731,15 +736,23 @@ var CharacterAnimationSchema = import_zod.z.object({
731
736
  path: ["directions"]
732
737
  });
733
738
  }
734
- if (animation.frames !== void 0 && animation.frames % 2 !== 0) {
735
- context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "frames must be even", path: ["frames"] });
739
+ const sentence = animation.mode === "skeleton-v3" && !animation.template;
740
+ if (animation.frames !== void 0 && !sentence) {
741
+ if (animation.frames % 2 !== 0) {
742
+ context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "frames must be even", path: ["frames"] });
743
+ } else if (animation.frames < 4) {
744
+ context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "frames must be 4 to 16", path: ["frames"] });
745
+ }
746
+ }
747
+ if (animation.frames !== void 0 && sentence && animation.frames > 15) {
748
+ context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "a skeleton-v3 loop from a sentence takes 3 to 15 frames", path: ["frames"] });
736
749
  }
737
750
  const mode2 = animation.mode ?? (animation.template ? "template" : "v3");
738
751
  for (const key of ["startFrame", "endFrame", "enhancePrompt"]) {
739
752
  if (animation[key] !== void 0 && mode2 !== "v3") {
740
753
  context.addIssue({
741
754
  code: import_zod.z.ZodIssueCode.custom,
742
- message: `${key} is for v3 loops; a ${mode2} loop ${mode2 === "template" || mode2 === "skeleton-v3" ? "starts from the character's rotation and follows its template" : "takes neither"}`,
755
+ message: `${key} is for v3 loops; a ${mode2} loop ${mode2 === "template" || mode2 === "skeleton-v3" ? `starts from the character's rotation and ${animation.template ? "follows its template" : "animates its sentence"}` : "takes neither"}`,
743
756
  path: [key]
744
757
  });
745
758
  }
@@ -763,8 +776,11 @@ var CharacterAnimationSchema = import_zod.z.object({
763
776
  path: ["frames"]
764
777
  });
765
778
  }
766
- if (!animation.template && (animation.mode === "template" || animation.mode === "skeleton-v3")) {
767
- context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: `${animation.mode} mode needs a template`, path: ["template"] });
779
+ if (!animation.template && animation.mode === "template") {
780
+ context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "template mode needs a template", path: ["template"] });
781
+ }
782
+ if (sentence && animation.subject !== void 0) {
783
+ context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "subject does not apply to a skeleton-v3 loop from a sentence; describe the motion in the prompt", path: ["subject"] });
768
784
  }
769
785
  });
770
786
  var RevisionDitheringSchema = import_zod.z.enum(["none", "2x2", "4x4", "8x8"]);
@@ -2992,7 +3008,10 @@ var PixelLabClient = class {
2992
3008
  * Every mode but skeleton-v3 goes to `/animate-character`, the path whose
2993
3009
  * shapes were measured live. skeleton-v3 is documented only on
2994
3010
  * `/characters/animations`, which takes the same request and answers with
2995
- * the same job list (plus an `animation_group_id`).
3011
+ * the same job list (plus an `animation_group_id`). With no `template` it
3012
+ * animates `actionDescription` as a sentence, 3 to 15 `frameCount` frames
3013
+ * written into one 3D motion; measured on a v3 character (8 frames, 64px:
3014
+ * 3 generations, frames back at 96x96).
2996
3015
  */
2997
3016
  async animateCharacter(args) {
2998
3017
  const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
@@ -3007,7 +3026,8 @@ var PixelLabClient = class {
3007
3026
  ...args.template ? { template_animation_id: args.template } : {},
3008
3027
  ...args.actionDescription ? { action_description: args.actionDescription } : {},
3009
3028
  ...args.description ? { description: args.description } : {},
3010
- ...args.mode === "v3" && args.frameCount ? { frame_count: args.frameCount } : {},
3029
+ // A template fixes its own frame count; v3 and a skeleton-v3 sentence take one.
3030
+ ...(args.mode === "v3" || args.mode === "skeleton-v3" && !args.template) && args.frameCount ? { frame_count: args.frameCount } : {},
3011
3031
  ...args.mode === "v3" && args.keepFirstFrame === false ? { keep_first_frame: false } : {},
3012
3032
  ...args.mode === "v3" && args.startFrame ? { custom_start_frame: encode(args.startFrame) } : {},
3013
3033
  ...args.mode === "v3" && args.endFrame ? { end_frame: encode(args.endFrame) } : {},
@@ -5659,6 +5679,16 @@ var PixelLabProvider = class _PixelLabProvider {
5659
5679
  if (skeletonTemplate && !CHARACTER_TEMPLATES.includes(skeletonTemplate)) {
5660
5680
  throw new Error(`PixelLab animate-skeleton template "${skeletonTemplate}" is not one of ${CHARACTER_TEMPLATES.join(", ")}`);
5661
5681
  }
5682
+ if (!spec.revision.description?.trim()) {
5683
+ throw new Error(
5684
+ `${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`
5685
+ );
5686
+ }
5687
+ if (!spec.prompt.trim()) {
5688
+ throw new Error(
5689
+ `${spec.styleId}/${spec.assetId}: an animate-skeleton revision needs a prompt, a short motion label (for example "swinging the sword")`
5690
+ );
5691
+ }
5662
5692
  }
5663
5693
  if (spec.revision?.mode === "animate" || spec.revision?.mode === "animate-pixminimax") {
5664
5694
  const { sourceWidth: width, sourceHeight: height, mode: mode2, frames, lastFrameWidth, lastFrameHeight } = spec.revision;
@@ -6062,12 +6092,18 @@ var PixelLabProvider = class _PixelLabProvider {
6062
6092
  `${spec.styleId}/${spec.assetId} animates "${character.animation.direction}", but its character has ${character.directions} directions: ${allowed.join(", ")}`
6063
6093
  );
6064
6094
  }
6065
- if (character.animation.mode !== "template" && character.animation.mode !== "skeleton-v3" && !spec.prompt.trim()) {
6066
- throw new Error(`${spec.styleId}/${spec.assetId} needs a prompt describing the motion, or a template`);
6095
+ const sentence = character.animation.mode === "skeleton-v3" && !character.animation.template;
6096
+ if (character.animation.mode !== "template" && (character.animation.mode !== "skeleton-v3" || sentence) && !spec.prompt.trim()) {
6097
+ throw new Error(
6098
+ `${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")' : "")
6099
+ );
6100
+ }
6101
+ if (sentence && (character.animation.frames < 3 || character.animation.frames > 15)) {
6102
+ throw new Error(`${spec.styleId}/${spec.assetId}: a skeleton-v3 loop from a sentence takes 3 to 15 frames; ${character.animation.frames} is out of range`);
6067
6103
  }
6068
6104
  if (character.animation.mode === "skeleton-v3" && character.mode === "standard") {
6069
6105
  throw new Error(
6070
- `${spec.styleId}/${spec.assetId}: skeleton-v3 poses a template onto a 3D skeleton, which a ${character.mode} character has none of; use a v3 base (style mode: "v3"), or mode: "template" for this loop`
6106
+ sentence ? `${spec.styleId}/${spec.assetId}: a skeleton-v3 loop from a sentence writes the motion onto a 3D skeleton, which a ${character.mode} character has none of; use a v3 base (style mode: "v3"), or mode: "template" with a template for this loop` : `${spec.styleId}/${spec.assetId}: skeleton-v3 poses a template onto a 3D skeleton, which a ${character.mode} character has none of; use a v3 base (style mode: "v3"), or mode: "template" for this loop`
6071
6107
  );
6072
6108
  }
6073
6109
  }
@@ -18448,7 +18484,8 @@ var SkeletonAnimationRequestSchema = import_zod12.z.object({
18448
18484
  /** The motion, sent as PixelLab's `action`. */
18449
18485
  prompt: import_zod12.z.string().min(1),
18450
18486
  direction: CharacterDirectionSchema,
18451
- description: import_zod12.z.string().min(1).optional(),
18487
+ /** What the character looks like; PixelLab refuses the request without it. */
18488
+ description: import_zod12.z.string().min(1),
18452
18489
  skeletonTemplate: import_zod12.z.string().min(1).optional(),
18453
18490
  /** Manifest-relative JSON path inside the project. */
18454
18491
  keypointsFile: import_zod12.z.string().min(1),
@@ -18498,7 +18535,7 @@ async function createSkeletonAnimation(manifestPath, request) {
18498
18535
  from: request.from,
18499
18536
  keypointsFile: relative.split(import_node_path33.default.sep).join("/"),
18500
18537
  direction: request.direction,
18501
- ...request.description ? { description: request.description } : {},
18538
+ description: request.description,
18502
18539
  ...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
18503
18540
  }
18504
18541
  }