@pixeldojo/mcp 0.9.1 → 0.10.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/README.md CHANGED
@@ -63,6 +63,18 @@ Restart your agent. You'll see 16 new tools.
63
63
  Generation skills take a `model` field for whitelist-checked overrides. Pass
64
64
  any apiId from the [API Platform catalog](https://pixeldojo.ai/api-platform).
65
65
 
66
+ **References.** `image_url` is for stills, `video_url` for a source clip, and
67
+ an audio URL can ride along in either. Each lands in the field the chosen
68
+ model actually declares for that kind of media. A clip or a track with no
69
+ `model` named routes to an engine that can read one; naming a model that
70
+ can't gets you the working engine plus a one-line note saying so.
71
+
72
+ **Model-specific settings.** Anything the shared inputs don't cover — a seed,
73
+ a negative prompt, a resolution tier, a quality toggle — goes in `params`
74
+ under that model's own field name, in the same call that names the `model`
75
+ (`{"model": "seedance-2-reference", "params": {"resolution": "1080p"}}`). A
76
+ wrong field comes back named with its allowed values and nothing is charged.
77
+
66
78
  ## How long jobs work
67
79
 
68
80
  Generation skills wait synchronously for up to 30 seconds. If the
package/dist/cli.cjs CHANGED
@@ -185,19 +185,23 @@ var init_upload_helpers = __esm({
185
185
  });
186
186
 
187
187
  // src/skills/schema-snap.ts
188
- function getFieldConstraints(apiId, client) {
188
+ function getModelInfo(apiId, client) {
189
189
  const cached = schemaCache.get(apiId);
190
190
  if (cached) return cached;
191
191
  if (typeof client?.getModelSchema !== "function") return Promise.resolve(null);
192
- const pending = Promise.resolve().then(() => client.getModelSchema(apiId)).then(
193
- (res) => res.schema?.properties ?? {}
194
- ).catch(() => {
192
+ const pending = Promise.resolve().then(() => client.getModelSchema(apiId)).then((res) => {
193
+ const body = res;
194
+ return { props: body.schema?.properties ?? {}, media: body.media };
195
+ }).catch(() => {
195
196
  schemaCache.delete(apiId);
196
197
  return null;
197
198
  });
198
199
  schemaCache.set(apiId, pending);
199
200
  return pending;
200
201
  }
202
+ async function getFieldConstraints(apiId, client) {
203
+ return (await getModelInfo(apiId, client))?.props ?? null;
204
+ }
201
205
  function parseAspectRatio(ar) {
202
206
  const m = /^\s*(\d+)\s*:\s*(\d+)\s*$/.exec(ar);
203
207
  if (!m) return null;
@@ -233,11 +237,20 @@ async function snapAspectRatio(apiId, requested, client, opts = {}) {
233
237
  if (!field.enum || field.enum.length === 0) return requested;
234
238
  return nearestAspectFromEnum(requested, field.enum);
235
239
  }
236
- async function resolveReferenceField(apiId, client) {
237
- const props = await getFieldConstraints(apiId, client);
238
- if (props === null) return void 0;
239
- for (const name of REFERENCE_FIELD_PRIORITY) {
240
- const field = props[name];
240
+ async function resolveMediaSlot(apiId, kind, client) {
241
+ const info = await getModelInfo(apiId, client);
242
+ if (info === null) return void 0;
243
+ const declared = info.media?.[kind];
244
+ if (declared) {
245
+ return {
246
+ name: declared.key,
247
+ isArray: info.props[declared.key]?.type === "array",
248
+ also: declared.also
249
+ };
250
+ }
251
+ if (info.media && kind !== "audio") return null;
252
+ for (const name of FALLBACK_KEYS[kind]) {
253
+ const field = info.props[name];
241
254
  if (field) return { name, isArray: field.type === "array" };
242
255
  }
243
256
  return null;
@@ -297,21 +310,29 @@ async function snapDuration(apiId, requested, client, opts = {}) {
297
310
  }
298
311
  return requested;
299
312
  }
313
+ function isVideoUrl(url) {
314
+ return /\.(mp4|mov|webm|m4v)(?:[?#]|$)/i.test(url);
315
+ }
316
+ function isAudioUrl(url) {
317
+ return /\.(mp3|wav|m4a|aac|ogg|flac)(?:[?#]|$)/i.test(url);
318
+ }
300
319
  async function resolveImageRefs(apiId, urls, client) {
301
320
  if (urls.length === 0) return null;
302
- const props = await getFieldConstraints(apiId, client);
303
- if (props === null) return null;
304
- const key = REF_KEYS.find((k) => props[k] !== void 0);
321
+ const info = await getModelInfo(apiId, client);
322
+ if (info === null) return null;
323
+ const props = info.props;
324
+ const key = info.media ? info.media.image?.key : REF_KEYS.find((k) => props[k] !== void 0);
305
325
  if (!key) return null;
306
- const field = props[key];
326
+ const also = info.media?.image?.also;
327
+ const field = props[key] ?? {};
307
328
  if (field.type === "array") {
308
329
  const cap = typeof field.maxItems === "number" ? field.maxItems : urls.length;
309
330
  const kept = urls.slice(0, Math.max(1, cap));
310
- return { key, value: kept, dropped: urls.length - kept.length };
331
+ return { key, value: kept, dropped: urls.length - kept.length, also };
311
332
  }
312
- return { key, value: urls[0], dropped: urls.length - 1 };
333
+ return { key, value: urls[0], dropped: urls.length - 1, also };
313
334
  }
314
- var schemaCache, REFERENCE_FIELD_PRIORITY, REF_KEYS;
335
+ var schemaCache, REFERENCE_FIELD_PRIORITY, FALLBACK_KEYS, REF_KEYS;
315
336
  var init_schema_snap = __esm({
316
337
  "src/skills/schema-snap.ts"() {
317
338
  schemaCache = /* @__PURE__ */ new Map();
@@ -327,6 +348,11 @@ var init_schema_snap = __esm({
327
348
  "image_input",
328
349
  "images"
329
350
  ];
351
+ FALLBACK_KEYS = {
352
+ image: REFERENCE_FIELD_PRIORITY,
353
+ video: ["reference_videos"],
354
+ audio: ["reference_audios", "audio_url"]
355
+ };
330
356
  REF_KEYS = [
331
357
  "image_urls",
332
358
  "images",
@@ -339,7 +365,51 @@ var init_schema_snap = __esm({
339
365
  ];
340
366
  }
341
367
  });
342
- function buildPayload(apiId, input, refField, referenceUrls = []) {
368
+ function paramsWithoutModel(params, model) {
369
+ if (!params || Object.keys(params).length === 0) return null;
370
+ if (model) return null;
371
+ return "Those settings are model-specific. Tell me which model to use and I will apply them.";
372
+ }
373
+ function applyParams(body, params, paramsModel, apiId) {
374
+ if (!params || Object.keys(params).length === 0) return void 0;
375
+ if (paramsModel !== apiId) {
376
+ return `This ran on ${apiId}, so I left those settings off: they are ${paramsModel}'s fields. Ask again naming ${apiId} if you want them applied.`;
377
+ }
378
+ const rest = { ...params };
379
+ delete rest.prompt;
380
+ Object.assign(body, rest);
381
+ return "prompt" in params ? "I kept the prompt from the skill call and ignored the one in params." : void 0;
382
+ }
383
+ function prefixNote(result, note) {
384
+ if (!note) return result;
385
+ return { ...result, text: `${note} ${result.text}`.trim() };
386
+ }
387
+ var ParamsInput, PARAMS_JSON_SCHEMA;
388
+ var init_params = __esm({
389
+ "src/skills/params.ts"() {
390
+ ParamsInput = zod.z.record(zod.z.unknown()).optional();
391
+ PARAMS_JSON_SCHEMA = {
392
+ type: "object",
393
+ additionalProperties: true,
394
+ description: 'Advanced settings for the model that runs this, named exactly as that model documents them (e.g. {"resolution": "1080p", "seed": 42}). Set `model` in the same call: field names differ per model, so params without one are refused. Applied last, so these override anything derived from the other inputs. An invalid field comes back named with its allowed values and nothing is charged, so fix it and call again.'
395
+ };
396
+ }
397
+ });
398
+ function fallbackSlot(apiId, kind) {
399
+ return apiId === VIDEO_REFERENCE_MODEL ? VIDEO_REFERENCE_SLOTS[kind] : void 0;
400
+ }
401
+ function sourceMedia(videoUrl, referenceUrls) {
402
+ const clip = videoUrl && !isAudioUrl(videoUrl) ? videoUrl : void 0;
403
+ const stills = referenceUrls.filter((u) => !isAudioUrl(u));
404
+ const url = clip ?? stills[0];
405
+ return {
406
+ url,
407
+ isClip: Boolean(clip) || !!url && isVideoUrl(url),
408
+ audioUrl: [videoUrl, ...referenceUrls].find((u) => u && isAudioUrl(u)),
409
+ stills
410
+ };
411
+ }
412
+ function buildPayload(apiId, input, src, slot, audioSlot) {
343
413
  const body = { prompt: input.prompt };
344
414
  if (input.aspect_ratio) body.aspect_ratio = input.aspect_ratio;
345
415
  if (input.preset) body.preset = input.preset;
@@ -347,9 +417,9 @@ function buildPayload(apiId, input, refField, referenceUrls = []) {
347
417
  case "grok-video-extend":
348
418
  case "wan-video-edit":
349
419
  case "seedance-video-edit":
350
- if (input.image_url) {
351
- body.video_url = input.image_url;
352
- body.image_url = input.image_url;
420
+ if (src.url) {
421
+ body.video_url = src.url;
422
+ body.image_url = src.url;
353
423
  }
354
424
  if (input.duration !== void 0) {
355
425
  body.video_duration = input.duration;
@@ -357,14 +427,19 @@ function buildPayload(apiId, input, refField, referenceUrls = []) {
357
427
  }
358
428
  break;
359
429
  default:
360
- if (input.image_url) {
361
- const refs = referenceUrls.length > 0 ? referenceUrls : [input.image_url];
362
- if (refField) {
363
- body[refField.name] = refField.isArray ? refs : refs[0];
430
+ if (src.url) {
431
+ const refs = src.isClip ? [src.url] : src.stills.length > 0 ? src.stills : [src.url];
432
+ if (slot) {
433
+ body[slot.name] = slot.isArray ? refs : refs[0];
434
+ if (slot.also) Object.assign(body, slot.also);
364
435
  } else {
365
436
  body.image_url = refs[0];
366
437
  }
367
438
  }
439
+ if (src.audioUrl && audioSlot) {
440
+ body[audioSlot.name] = audioSlot.isArray ? [src.audioUrl] : src.audioUrl;
441
+ if (audioSlot.also) Object.assign(body, audioSlot.also);
442
+ }
368
443
  if (input.duration !== void 0) body.duration = input.duration;
369
444
  if (input.resolution) body.resolution = input.resolution;
370
445
  break;
@@ -372,7 +447,8 @@ function buildPayload(apiId, input, refField, referenceUrls = []) {
372
447
  return body;
373
448
  }
374
449
  async function run(input, ctx) {
375
- const apiId = input.model ?? DEFAULT_MODEL;
450
+ const paramsGate = paramsWithoutModel(input.params, input.model);
451
+ if (paramsGate) return { kind: "text", text: paramsGate };
376
452
  let resolved = input;
377
453
  let referenceUrls = [];
378
454
  try {
@@ -393,11 +469,22 @@ async function run(input, ctx) {
393
469
  }
394
470
  throw err;
395
471
  }
472
+ const src = sourceMedia(input.video_url, referenceUrls);
473
+ let apiId = input.model ?? DEFAULT_MODEL;
474
+ let routeNote;
475
+ if ((src.isClip || src.audioUrl) && !VIDEO_INPUT_MODELS.has(apiId)) {
476
+ if (!input.model) {
477
+ apiId = VIDEO_REFERENCE_MODEL;
478
+ } else if (src.isClip && await resolveMediaSlot(input.model, "video", ctx.client) === null) {
479
+ apiId = VIDEO_REFERENCE_MODEL;
480
+ routeNote = `${input.model} can't work from a video clip, so I used ${VIDEO_REFERENCE_MODEL} instead.`;
481
+ }
482
+ }
396
483
  if (apiId === "grok-video-extend") {
397
- if (!resolved.image_url) {
484
+ if (!src.url) {
398
485
  return {
399
486
  kind: "text",
400
- text: 'pixeldojo_generate with model="grok-video-extend" requires image_url (the source video URL to extend, or image_path for a local file).'
487
+ text: 'pixeldojo_generate with model="grok-video-extend" requires video_url (the source video URL to extend, or image_path for a local file).'
401
488
  };
402
489
  }
403
490
  if (resolved.duration === void 0) {
@@ -408,17 +495,22 @@ async function run(input, ctx) {
408
495
  }
409
496
  }
410
497
  let refField;
498
+ let audioSlot;
411
499
  if (!VIDEO_INPUT_MODELS.has(apiId)) {
412
- const [aspect_ratio, duration, resolution, ref] = await Promise.all([
500
+ const [aspect_ratio, duration, resolution, ref, audio] = await Promise.all([
413
501
  snapAspectRatio(apiId, resolved.aspect_ratio, ctx.client),
414
502
  snapDuration(apiId, resolved.duration, ctx.client),
415
503
  snapResolution(apiId, resolved.resolution, ctx.client),
416
- resolved.image_url ? resolveReferenceField(apiId, ctx.client) : Promise.resolve(void 0)
504
+ src.url ? resolveMediaSlot(apiId, src.isClip ? "video" : "image", ctx.client) : Promise.resolve(void 0),
505
+ src.audioUrl ? resolveMediaSlot(apiId, "audio", ctx.client) : Promise.resolve(void 0)
417
506
  ]);
418
507
  resolved = { ...resolved, aspect_ratio, duration, resolution };
419
- refField = ref;
508
+ refField = ref === void 0 && src.url ? fallbackSlot(apiId, src.isClip ? "video" : "image") : ref;
509
+ audioSlot = audio === void 0 && src.audioUrl ? fallbackSlot(apiId, "audio") : audio;
420
510
  }
421
- const body = buildPayload(apiId, resolved, refField, referenceUrls);
511
+ const body = buildPayload(apiId, resolved, src, refField, audioSlot);
512
+ const paramsNote = applyParams(body, input.params, input.model, apiId);
513
+ const note = [routeNote, paramsNote].filter(Boolean).join(" ");
422
514
  const submitted = await ctx.client.run(apiId, body);
423
515
  const result = await pollJobUntilDoneOrHandoff(
424
516
  submitted.jobId,
@@ -429,18 +521,22 @@ async function run(input, ctx) {
429
521
  sleep: ctx.pollOverrides?.sleep
430
522
  }
431
523
  );
432
- return pollResultToSkillResult(
433
- result,
434
- (urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : urls.length === 1 ? `Generated. Asset: ${urls[0]}` : `Generated ${urls.length} assets. URLs: ${urls.join(", ")}`
524
+ return prefixNote(
525
+ pollResultToSkillResult(
526
+ result,
527
+ (urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : urls.length === 1 ? `Generated. Asset: ${urls[0]}` : `Generated ${urls.length} assets. URLs: ${urls.join(", ")}`
528
+ ),
529
+ note
435
530
  );
436
531
  }
437
- var InputSchema, DEFAULT_MODEL, VIDEO_INPUT_MODELS, InputJsonSchema, generateSkill;
532
+ var InputSchema, DEFAULT_MODEL, VIDEO_REFERENCE_MODEL, VIDEO_REFERENCE_SLOTS, VIDEO_INPUT_MODELS, InputJsonSchema, generateSkill;
438
533
  var init_generate = __esm({
439
534
  "src/skills/generate.ts"() {
440
535
  init_poll();
441
536
  init_types();
442
537
  init_upload_helpers();
443
538
  init_schema_snap();
539
+ init_params();
444
540
  InputSchema = zod.z.object({
445
541
  prompt: zod.z.string().min(1).max(8e3).describe("Prompt describing what to generate."),
446
542
  model: zod.z.string().optional().describe(
@@ -455,6 +551,10 @@ var init_generate = __esm({
455
551
  'Output resolution when the model supports it (e.g. "480p", "720p", "1080p", "4k"). Lower tiers cost fewer credits. Omit to use the model default.'
456
552
  ),
457
553
  image_url: zod.z.string().url().optional().describe("Reference image URL for image-to-image / image-to-video tools."),
554
+ video_url: zod.z.string().url().optional().describe(
555
+ "Source video clip to animate / restyle / extend. Use this for videos; image_url is for still frames. A clip routes to a model that can actually read one."
556
+ ),
557
+ params: ParamsInput,
458
558
  image_path: zod.z.string().min(1).optional().describe(
459
559
  "Absolute local file path to a reference image/video. The skill uploads it to PixelDojo first (24h temp storage) and then dispatches. Prefer this over image_url when the user has the file on disk."
460
560
  ),
@@ -466,6 +566,12 @@ var init_generate = __esm({
466
566
  )
467
567
  });
468
568
  DEFAULT_MODEL = "google-nano-banana";
569
+ VIDEO_REFERENCE_MODEL = "seedance-2-reference";
570
+ VIDEO_REFERENCE_SLOTS = {
571
+ image: { name: "reference_images", isArray: true },
572
+ video: { name: "reference_videos", isArray: true },
573
+ audio: { name: "reference_audios", isArray: true }
574
+ };
469
575
  VIDEO_INPUT_MODELS = /* @__PURE__ */ new Set(["grok-video-extend", "wan-video-edit", "seedance-video-edit"]);
470
576
  InputJsonSchema = {
471
577
  type: "object",
@@ -486,6 +592,12 @@ var init_generate = __esm({
486
592
  description: 'Output resolution when supported (e.g. "480p", "720p", "1080p", "4k"). Lower tiers cost fewer credits.'
487
593
  },
488
594
  image_url: { type: "string", format: "uri" },
595
+ video_url: {
596
+ type: "string",
597
+ format: "uri",
598
+ description: "Source video clip to animate / restyle / extend. Use this for videos; image_url is for still frames."
599
+ },
600
+ params: PARAMS_JSON_SCHEMA,
489
601
  image_urls: {
490
602
  type: "array",
491
603
  items: { type: "string", format: "uri" },
@@ -505,7 +617,7 @@ var init_generate = __esm({
505
617
  };
506
618
  generateSkill = {
507
619
  name: "pixeldojo_generate",
508
- description: "Generate any image or video from a prompt. Auto-routes to a sensible default model; pass `model` to pick a specific apiId from the catalog. For reference-image / image-to-video flows: pass `image_url` if you already have a URL, or `image_path` (absolute local path) and the skill uploads it to 24h temp storage automatically.",
620
+ description: "Generate any image or video from a prompt. Auto-routes to a sensible default model; pass `model` to pick a specific apiId from the catalog. For reference flows: pass `image_url` for a still, `video_url` for a source clip, or `image_path` (absolute local path) and the skill uploads it to 24h temp storage automatically. A clip or an audio reference routes to an engine that can read one. Model-specific settings go in `params` alongside an explicit `model`.",
509
621
  inputZod: InputSchema,
510
622
  inputJsonSchema: InputJsonSchema,
511
623
  allowedModels: null,
@@ -572,9 +684,12 @@ async function buildBody(apiId, input, ctx) {
572
684
  }
573
685
  body.prompt = input.prompt;
574
686
  body[refs.key] = refs.value;
687
+ if (refs.also) Object.assign(body, refs.also);
575
688
  return { body, dropped: refs.dropped };
576
689
  }
577
690
  async function run2(input, ctx) {
691
+ const paramsGate = paramsWithoutModel(input.params, input.model);
692
+ if (paramsGate) return { kind: "text", text: paramsGate };
578
693
  let resolved = input;
579
694
  try {
580
695
  if (input.image_paths && input.image_paths.length > 0) {
@@ -606,6 +721,7 @@ async function run2(input, ctx) {
606
721
  };
607
722
  const { body, dropped } = await buildBody(apiId, resolved, ctx);
608
723
  const droppedNote = dropped > 0 ? ` (${dropped} reference${dropped === 1 ? "" : "s"} dropped \u2014 ${apiId} accepts fewer)` : "";
724
+ const paramsNote = applyParams(body, input.params, input.model, apiId);
609
725
  const submitted = await ctx.client.run(apiId, body);
610
726
  const result = await pollJobUntilDoneOrHandoff(
611
727
  submitted.jobId,
@@ -616,9 +732,12 @@ async function run2(input, ctx) {
616
732
  sleep: ctx.pollOverrides?.sleep
617
733
  }
618
734
  );
619
- return pollResultToSkillResult(
620
- result,
621
- (urls) => urls.length === 0 ? `Edit job ${submitted.jobId} completed but returned no assets.` : urls.length === 1 ? `Edit complete${droppedNote}. Asset: ${urls[0]}` : `Edit produced ${urls.length} assets${droppedNote}. URLs: ${urls.join(", ")}`
735
+ return prefixNote(
736
+ pollResultToSkillResult(
737
+ result,
738
+ (urls) => urls.length === 0 ? `Edit job ${submitted.jobId} completed but returned no assets.` : urls.length === 1 ? `Edit complete${droppedNote}. Asset: ${urls[0]}` : `Edit produced ${urls.length} assets${droppedNote}. URLs: ${urls.join(", ")}`
739
+ ),
740
+ paramsNote
622
741
  );
623
742
  }
624
743
  var DEFAULT_MODEL2, ALLOWED_MODELS, InputSchema2, InputJsonSchema2, editSkill;
@@ -628,6 +747,7 @@ var init_edit = __esm({
628
747
  init_types();
629
748
  init_upload_helpers();
630
749
  init_schema_snap();
750
+ init_params();
631
751
  DEFAULT_MODEL2 = "wan-2.7-image-edit";
632
752
  ALLOWED_MODELS = [
633
753
  "wan-2.7-image-edit",
@@ -651,7 +771,8 @@ var init_edit = __esm({
651
771
  model: zod.z.enum(ALLOWED_MODELS).optional().describe(
652
772
  "Override the default edit model. Defaults to wan-2.7-image-edit (most popular)."
653
773
  ),
654
- aspect_ratio: zod.z.string().optional().describe('Aspect ratio when supported (e.g. "1:1", "16:9", "match_input_image").')
774
+ aspect_ratio: zod.z.string().optional().describe('Aspect ratio when supported (e.g. "1:1", "16:9", "match_input_image").'),
775
+ params: ParamsInput
655
776
  });
656
777
  InputJsonSchema2 = {
657
778
  type: "object",
@@ -685,7 +806,8 @@ var init_edit = __esm({
685
806
  enum: [...ALLOWED_MODELS],
686
807
  description: "Override edit model. Default: wan-2.7-image-edit."
687
808
  },
688
- aspect_ratio: { type: "string" }
809
+ aspect_ratio: { type: "string" },
810
+ params: PARAMS_JSON_SCHEMA
689
811
  },
690
812
  required: ["prompt"]
691
813
  };
@@ -701,6 +823,8 @@ var init_edit = __esm({
701
823
  }
702
824
  });
703
825
  async function run3(input, ctx) {
826
+ const paramsGate = paramsWithoutModel(input.params, input.model);
827
+ if (paramsGate) return { kind: "text", text: paramsGate };
704
828
  const requested = input.model ?? DEFAULT_MODEL3;
705
829
  if (!ALLOWED.includes(requested)) {
706
830
  return {
@@ -709,24 +833,40 @@ async function run3(input, ctx) {
709
833
  };
710
834
  }
711
835
  const apiId = requested;
712
- if (REQUIRES_REFERENCE.has(apiId) && !input.reference_image_url) {
836
+ const refUrls = (input.reference_image_urls && input.reference_image_urls.length > 0 ? input.reference_image_urls : input.reference_image_url ? [input.reference_image_url] : []).filter((u, i, all) => all.indexOf(u) === i);
837
+ const wrongKind = refUrls.find((u) => isVideoUrl(u) || isAudioUrl(u));
838
+ if (wrongKind) {
713
839
  return {
714
840
  kind: "text",
715
- text: `pixeldojo_character with model="${apiId}" requires reference_image_url (the source image of the character to edit). For generation from a prompt alone, use model="${DEFAULT_MODEL3}".`
841
+ text: `pixeldojo_character works on still images, and ${wrongKind} is not one. Use pixeldojo_generate or pixeldojo_storyboard with video_url to work from a clip (or image_url for an audio track \u2014 they route to the reference engine). For this treatment, give me a still frame.`
842
+ };
843
+ }
844
+ if (REQUIRES_REFERENCE.has(apiId) && refUrls.length === 0) {
845
+ return {
846
+ kind: "text",
847
+ text: `pixeldojo_character with model="${apiId}" requires a reference image (reference_image_url / reference_image_urls \u2014 the source image of the character to edit). For generation from a prompt alone, use model="${DEFAULT_MODEL3}".`
716
848
  };
717
849
  }
718
850
  const body = { prompt: input.prompt };
719
- if (input.reference_image_url) {
720
- if (apiId === "flux-edit") {
721
- body.input_image = input.reference_image_url;
851
+ let dropped = 0;
852
+ if (refUrls.length > 0) {
853
+ const refs = await resolveImageRefs(apiId, refUrls, ctx.client);
854
+ if (refs) {
855
+ body[refs.key] = refs.value;
856
+ if (refs.also) Object.assign(body, refs.also);
857
+ dropped = refs.dropped;
722
858
  } else {
723
- body.image_urls = [input.reference_image_url];
859
+ body[FALLBACK_REF_KEY[apiId]] = refUrls;
724
860
  }
725
861
  }
726
862
  const snappedAspect = await snapAspectRatio(apiId, input.aspect_ratio, ctx.client, {
727
863
  onUnknown: "omit"
728
864
  });
729
865
  if (snappedAspect !== void 0) body.aspect_ratio = snappedAspect;
866
+ const note = [
867
+ dropped > 0 ? `${dropped} reference${dropped === 1 ? "" : "s"} dropped \u2014 ${apiId} accepts fewer.` : void 0,
868
+ applyParams(body, input.params, input.model, apiId)
869
+ ].filter(Boolean).join(" ");
730
870
  const submitted = await ctx.client.run(apiId, body);
731
871
  const result = await pollJobUntilDoneOrHandoff(
732
872
  submitted.jobId,
@@ -737,27 +877,49 @@ async function run3(input, ctx) {
737
877
  sleep: ctx.pollOverrides?.sleep
738
878
  }
739
879
  );
740
- return pollResultToSkillResult(
741
- result,
742
- (urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Character shot ready. Asset: ${urls[0]}`
880
+ return prefixNote(
881
+ pollResultToSkillResult(
882
+ result,
883
+ (urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Character shot ready. Asset: ${urls[0]}`
884
+ ),
885
+ note
743
886
  );
744
887
  }
745
- var ALLOWED, DEFAULT_MODEL3, REQUIRES_REFERENCE, InputSchema3, InputJsonSchema3, characterSkill;
888
+ var ALLOWED, DEFAULT_MODEL3, MAX_REFERENCES, REQUIRES_REFERENCE, FALLBACK_REF_KEY, InputSchema3, InputJsonSchema3, characterSkill;
746
889
  var init_character = __esm({
747
890
  "src/skills/character.ts"() {
748
891
  init_poll();
749
892
  init_types();
750
893
  init_schema_snap();
751
- ALLOWED = ["seedream-5", "seedream-4", "flux-edit", "gpt-image-2-edit"];
894
+ init_params();
895
+ ALLOWED = [
896
+ "seedream-5",
897
+ "seedream-4",
898
+ "google-nano-banana",
899
+ "wan-2.7-image-edit",
900
+ "qwen-image-2.0-pro"
901
+ ];
752
902
  DEFAULT_MODEL3 = "seedream-5";
753
- REQUIRES_REFERENCE = /* @__PURE__ */ new Set(["flux-edit", "gpt-image-2-edit"]);
903
+ MAX_REFERENCES = 9;
904
+ REQUIRES_REFERENCE = /* @__PURE__ */ new Set(["wan-2.7-image-edit"]);
905
+ FALLBACK_REF_KEY = {
906
+ "seedream-5": "image_urls",
907
+ "seedream-4": "image_urls",
908
+ "google-nano-banana": "image_input",
909
+ "wan-2.7-image-edit": "image_urls",
910
+ "qwen-image-2.0-pro": "reference_images"
911
+ };
754
912
  InputSchema3 = zod.z.object({
755
913
  prompt: zod.z.string().min(1).max(8e3),
756
914
  model: zod.z.string().optional().describe(`One of: ${ALLOWED.join(", ")}. Defaults to ${DEFAULT_MODEL3}.`),
757
915
  reference_image_url: zod.z.string().url().optional().describe(
758
- "Reference image of the character. Required by flux-edit and gpt-image-2-edit; optional for seedream-5/seedream-4."
916
+ "Single reference image of the character. Prefer reference_image_urls \u2014 more angles lock the face better."
759
917
  ),
760
- aspect_ratio: zod.z.string().optional().describe('Only honored for model="flux-edit".')
918
+ reference_image_urls: zod.z.array(zod.z.string().url()).min(1).max(MAX_REFERENCES).optional().describe(
919
+ `All references for this shot (1-${MAX_REFERENCES}), most important first. Multiple angles of the same person lock the face far better than one photo. Models that take fewer are clamped to their own cap.`
920
+ ),
921
+ aspect_ratio: zod.z.string().optional().describe("Honored where the model declares the field."),
922
+ params: ParamsInput
761
923
  });
762
924
  InputJsonSchema3 = {
763
925
  type: "object",
@@ -768,14 +930,26 @@ var init_character = __esm({
768
930
  enum: ALLOWED,
769
931
  description: `One of: ${ALLOWED.join(", ")}. Defaults to ${DEFAULT_MODEL3}.`
770
932
  },
771
- reference_image_url: { type: "string", format: "uri" },
772
- aspect_ratio: { type: "string" }
933
+ reference_image_url: {
934
+ type: "string",
935
+ format: "uri",
936
+ description: "Single reference. Prefer reference_image_urls."
937
+ },
938
+ reference_image_urls: {
939
+ type: "array",
940
+ items: { type: "string", format: "uri" },
941
+ minItems: 1,
942
+ maxItems: MAX_REFERENCES,
943
+ description: "All references for this shot, most important first. Multiple angles of the same person lock the face far better than one photo."
944
+ },
945
+ aspect_ratio: { type: "string" },
946
+ params: PARAMS_JSON_SCHEMA
773
947
  },
774
948
  required: ["prompt"]
775
949
  };
776
950
  characterSkill = {
777
951
  name: "pixeldojo_character",
778
- description: "Generate or edit consistent characters across shots. Pass a reference_image_url for identity locking (seedream-5 by default).",
952
+ description: "Generate or edit consistent characters across shots. Pass reference_image_urls (up to 9) to lock identity \u2014 send every photo of the person you have, since more angles hold the face far better than one. Defaults to seedream-5.",
779
953
  inputZod: InputSchema3,
780
954
  inputJsonSchema: InputJsonSchema3,
781
955
  allowedModels: [...ALLOWED],
@@ -785,6 +959,8 @@ var init_character = __esm({
785
959
  }
786
960
  });
787
961
  async function run4(input, ctx) {
962
+ const paramsGate = paramsWithoutModel(input.params, input.model);
963
+ if (paramsGate) return { kind: "text", text: paramsGate };
788
964
  const requested = input.model ?? DEFAULT_MODEL4;
789
965
  if (!ALLOWED2.includes(requested)) {
790
966
  return {
@@ -792,6 +968,20 @@ async function run4(input, ctx) {
792
968
  text: `Unknown model "${requested}" for pixeldojo_storyboard. Allowed: ${ALLOWED2.join(", ")}.`
793
969
  };
794
970
  }
971
+ if (input.image_url || input.video_url) {
972
+ return generateSkill.run(
973
+ {
974
+ prompt: input.brief,
975
+ model: input.model ?? VIDEO_REFERENCE_MODEL,
976
+ image_url: input.image_url,
977
+ video_url: input.video_url,
978
+ aspect_ratio: input.aspect_ratio,
979
+ duration: input.duration,
980
+ params: input.params
981
+ },
982
+ ctx
983
+ );
984
+ }
795
985
  const apiId = requested;
796
986
  const [snappedAspect, snappedDuration] = await Promise.all([
797
987
  snapAspectRatio(apiId, input.aspect_ratio, ctx.client),
@@ -800,6 +990,7 @@ async function run4(input, ctx) {
800
990
  const body = { prompt: input.brief };
801
991
  if (snappedAspect !== void 0) body.aspect_ratio = snappedAspect;
802
992
  if (snappedDuration !== void 0) body.duration = snappedDuration;
993
+ const note = applyParams(body, input.params, input.model, apiId);
803
994
  const submitted = await ctx.client.run(apiId, body);
804
995
  const result = await pollJobUntilDoneOrHandoff(
805
996
  submitted.jobId,
@@ -810,9 +1001,12 @@ async function run4(input, ctx) {
810
1001
  sleep: ctx.pollOverrides?.sleep
811
1002
  }
812
1003
  );
813
- return pollResultToSkillResult(
814
- result,
815
- (urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Storyboard preview ready. Asset: ${urls[0]}`
1004
+ return prefixNote(
1005
+ pollResultToSkillResult(
1006
+ result,
1007
+ (urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Storyboard preview ready. Asset: ${urls[0]}`
1008
+ ),
1009
+ note
816
1010
  );
817
1011
  }
818
1012
  var ALLOWED2, DEFAULT_MODEL4, InputSchema4, InputJsonSchema4, storyboardSkill;
@@ -821,18 +1015,24 @@ var init_storyboard = __esm({
821
1015
  init_poll();
822
1016
  init_types();
823
1017
  init_schema_snap();
1018
+ init_generate();
1019
+ init_params();
824
1020
  ALLOWED2 = [
825
1021
  "veo-3.1-fast",
826
1022
  "veo-3.1-standard",
827
1023
  "kling-video-v3-standard-text",
828
- "seedance-2-high"
1024
+ "seedance-2-high",
1025
+ VIDEO_REFERENCE_MODEL
829
1026
  ];
830
1027
  DEFAULT_MODEL4 = "veo-3.1-fast";
831
1028
  InputSchema4 = zod.z.object({
832
1029
  brief: zod.z.string().min(1).max(8e3).describe("Storyboard brief: scene, shots, mood, action."),
833
1030
  model: zod.z.string().optional().describe(`One of: ${ALLOWED2.join(", ")}. Defaults to ${DEFAULT_MODEL4}.`),
834
1031
  aspect_ratio: zod.z.string().optional(),
835
- duration: zod.z.number().int().positive().optional()
1032
+ duration: zod.z.number().int().positive().optional(),
1033
+ image_url: zod.z.string().url().optional().describe("Reference still to build the shot from (character, product, location)."),
1034
+ video_url: zod.z.string().url().optional().describe("Source clip to work from. Use this for videos; image_url is for still frames."),
1035
+ params: ParamsInput
836
1036
  });
837
1037
  InputJsonSchema4 = {
838
1038
  type: "object",
@@ -847,13 +1047,24 @@ var init_storyboard = __esm({
847
1047
  description: `One of: ${ALLOWED2.join(", ")}. Defaults to ${DEFAULT_MODEL4}.`
848
1048
  },
849
1049
  aspect_ratio: { type: "string" },
850
- duration: { type: "integer" }
1050
+ duration: { type: "integer" },
1051
+ image_url: {
1052
+ type: "string",
1053
+ format: "uri",
1054
+ description: "Reference still to build the shot from."
1055
+ },
1056
+ video_url: {
1057
+ type: "string",
1058
+ format: "uri",
1059
+ description: "Source clip to work from. Use this for videos; image_url is for stills."
1060
+ },
1061
+ params: PARAMS_JSON_SCHEMA
851
1062
  },
852
1063
  required: ["brief"]
853
1064
  };
854
1065
  storyboardSkill = {
855
1066
  name: "pixeldojo_storyboard",
856
- description: "Generate a video storyboard preview from a single brief. v1: single-shot preview; multi-shot orchestration coming.",
1067
+ description: "Generate a video storyboard preview from a single brief. Pass image_url for a reference still or video_url for a source clip. v1: single-shot preview; multi-shot orchestration coming.",
857
1068
  inputZod: InputSchema4,
858
1069
  inputJsonSchema: InputJsonSchema4,
859
1070
  allowedModels: [...ALLOWED2],
@@ -2183,7 +2394,7 @@ function makeRedactedLogger(target = console) {
2183
2394
  }
2184
2395
 
2185
2396
  // src/server.ts
2186
- var PACKAGE_VERSION = "0.9.1" ;
2397
+ var PACKAGE_VERSION = "0.10.0" ;
2187
2398
  function buildServer(options) {
2188
2399
  if (!options.apiKey) {
2189
2400
  throw new Error(