@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/dist/index.js CHANGED
@@ -183,19 +183,23 @@ var init_upload_helpers = __esm({
183
183
  });
184
184
 
185
185
  // src/skills/schema-snap.ts
186
- function getFieldConstraints(apiId, client) {
186
+ function getModelInfo(apiId, client) {
187
187
  const cached = schemaCache.get(apiId);
188
188
  if (cached) return cached;
189
189
  if (typeof client?.getModelSchema !== "function") return Promise.resolve(null);
190
- const pending = Promise.resolve().then(() => client.getModelSchema(apiId)).then(
191
- (res) => res.schema?.properties ?? {}
192
- ).catch(() => {
190
+ const pending = Promise.resolve().then(() => client.getModelSchema(apiId)).then((res) => {
191
+ const body = res;
192
+ return { props: body.schema?.properties ?? {}, media: body.media };
193
+ }).catch(() => {
193
194
  schemaCache.delete(apiId);
194
195
  return null;
195
196
  });
196
197
  schemaCache.set(apiId, pending);
197
198
  return pending;
198
199
  }
200
+ async function getFieldConstraints(apiId, client) {
201
+ return (await getModelInfo(apiId, client))?.props ?? null;
202
+ }
199
203
  function parseAspectRatio(ar) {
200
204
  const m = /^\s*(\d+)\s*:\s*(\d+)\s*$/.exec(ar);
201
205
  if (!m) return null;
@@ -231,11 +235,20 @@ async function snapAspectRatio(apiId, requested, client, opts = {}) {
231
235
  if (!field.enum || field.enum.length === 0) return requested;
232
236
  return nearestAspectFromEnum(requested, field.enum);
233
237
  }
234
- async function resolveReferenceField(apiId, client) {
235
- const props = await getFieldConstraints(apiId, client);
236
- if (props === null) return void 0;
237
- for (const name of REFERENCE_FIELD_PRIORITY) {
238
- const field = props[name];
238
+ async function resolveMediaSlot(apiId, kind, client) {
239
+ const info = await getModelInfo(apiId, client);
240
+ if (info === null) return void 0;
241
+ const declared = info.media?.[kind];
242
+ if (declared) {
243
+ return {
244
+ name: declared.key,
245
+ isArray: info.props[declared.key]?.type === "array",
246
+ also: declared.also
247
+ };
248
+ }
249
+ if (info.media && kind !== "audio") return null;
250
+ for (const name of FALLBACK_KEYS[kind]) {
251
+ const field = info.props[name];
239
252
  if (field) return { name, isArray: field.type === "array" };
240
253
  }
241
254
  return null;
@@ -295,21 +308,29 @@ async function snapDuration(apiId, requested, client, opts = {}) {
295
308
  }
296
309
  return requested;
297
310
  }
311
+ function isVideoUrl(url) {
312
+ return /\.(mp4|mov|webm|m4v)(?:[?#]|$)/i.test(url);
313
+ }
314
+ function isAudioUrl(url) {
315
+ return /\.(mp3|wav|m4a|aac|ogg|flac)(?:[?#]|$)/i.test(url);
316
+ }
298
317
  async function resolveImageRefs(apiId, urls, client) {
299
318
  if (urls.length === 0) return null;
300
- const props = await getFieldConstraints(apiId, client);
301
- if (props === null) return null;
302
- const key = REF_KEYS.find((k) => props[k] !== void 0);
319
+ const info = await getModelInfo(apiId, client);
320
+ if (info === null) return null;
321
+ const props = info.props;
322
+ const key = info.media ? info.media.image?.key : REF_KEYS.find((k) => props[k] !== void 0);
303
323
  if (!key) return null;
304
- const field = props[key];
324
+ const also = info.media?.image?.also;
325
+ const field = props[key] ?? {};
305
326
  if (field.type === "array") {
306
327
  const cap = typeof field.maxItems === "number" ? field.maxItems : urls.length;
307
328
  const kept = urls.slice(0, Math.max(1, cap));
308
- return { key, value: kept, dropped: urls.length - kept.length };
329
+ return { key, value: kept, dropped: urls.length - kept.length, also };
309
330
  }
310
- return { key, value: urls[0], dropped: urls.length - 1 };
331
+ return { key, value: urls[0], dropped: urls.length - 1, also };
311
332
  }
312
- var schemaCache, REFERENCE_FIELD_PRIORITY, REF_KEYS;
333
+ var schemaCache, REFERENCE_FIELD_PRIORITY, FALLBACK_KEYS, REF_KEYS;
313
334
  var init_schema_snap = __esm({
314
335
  "src/skills/schema-snap.ts"() {
315
336
  schemaCache = /* @__PURE__ */ new Map();
@@ -325,6 +346,11 @@ var init_schema_snap = __esm({
325
346
  "image_input",
326
347
  "images"
327
348
  ];
349
+ FALLBACK_KEYS = {
350
+ image: REFERENCE_FIELD_PRIORITY,
351
+ video: ["reference_videos"],
352
+ audio: ["reference_audios", "audio_url"]
353
+ };
328
354
  REF_KEYS = [
329
355
  "image_urls",
330
356
  "images",
@@ -337,7 +363,51 @@ var init_schema_snap = __esm({
337
363
  ];
338
364
  }
339
365
  });
340
- function buildPayload(apiId, input, refField, referenceUrls = []) {
366
+ function paramsWithoutModel(params, model) {
367
+ if (!params || Object.keys(params).length === 0) return null;
368
+ if (model) return null;
369
+ return "Those settings are model-specific. Tell me which model to use and I will apply them.";
370
+ }
371
+ function applyParams(body, params, paramsModel, apiId) {
372
+ if (!params || Object.keys(params).length === 0) return void 0;
373
+ if (paramsModel !== apiId) {
374
+ 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.`;
375
+ }
376
+ const rest = { ...params };
377
+ delete rest.prompt;
378
+ Object.assign(body, rest);
379
+ return "prompt" in params ? "I kept the prompt from the skill call and ignored the one in params." : void 0;
380
+ }
381
+ function prefixNote(result, note) {
382
+ if (!note) return result;
383
+ return { ...result, text: `${note} ${result.text}`.trim() };
384
+ }
385
+ var ParamsInput, PARAMS_JSON_SCHEMA;
386
+ var init_params = __esm({
387
+ "src/skills/params.ts"() {
388
+ ParamsInput = z.record(z.unknown()).optional();
389
+ PARAMS_JSON_SCHEMA = {
390
+ type: "object",
391
+ additionalProperties: true,
392
+ 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.'
393
+ };
394
+ }
395
+ });
396
+ function fallbackSlot(apiId, kind) {
397
+ return apiId === VIDEO_REFERENCE_MODEL ? VIDEO_REFERENCE_SLOTS[kind] : void 0;
398
+ }
399
+ function sourceMedia(videoUrl, referenceUrls) {
400
+ const clip = videoUrl && !isAudioUrl(videoUrl) ? videoUrl : void 0;
401
+ const stills = referenceUrls.filter((u) => !isAudioUrl(u));
402
+ const url = clip ?? stills[0];
403
+ return {
404
+ url,
405
+ isClip: Boolean(clip) || !!url && isVideoUrl(url),
406
+ audioUrl: [videoUrl, ...referenceUrls].find((u) => u && isAudioUrl(u)),
407
+ stills
408
+ };
409
+ }
410
+ function buildPayload(apiId, input, src, slot, audioSlot) {
341
411
  const body = { prompt: input.prompt };
342
412
  if (input.aspect_ratio) body.aspect_ratio = input.aspect_ratio;
343
413
  if (input.preset) body.preset = input.preset;
@@ -345,9 +415,9 @@ function buildPayload(apiId, input, refField, referenceUrls = []) {
345
415
  case "grok-video-extend":
346
416
  case "wan-video-edit":
347
417
  case "seedance-video-edit":
348
- if (input.image_url) {
349
- body.video_url = input.image_url;
350
- body.image_url = input.image_url;
418
+ if (src.url) {
419
+ body.video_url = src.url;
420
+ body.image_url = src.url;
351
421
  }
352
422
  if (input.duration !== void 0) {
353
423
  body.video_duration = input.duration;
@@ -355,14 +425,19 @@ function buildPayload(apiId, input, refField, referenceUrls = []) {
355
425
  }
356
426
  break;
357
427
  default:
358
- if (input.image_url) {
359
- const refs = referenceUrls.length > 0 ? referenceUrls : [input.image_url];
360
- if (refField) {
361
- body[refField.name] = refField.isArray ? refs : refs[0];
428
+ if (src.url) {
429
+ const refs = src.isClip ? [src.url] : src.stills.length > 0 ? src.stills : [src.url];
430
+ if (slot) {
431
+ body[slot.name] = slot.isArray ? refs : refs[0];
432
+ if (slot.also) Object.assign(body, slot.also);
362
433
  } else {
363
434
  body.image_url = refs[0];
364
435
  }
365
436
  }
437
+ if (src.audioUrl && audioSlot) {
438
+ body[audioSlot.name] = audioSlot.isArray ? [src.audioUrl] : src.audioUrl;
439
+ if (audioSlot.also) Object.assign(body, audioSlot.also);
440
+ }
366
441
  if (input.duration !== void 0) body.duration = input.duration;
367
442
  if (input.resolution) body.resolution = input.resolution;
368
443
  break;
@@ -370,7 +445,8 @@ function buildPayload(apiId, input, refField, referenceUrls = []) {
370
445
  return body;
371
446
  }
372
447
  async function run(input, ctx) {
373
- const apiId = input.model ?? DEFAULT_MODEL;
448
+ const paramsGate = paramsWithoutModel(input.params, input.model);
449
+ if (paramsGate) return { kind: "text", text: paramsGate };
374
450
  let resolved = input;
375
451
  let referenceUrls = [];
376
452
  try {
@@ -391,11 +467,22 @@ async function run(input, ctx) {
391
467
  }
392
468
  throw err;
393
469
  }
470
+ const src = sourceMedia(input.video_url, referenceUrls);
471
+ let apiId = input.model ?? DEFAULT_MODEL;
472
+ let routeNote;
473
+ if ((src.isClip || src.audioUrl) && !VIDEO_INPUT_MODELS.has(apiId)) {
474
+ if (!input.model) {
475
+ apiId = VIDEO_REFERENCE_MODEL;
476
+ } else if (src.isClip && await resolveMediaSlot(input.model, "video", ctx.client) === null) {
477
+ apiId = VIDEO_REFERENCE_MODEL;
478
+ routeNote = `${input.model} can't work from a video clip, so I used ${VIDEO_REFERENCE_MODEL} instead.`;
479
+ }
480
+ }
394
481
  if (apiId === "grok-video-extend") {
395
- if (!resolved.image_url) {
482
+ if (!src.url) {
396
483
  return {
397
484
  kind: "text",
398
- text: 'pixeldojo_generate with model="grok-video-extend" requires image_url (the source video URL to extend, or image_path for a local file).'
485
+ text: 'pixeldojo_generate with model="grok-video-extend" requires video_url (the source video URL to extend, or image_path for a local file).'
399
486
  };
400
487
  }
401
488
  if (resolved.duration === void 0) {
@@ -406,17 +493,22 @@ async function run(input, ctx) {
406
493
  }
407
494
  }
408
495
  let refField;
496
+ let audioSlot;
409
497
  if (!VIDEO_INPUT_MODELS.has(apiId)) {
410
- const [aspect_ratio, duration, resolution, ref] = await Promise.all([
498
+ const [aspect_ratio, duration, resolution, ref, audio] = await Promise.all([
411
499
  snapAspectRatio(apiId, resolved.aspect_ratio, ctx.client),
412
500
  snapDuration(apiId, resolved.duration, ctx.client),
413
501
  snapResolution(apiId, resolved.resolution, ctx.client),
414
- resolved.image_url ? resolveReferenceField(apiId, ctx.client) : Promise.resolve(void 0)
502
+ src.url ? resolveMediaSlot(apiId, src.isClip ? "video" : "image", ctx.client) : Promise.resolve(void 0),
503
+ src.audioUrl ? resolveMediaSlot(apiId, "audio", ctx.client) : Promise.resolve(void 0)
415
504
  ]);
416
505
  resolved = { ...resolved, aspect_ratio, duration, resolution };
417
- refField = ref;
506
+ refField = ref === void 0 && src.url ? fallbackSlot(apiId, src.isClip ? "video" : "image") : ref;
507
+ audioSlot = audio === void 0 && src.audioUrl ? fallbackSlot(apiId, "audio") : audio;
418
508
  }
419
- const body = buildPayload(apiId, resolved, refField, referenceUrls);
509
+ const body = buildPayload(apiId, resolved, src, refField, audioSlot);
510
+ const paramsNote = applyParams(body, input.params, input.model, apiId);
511
+ const note = [routeNote, paramsNote].filter(Boolean).join(" ");
420
512
  const submitted = await ctx.client.run(apiId, body);
421
513
  const result = await pollJobUntilDoneOrHandoff(
422
514
  submitted.jobId,
@@ -427,18 +519,22 @@ async function run(input, ctx) {
427
519
  sleep: ctx.pollOverrides?.sleep
428
520
  }
429
521
  );
430
- return pollResultToSkillResult(
431
- result,
432
- (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(", ")}`
522
+ return prefixNote(
523
+ pollResultToSkillResult(
524
+ result,
525
+ (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(", ")}`
526
+ ),
527
+ note
433
528
  );
434
529
  }
435
- var InputSchema, DEFAULT_MODEL, VIDEO_INPUT_MODELS, InputJsonSchema, generateSkill;
530
+ var InputSchema, DEFAULT_MODEL, VIDEO_REFERENCE_MODEL, VIDEO_REFERENCE_SLOTS, VIDEO_INPUT_MODELS, InputJsonSchema, generateSkill;
436
531
  var init_generate = __esm({
437
532
  "src/skills/generate.ts"() {
438
533
  init_poll();
439
534
  init_types();
440
535
  init_upload_helpers();
441
536
  init_schema_snap();
537
+ init_params();
442
538
  InputSchema = z.object({
443
539
  prompt: z.string().min(1).max(8e3).describe("Prompt describing what to generate."),
444
540
  model: z.string().optional().describe(
@@ -453,6 +549,10 @@ var init_generate = __esm({
453
549
  'Output resolution when the model supports it (e.g. "480p", "720p", "1080p", "4k"). Lower tiers cost fewer credits. Omit to use the model default.'
454
550
  ),
455
551
  image_url: z.string().url().optional().describe("Reference image URL for image-to-image / image-to-video tools."),
552
+ video_url: z.string().url().optional().describe(
553
+ "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."
554
+ ),
555
+ params: ParamsInput,
456
556
  image_path: z.string().min(1).optional().describe(
457
557
  "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."
458
558
  ),
@@ -464,6 +564,12 @@ var init_generate = __esm({
464
564
  )
465
565
  });
466
566
  DEFAULT_MODEL = "google-nano-banana";
567
+ VIDEO_REFERENCE_MODEL = "seedance-2-reference";
568
+ VIDEO_REFERENCE_SLOTS = {
569
+ image: { name: "reference_images", isArray: true },
570
+ video: { name: "reference_videos", isArray: true },
571
+ audio: { name: "reference_audios", isArray: true }
572
+ };
467
573
  VIDEO_INPUT_MODELS = /* @__PURE__ */ new Set(["grok-video-extend", "wan-video-edit", "seedance-video-edit"]);
468
574
  InputJsonSchema = {
469
575
  type: "object",
@@ -484,6 +590,12 @@ var init_generate = __esm({
484
590
  description: 'Output resolution when supported (e.g. "480p", "720p", "1080p", "4k"). Lower tiers cost fewer credits.'
485
591
  },
486
592
  image_url: { type: "string", format: "uri" },
593
+ video_url: {
594
+ type: "string",
595
+ format: "uri",
596
+ description: "Source video clip to animate / restyle / extend. Use this for videos; image_url is for still frames."
597
+ },
598
+ params: PARAMS_JSON_SCHEMA,
487
599
  image_urls: {
488
600
  type: "array",
489
601
  items: { type: "string", format: "uri" },
@@ -503,7 +615,7 @@ var init_generate = __esm({
503
615
  };
504
616
  generateSkill = {
505
617
  name: "pixeldojo_generate",
506
- 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.",
618
+ 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`.",
507
619
  inputZod: InputSchema,
508
620
  inputJsonSchema: InputJsonSchema,
509
621
  allowedModels: null,
@@ -570,9 +682,12 @@ async function buildBody(apiId, input, ctx) {
570
682
  }
571
683
  body.prompt = input.prompt;
572
684
  body[refs.key] = refs.value;
685
+ if (refs.also) Object.assign(body, refs.also);
573
686
  return { body, dropped: refs.dropped };
574
687
  }
575
688
  async function run2(input, ctx) {
689
+ const paramsGate = paramsWithoutModel(input.params, input.model);
690
+ if (paramsGate) return { kind: "text", text: paramsGate };
576
691
  let resolved = input;
577
692
  try {
578
693
  if (input.image_paths && input.image_paths.length > 0) {
@@ -604,6 +719,7 @@ async function run2(input, ctx) {
604
719
  };
605
720
  const { body, dropped } = await buildBody(apiId, resolved, ctx);
606
721
  const droppedNote = dropped > 0 ? ` (${dropped} reference${dropped === 1 ? "" : "s"} dropped \u2014 ${apiId} accepts fewer)` : "";
722
+ const paramsNote = applyParams(body, input.params, input.model, apiId);
607
723
  const submitted = await ctx.client.run(apiId, body);
608
724
  const result = await pollJobUntilDoneOrHandoff(
609
725
  submitted.jobId,
@@ -614,9 +730,12 @@ async function run2(input, ctx) {
614
730
  sleep: ctx.pollOverrides?.sleep
615
731
  }
616
732
  );
617
- return pollResultToSkillResult(
618
- result,
619
- (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(", ")}`
733
+ return prefixNote(
734
+ pollResultToSkillResult(
735
+ result,
736
+ (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(", ")}`
737
+ ),
738
+ paramsNote
620
739
  );
621
740
  }
622
741
  var DEFAULT_MODEL2, ALLOWED_MODELS, InputSchema2, InputJsonSchema2, editSkill;
@@ -626,6 +745,7 @@ var init_edit = __esm({
626
745
  init_types();
627
746
  init_upload_helpers();
628
747
  init_schema_snap();
748
+ init_params();
629
749
  DEFAULT_MODEL2 = "wan-2.7-image-edit";
630
750
  ALLOWED_MODELS = [
631
751
  "wan-2.7-image-edit",
@@ -649,7 +769,8 @@ var init_edit = __esm({
649
769
  model: z.enum(ALLOWED_MODELS).optional().describe(
650
770
  "Override the default edit model. Defaults to wan-2.7-image-edit (most popular)."
651
771
  ),
652
- aspect_ratio: z.string().optional().describe('Aspect ratio when supported (e.g. "1:1", "16:9", "match_input_image").')
772
+ aspect_ratio: z.string().optional().describe('Aspect ratio when supported (e.g. "1:1", "16:9", "match_input_image").'),
773
+ params: ParamsInput
653
774
  });
654
775
  InputJsonSchema2 = {
655
776
  type: "object",
@@ -683,7 +804,8 @@ var init_edit = __esm({
683
804
  enum: [...ALLOWED_MODELS],
684
805
  description: "Override edit model. Default: wan-2.7-image-edit."
685
806
  },
686
- aspect_ratio: { type: "string" }
807
+ aspect_ratio: { type: "string" },
808
+ params: PARAMS_JSON_SCHEMA
687
809
  },
688
810
  required: ["prompt"]
689
811
  };
@@ -699,6 +821,8 @@ var init_edit = __esm({
699
821
  }
700
822
  });
701
823
  async function run3(input, ctx) {
824
+ const paramsGate = paramsWithoutModel(input.params, input.model);
825
+ if (paramsGate) return { kind: "text", text: paramsGate };
702
826
  const requested = input.model ?? DEFAULT_MODEL3;
703
827
  if (!ALLOWED.includes(requested)) {
704
828
  return {
@@ -707,24 +831,40 @@ async function run3(input, ctx) {
707
831
  };
708
832
  }
709
833
  const apiId = requested;
710
- if (REQUIRES_REFERENCE.has(apiId) && !input.reference_image_url) {
834
+ 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);
835
+ const wrongKind = refUrls.find((u) => isVideoUrl(u) || isAudioUrl(u));
836
+ if (wrongKind) {
711
837
  return {
712
838
  kind: "text",
713
- 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}".`
839
+ 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.`
840
+ };
841
+ }
842
+ if (REQUIRES_REFERENCE.has(apiId) && refUrls.length === 0) {
843
+ return {
844
+ kind: "text",
845
+ 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}".`
714
846
  };
715
847
  }
716
848
  const body = { prompt: input.prompt };
717
- if (input.reference_image_url) {
718
- if (apiId === "flux-edit") {
719
- body.input_image = input.reference_image_url;
849
+ let dropped = 0;
850
+ if (refUrls.length > 0) {
851
+ const refs = await resolveImageRefs(apiId, refUrls, ctx.client);
852
+ if (refs) {
853
+ body[refs.key] = refs.value;
854
+ if (refs.also) Object.assign(body, refs.also);
855
+ dropped = refs.dropped;
720
856
  } else {
721
- body.image_urls = [input.reference_image_url];
857
+ body[FALLBACK_REF_KEY[apiId]] = refUrls;
722
858
  }
723
859
  }
724
860
  const snappedAspect = await snapAspectRatio(apiId, input.aspect_ratio, ctx.client, {
725
861
  onUnknown: "omit"
726
862
  });
727
863
  if (snappedAspect !== void 0) body.aspect_ratio = snappedAspect;
864
+ const note = [
865
+ dropped > 0 ? `${dropped} reference${dropped === 1 ? "" : "s"} dropped \u2014 ${apiId} accepts fewer.` : void 0,
866
+ applyParams(body, input.params, input.model, apiId)
867
+ ].filter(Boolean).join(" ");
728
868
  const submitted = await ctx.client.run(apiId, body);
729
869
  const result = await pollJobUntilDoneOrHandoff(
730
870
  submitted.jobId,
@@ -735,27 +875,49 @@ async function run3(input, ctx) {
735
875
  sleep: ctx.pollOverrides?.sleep
736
876
  }
737
877
  );
738
- return pollResultToSkillResult(
739
- result,
740
- (urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Character shot ready. Asset: ${urls[0]}`
878
+ return prefixNote(
879
+ pollResultToSkillResult(
880
+ result,
881
+ (urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Character shot ready. Asset: ${urls[0]}`
882
+ ),
883
+ note
741
884
  );
742
885
  }
743
- var ALLOWED, DEFAULT_MODEL3, REQUIRES_REFERENCE, InputSchema3, InputJsonSchema3, characterSkill;
886
+ var ALLOWED, DEFAULT_MODEL3, MAX_REFERENCES, REQUIRES_REFERENCE, FALLBACK_REF_KEY, InputSchema3, InputJsonSchema3, characterSkill;
744
887
  var init_character = __esm({
745
888
  "src/skills/character.ts"() {
746
889
  init_poll();
747
890
  init_types();
748
891
  init_schema_snap();
749
- ALLOWED = ["seedream-5", "seedream-4", "flux-edit", "gpt-image-2-edit"];
892
+ init_params();
893
+ ALLOWED = [
894
+ "seedream-5",
895
+ "seedream-4",
896
+ "google-nano-banana",
897
+ "wan-2.7-image-edit",
898
+ "qwen-image-2.0-pro"
899
+ ];
750
900
  DEFAULT_MODEL3 = "seedream-5";
751
- REQUIRES_REFERENCE = /* @__PURE__ */ new Set(["flux-edit", "gpt-image-2-edit"]);
901
+ MAX_REFERENCES = 9;
902
+ REQUIRES_REFERENCE = /* @__PURE__ */ new Set(["wan-2.7-image-edit"]);
903
+ FALLBACK_REF_KEY = {
904
+ "seedream-5": "image_urls",
905
+ "seedream-4": "image_urls",
906
+ "google-nano-banana": "image_input",
907
+ "wan-2.7-image-edit": "image_urls",
908
+ "qwen-image-2.0-pro": "reference_images"
909
+ };
752
910
  InputSchema3 = z.object({
753
911
  prompt: z.string().min(1).max(8e3),
754
912
  model: z.string().optional().describe(`One of: ${ALLOWED.join(", ")}. Defaults to ${DEFAULT_MODEL3}.`),
755
913
  reference_image_url: z.string().url().optional().describe(
756
- "Reference image of the character. Required by flux-edit and gpt-image-2-edit; optional for seedream-5/seedream-4."
914
+ "Single reference image of the character. Prefer reference_image_urls \u2014 more angles lock the face better."
757
915
  ),
758
- aspect_ratio: z.string().optional().describe('Only honored for model="flux-edit".')
916
+ reference_image_urls: z.array(z.string().url()).min(1).max(MAX_REFERENCES).optional().describe(
917
+ `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.`
918
+ ),
919
+ aspect_ratio: z.string().optional().describe("Honored where the model declares the field."),
920
+ params: ParamsInput
759
921
  });
760
922
  InputJsonSchema3 = {
761
923
  type: "object",
@@ -766,14 +928,26 @@ var init_character = __esm({
766
928
  enum: ALLOWED,
767
929
  description: `One of: ${ALLOWED.join(", ")}. Defaults to ${DEFAULT_MODEL3}.`
768
930
  },
769
- reference_image_url: { type: "string", format: "uri" },
770
- aspect_ratio: { type: "string" }
931
+ reference_image_url: {
932
+ type: "string",
933
+ format: "uri",
934
+ description: "Single reference. Prefer reference_image_urls."
935
+ },
936
+ reference_image_urls: {
937
+ type: "array",
938
+ items: { type: "string", format: "uri" },
939
+ minItems: 1,
940
+ maxItems: MAX_REFERENCES,
941
+ description: "All references for this shot, most important first. Multiple angles of the same person lock the face far better than one photo."
942
+ },
943
+ aspect_ratio: { type: "string" },
944
+ params: PARAMS_JSON_SCHEMA
771
945
  },
772
946
  required: ["prompt"]
773
947
  };
774
948
  characterSkill = {
775
949
  name: "pixeldojo_character",
776
- description: "Generate or edit consistent characters across shots. Pass a reference_image_url for identity locking (seedream-5 by default).",
950
+ 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.",
777
951
  inputZod: InputSchema3,
778
952
  inputJsonSchema: InputJsonSchema3,
779
953
  allowedModels: [...ALLOWED],
@@ -783,6 +957,8 @@ var init_character = __esm({
783
957
  }
784
958
  });
785
959
  async function run4(input, ctx) {
960
+ const paramsGate = paramsWithoutModel(input.params, input.model);
961
+ if (paramsGate) return { kind: "text", text: paramsGate };
786
962
  const requested = input.model ?? DEFAULT_MODEL4;
787
963
  if (!ALLOWED2.includes(requested)) {
788
964
  return {
@@ -790,6 +966,20 @@ async function run4(input, ctx) {
790
966
  text: `Unknown model "${requested}" for pixeldojo_storyboard. Allowed: ${ALLOWED2.join(", ")}.`
791
967
  };
792
968
  }
969
+ if (input.image_url || input.video_url) {
970
+ return generateSkill.run(
971
+ {
972
+ prompt: input.brief,
973
+ model: input.model ?? VIDEO_REFERENCE_MODEL,
974
+ image_url: input.image_url,
975
+ video_url: input.video_url,
976
+ aspect_ratio: input.aspect_ratio,
977
+ duration: input.duration,
978
+ params: input.params
979
+ },
980
+ ctx
981
+ );
982
+ }
793
983
  const apiId = requested;
794
984
  const [snappedAspect, snappedDuration] = await Promise.all([
795
985
  snapAspectRatio(apiId, input.aspect_ratio, ctx.client),
@@ -798,6 +988,7 @@ async function run4(input, ctx) {
798
988
  const body = { prompt: input.brief };
799
989
  if (snappedAspect !== void 0) body.aspect_ratio = snappedAspect;
800
990
  if (snappedDuration !== void 0) body.duration = snappedDuration;
991
+ const note = applyParams(body, input.params, input.model, apiId);
801
992
  const submitted = await ctx.client.run(apiId, body);
802
993
  const result = await pollJobUntilDoneOrHandoff(
803
994
  submitted.jobId,
@@ -808,9 +999,12 @@ async function run4(input, ctx) {
808
999
  sleep: ctx.pollOverrides?.sleep
809
1000
  }
810
1001
  );
811
- return pollResultToSkillResult(
812
- result,
813
- (urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Storyboard preview ready. Asset: ${urls[0]}`
1002
+ return prefixNote(
1003
+ pollResultToSkillResult(
1004
+ result,
1005
+ (urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Storyboard preview ready. Asset: ${urls[0]}`
1006
+ ),
1007
+ note
814
1008
  );
815
1009
  }
816
1010
  var ALLOWED2, DEFAULT_MODEL4, InputSchema4, InputJsonSchema4, storyboardSkill;
@@ -819,18 +1013,24 @@ var init_storyboard = __esm({
819
1013
  init_poll();
820
1014
  init_types();
821
1015
  init_schema_snap();
1016
+ init_generate();
1017
+ init_params();
822
1018
  ALLOWED2 = [
823
1019
  "veo-3.1-fast",
824
1020
  "veo-3.1-standard",
825
1021
  "kling-video-v3-standard-text",
826
- "seedance-2-high"
1022
+ "seedance-2-high",
1023
+ VIDEO_REFERENCE_MODEL
827
1024
  ];
828
1025
  DEFAULT_MODEL4 = "veo-3.1-fast";
829
1026
  InputSchema4 = z.object({
830
1027
  brief: z.string().min(1).max(8e3).describe("Storyboard brief: scene, shots, mood, action."),
831
1028
  model: z.string().optional().describe(`One of: ${ALLOWED2.join(", ")}. Defaults to ${DEFAULT_MODEL4}.`),
832
1029
  aspect_ratio: z.string().optional(),
833
- duration: z.number().int().positive().optional()
1030
+ duration: z.number().int().positive().optional(),
1031
+ image_url: z.string().url().optional().describe("Reference still to build the shot from (character, product, location)."),
1032
+ video_url: z.string().url().optional().describe("Source clip to work from. Use this for videos; image_url is for still frames."),
1033
+ params: ParamsInput
834
1034
  });
835
1035
  InputJsonSchema4 = {
836
1036
  type: "object",
@@ -845,13 +1045,24 @@ var init_storyboard = __esm({
845
1045
  description: `One of: ${ALLOWED2.join(", ")}. Defaults to ${DEFAULT_MODEL4}.`
846
1046
  },
847
1047
  aspect_ratio: { type: "string" },
848
- duration: { type: "integer" }
1048
+ duration: { type: "integer" },
1049
+ image_url: {
1050
+ type: "string",
1051
+ format: "uri",
1052
+ description: "Reference still to build the shot from."
1053
+ },
1054
+ video_url: {
1055
+ type: "string",
1056
+ format: "uri",
1057
+ description: "Source clip to work from. Use this for videos; image_url is for stills."
1058
+ },
1059
+ params: PARAMS_JSON_SCHEMA
849
1060
  },
850
1061
  required: ["brief"]
851
1062
  };
852
1063
  storyboardSkill = {
853
1064
  name: "pixeldojo_storyboard",
854
- description: "Generate a video storyboard preview from a single brief. v1: single-shot preview; multi-shot orchestration coming.",
1065
+ 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.",
855
1066
  inputZod: InputSchema4,
856
1067
  inputJsonSchema: InputJsonSchema4,
857
1068
  allowedModels: [...ALLOWED2],
@@ -2181,7 +2392,7 @@ function makeRedactedLogger(target = console) {
2181
2392
  }
2182
2393
 
2183
2394
  // src/server.ts
2184
- var PACKAGE_VERSION = "0.9.1" ;
2395
+ var PACKAGE_VERSION = "0.10.0" ;
2185
2396
  function buildServer(options) {
2186
2397
  if (!options.apiKey) {
2187
2398
  throw new Error(