@pixeldojo/mcp 0.9.1 → 0.11.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 +13 -1
- package/dist/cli.cjs +338 -99
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +338 -99
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +337 -98
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +337 -98
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -185,19 +185,23 @@ var init_upload_helpers = __esm({
|
|
|
185
185
|
});
|
|
186
186
|
|
|
187
187
|
// src/skills/schema-snap.ts
|
|
188
|
-
function
|
|
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
|
-
|
|
194
|
-
|
|
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
|
|
237
|
-
const
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
303
|
-
if (
|
|
304
|
-
const
|
|
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
|
|
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", "video_url", "video"],
|
|
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
|
|
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 (
|
|
351
|
-
body.video_url =
|
|
352
|
-
body.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 (
|
|
361
|
-
const refs =
|
|
362
|
-
if (
|
|
363
|
-
body[
|
|
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
|
|
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 (!
|
|
484
|
+
if (!src.url) {
|
|
398
485
|
return {
|
|
399
486
|
kind: "text",
|
|
400
|
-
text: 'pixeldojo_generate with model="grok-video-extend" requires
|
|
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
|
-
|
|
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,
|
|
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
|
|
433
|
-
|
|
434
|
-
|
|
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
|
|
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
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
|
|
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) {
|
|
839
|
+
return {
|
|
840
|
+
kind: "text",
|
|
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) {
|
|
713
845
|
return {
|
|
714
846
|
kind: "text",
|
|
715
|
-
text: `pixeldojo_character with model="${apiId}" requires reference_image_url
|
|
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
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
|
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
|
|
741
|
-
|
|
742
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
916
|
+
"Single reference image of the character. Prefer reference_image_urls \u2014 more angles lock the face better."
|
|
917
|
+
),
|
|
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.`
|
|
759
920
|
),
|
|
760
|
-
aspect_ratio: zod.z.string().optional().describe(
|
|
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: {
|
|
772
|
-
|
|
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
|
|
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
|
|
814
|
-
|
|
815
|
-
|
|
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],
|
|
@@ -863,7 +1074,8 @@ var init_storyboard = __esm({
|
|
|
863
1074
|
}
|
|
864
1075
|
});
|
|
865
1076
|
async function run5(input, ctx) {
|
|
866
|
-
const
|
|
1077
|
+
const clipUrl = input.video_url ?? (input.image_url && isVideoUrl(input.image_url) ? input.image_url : void 0);
|
|
1078
|
+
const requested = input.model ?? (clipUrl ? VIDEO_MODEL : DEFAULT_MODEL5);
|
|
867
1079
|
if (!ALLOWED3.includes(requested)) {
|
|
868
1080
|
return {
|
|
869
1081
|
kind: "text",
|
|
@@ -871,11 +1083,33 @@ async function run5(input, ctx) {
|
|
|
871
1083
|
};
|
|
872
1084
|
}
|
|
873
1085
|
const apiId = requested;
|
|
1086
|
+
if (apiId === VIDEO_MODEL) {
|
|
1087
|
+
if (!clipUrl) {
|
|
1088
|
+
return {
|
|
1089
|
+
kind: "text",
|
|
1090
|
+
text: 'pixeldojo_upscale with model="video-upscaler" requires video_url (the source clip).'
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
if (input.video_duration === void 0) {
|
|
1094
|
+
return {
|
|
1095
|
+
kind: "text",
|
|
1096
|
+
text: 'pixeldojo_upscale with model="video-upscaler" requires video_duration (source clip length in seconds \u2014 it sets the credit cost, 1 credit/second).'
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
} else if (!input.image_url || input.image_url === clipUrl) {
|
|
1100
|
+
return {
|
|
1101
|
+
kind: "text",
|
|
1102
|
+
text: input.image_url ? `${apiId} upscales still images, but the source is a video clip. Use model="video-upscaler" with video_url and video_duration (seconds).` : `pixeldojo_upscale with model="${apiId}" requires image_url (the source image).`
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
874
1105
|
const body = {
|
|
875
1106
|
prompt: "",
|
|
876
|
-
image: input.image_url
|
|
1107
|
+
...apiId === VIDEO_MODEL ? { video: clipUrl } : { image: input.image_url }
|
|
877
1108
|
};
|
|
878
|
-
if (apiId ===
|
|
1109
|
+
if (apiId === VIDEO_MODEL) {
|
|
1110
|
+
body.video_duration = input.video_duration;
|
|
1111
|
+
if (input.target_resolution) body.target_resolution = input.target_resolution;
|
|
1112
|
+
} else if (apiId === "magnific-upscaler") {
|
|
879
1113
|
const allowed = [2, 4, 8, 16];
|
|
880
1114
|
const requestedScale = input.scale_factor ?? 2;
|
|
881
1115
|
const nearest = allowed.reduce(
|
|
@@ -903,34 +1137,49 @@ async function run5(input, ctx) {
|
|
|
903
1137
|
(urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Upscaled. Asset: ${urls[0]}`
|
|
904
1138
|
);
|
|
905
1139
|
}
|
|
906
|
-
var ALLOWED3, DEFAULT_MODEL5, InputSchema5, InputJsonSchema5, upscaleSkill;
|
|
1140
|
+
var ALLOWED3, DEFAULT_MODEL5, VIDEO_MODEL, VIDEO_RESOLUTIONS, InputSchema5, InputJsonSchema5, upscaleSkill;
|
|
907
1141
|
var init_upscale = __esm({
|
|
908
1142
|
"src/skills/upscale.ts"() {
|
|
909
1143
|
init_poll();
|
|
910
1144
|
init_types();
|
|
911
|
-
|
|
1145
|
+
init_schema_snap();
|
|
1146
|
+
ALLOWED3 = ["magnific-upscaler", "creative-upscale", "face-enhance", "video-upscaler"];
|
|
912
1147
|
DEFAULT_MODEL5 = "magnific-upscaler";
|
|
1148
|
+
VIDEO_MODEL = "video-upscaler";
|
|
1149
|
+
VIDEO_RESOLUTIONS = ["720p", "1080p", "2k", "4k"];
|
|
913
1150
|
InputSchema5 = zod.z.object({
|
|
914
|
-
image_url: zod.z.string().url().describe("URL of the source image to upscale."),
|
|
915
|
-
|
|
1151
|
+
image_url: zod.z.string().url().optional().describe("URL of the source image to upscale."),
|
|
1152
|
+
video_url: zod.z.string().url().optional().describe("URL of a source video clip \u2014 routes to video-upscaler. MP4 only, max 500 MB."),
|
|
1153
|
+
model: zod.z.string().optional().describe(`One of: ${ALLOWED3.join(", ")}. Defaults to ${DEFAULT_MODEL5} (a clip defaults to ${VIDEO_MODEL}).`),
|
|
916
1154
|
scale_factor: zod.z.number().min(1).max(16).optional().describe(
|
|
917
|
-
"
|
|
1155
|
+
"Image models only. Magnific snaps to the nearest of 2/4/8/16 (default 2); creative-upscale accepts 1-10 (default 2); face-enhance accepts 1-8 (default 4)."
|
|
918
1156
|
),
|
|
919
|
-
creativity: zod.z.number().min(0).max(1).optional().describe("Magnific only: 0 (preserve) \u2192 1 (creative). Defaults to 0.")
|
|
1157
|
+
creativity: zod.z.number().min(0).max(1).optional().describe("Magnific only: 0 (preserve) \u2192 1 (creative). Defaults to 0."),
|
|
1158
|
+
video_duration: zod.z.number().positive().optional().describe("video-upscaler only, REQUIRED there: source clip length in seconds (its credit meter)."),
|
|
1159
|
+
target_resolution: zod.z.enum(VIDEO_RESOLUTIONS).optional().describe("video-upscaler only: target output resolution. Defaults to 1080p.")
|
|
920
1160
|
});
|
|
921
1161
|
InputJsonSchema5 = {
|
|
922
1162
|
type: "object",
|
|
923
1163
|
properties: {
|
|
924
1164
|
image_url: { type: "string", format: "uri" },
|
|
1165
|
+
video_url: {
|
|
1166
|
+
type: "string",
|
|
1167
|
+
format: "uri",
|
|
1168
|
+
description: "Source video clip \u2014 routes to video-upscaler."
|
|
1169
|
+
},
|
|
925
1170
|
model: { type: "string", enum: ALLOWED3 },
|
|
926
1171
|
scale_factor: { type: "number", minimum: 1, maximum: 16 },
|
|
927
|
-
creativity: { type: "number", minimum: 0, maximum: 1 }
|
|
928
|
-
|
|
929
|
-
|
|
1172
|
+
creativity: { type: "number", minimum: 0, maximum: 1 },
|
|
1173
|
+
video_duration: {
|
|
1174
|
+
type: "number",
|
|
1175
|
+
description: "video-upscaler only, required there: source clip length in seconds."
|
|
1176
|
+
},
|
|
1177
|
+
target_resolution: { type: "string", enum: VIDEO_RESOLUTIONS }
|
|
1178
|
+
}
|
|
930
1179
|
};
|
|
931
1180
|
upscaleSkill = {
|
|
932
1181
|
name: "pixeldojo_upscale",
|
|
933
|
-
description: "Enhance an image. Magnific (default) supports configurable scale_factor and creativity; face-enhance for portraits.",
|
|
1182
|
+
description: "Enhance an image or video. Magnific (default) supports configurable scale_factor and creativity; face-enhance for portraits; video-upscaler (FlashVSR, up to 4K) for clips \u2014 pass video_url plus video_duration in seconds.",
|
|
934
1183
|
inputZod: InputSchema5,
|
|
935
1184
|
inputJsonSchema: InputJsonSchema5,
|
|
936
1185
|
allowedModels: [...ALLOWED3],
|
|
@@ -1078,6 +1327,9 @@ var init_from_url = __esm({
|
|
|
1078
1327
|
};
|
|
1079
1328
|
}
|
|
1080
1329
|
});
|
|
1330
|
+
function needsSave(url) {
|
|
1331
|
+
return Boolean(url && url.startsWith("https://temp.pixeldojo.ai/"));
|
|
1332
|
+
}
|
|
1081
1333
|
async function filmApi(ctx, method, path, body) {
|
|
1082
1334
|
const res = await fetch(`${ctx.baseUrl}${path}`, {
|
|
1083
1335
|
method,
|
|
@@ -1174,7 +1426,7 @@ function buildScorePlanBody(film) {
|
|
|
1174
1426
|
}
|
|
1175
1427
|
function buildFlatMusicBody(film, all) {
|
|
1176
1428
|
const runtimeSec = all.reduce((n, sh) => n + (sh.durationSec || 0), 0);
|
|
1177
|
-
const ms = Math.min(
|
|
1429
|
+
const ms = Math.min(3e5, Math.max(3e4, Math.ceil(runtimeSec * 1e3 / 5e3) * 5e3));
|
|
1178
1430
|
return {
|
|
1179
1431
|
prompt: `${LOOK_MUSIC[film.look] ?? LOOK_MUSIC["live-action"]}. Instrumental only, no vocals. One consistent theme, loopable as a background bed under dialogue.`,
|
|
1180
1432
|
music_length_ms: ms,
|
|
@@ -1204,24 +1456,8 @@ It is saved in the Library folder "Film: ${film.title}".`
|
|
|
1204
1456
|
await filmApi(ctx, "POST", `/api/films/${filmId}/shots/${sh.id}/generate`, { kind: "frame" });
|
|
1205
1457
|
}
|
|
1206
1458
|
const framesDone = all.every((sh) => ["frame-ready", "clip-generating", "clip-ready"].includes(sh.status));
|
|
1207
|
-
if (framesDone && all.some((sh) => sh.status === "frame-ready" &&
|
|
1208
|
-
await filmApi(ctx, "
|
|
1209
|
-
scenes: film.scenes.map((s) => ({
|
|
1210
|
-
id: s.id,
|
|
1211
|
-
title: s.title,
|
|
1212
|
-
location: s.location,
|
|
1213
|
-
shots: s.shots.map((sh) => ({
|
|
1214
|
-
id: sh.id,
|
|
1215
|
-
prompt: sh.prompt,
|
|
1216
|
-
characterIds: sh.characterIds ?? [],
|
|
1217
|
-
elementIds: sh.elementIds ?? [],
|
|
1218
|
-
engine: sh.engine,
|
|
1219
|
-
cameraPreset: sh.cameraPreset,
|
|
1220
|
-
approved: true,
|
|
1221
|
-
durationSec: sh.durationSec
|
|
1222
|
-
}))
|
|
1223
|
-
}))
|
|
1224
|
-
});
|
|
1459
|
+
if (framesDone && all.some((sh) => sh.status === "frame-ready" && needsSave(sh.frameUrl))) {
|
|
1460
|
+
await filmApi(ctx, "POST", `/api/films/${filmId}/save`, { kind: "frame" });
|
|
1225
1461
|
}
|
|
1226
1462
|
const inFlight = count(film, (sh) => sh.status === "clip-generating");
|
|
1227
1463
|
const clipQueue = all.filter((sh) => (sh.status === "frame-ready" || sh.status === "failed" && Boolean(sh.frameUrl)) && framesDone);
|
|
@@ -1230,6 +1466,9 @@ It is saved in the Library folder "Film: ${film.title}".`
|
|
|
1230
1466
|
}
|
|
1231
1467
|
const clipsDone = all.length > 0 && all.every((sh) => sh.status === "clip-ready");
|
|
1232
1468
|
if (clipsDone) {
|
|
1469
|
+
if (all.some((sh) => needsSave(sh.videoUrl))) {
|
|
1470
|
+
await filmApi(ctx, "POST", `/api/films/${filmId}/save`, { kind: "clip" });
|
|
1471
|
+
}
|
|
1233
1472
|
const hasMusic = film.audioStems.some((st) => st.kind === "music");
|
|
1234
1473
|
if (soundtrack !== "none" && !hasMusic) {
|
|
1235
1474
|
if (musicJobId) {
|
|
@@ -2183,7 +2422,7 @@ function makeRedactedLogger(target = console) {
|
|
|
2183
2422
|
}
|
|
2184
2423
|
|
|
2185
2424
|
// src/server.ts
|
|
2186
|
-
var PACKAGE_VERSION = "0.
|
|
2425
|
+
var PACKAGE_VERSION = "0.11.0" ;
|
|
2187
2426
|
function buildServer(options) {
|
|
2188
2427
|
if (!options.apiKey) {
|
|
2189
2428
|
throw new Error(
|