@pixeldojo/mcp 0.6.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +598 -68
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +598 -68
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +598 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +598 -68
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/cli.cjs
CHANGED
|
@@ -183,6 +183,109 @@ var init_upload_helpers = __esm({
|
|
|
183
183
|
};
|
|
184
184
|
}
|
|
185
185
|
});
|
|
186
|
+
|
|
187
|
+
// src/skills/schema-snap.ts
|
|
188
|
+
function getFieldConstraints(apiId, client) {
|
|
189
|
+
const cached = schemaCache.get(apiId);
|
|
190
|
+
if (cached) return cached;
|
|
191
|
+
const pending = Promise.resolve().then(() => client.getModelSchema(apiId)).then(
|
|
192
|
+
(res) => res.schema?.properties ?? {}
|
|
193
|
+
).catch(() => {
|
|
194
|
+
schemaCache.delete(apiId);
|
|
195
|
+
return null;
|
|
196
|
+
});
|
|
197
|
+
schemaCache.set(apiId, pending);
|
|
198
|
+
return pending;
|
|
199
|
+
}
|
|
200
|
+
function parseAspectRatio(ar) {
|
|
201
|
+
const m = /^\s*(\d+)\s*:\s*(\d+)\s*$/.exec(ar);
|
|
202
|
+
if (!m) return null;
|
|
203
|
+
const w = Number(m[1]);
|
|
204
|
+
const h = Number(m[2]);
|
|
205
|
+
return h > 0 ? w / h : null;
|
|
206
|
+
}
|
|
207
|
+
function nearestAspectFromEnum(requested, allowed) {
|
|
208
|
+
if (allowed.includes(requested)) return requested;
|
|
209
|
+
const target = parseAspectRatio(requested);
|
|
210
|
+
if (target === null) return requested;
|
|
211
|
+
let best = requested;
|
|
212
|
+
let bestDiff = Infinity;
|
|
213
|
+
for (const candidate of allowed) {
|
|
214
|
+
const r = parseAspectRatio(candidate);
|
|
215
|
+
if (r === null) continue;
|
|
216
|
+
const diff = Math.abs(r - target);
|
|
217
|
+
if (diff < bestDiff) {
|
|
218
|
+
bestDiff = diff;
|
|
219
|
+
best = candidate;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return best;
|
|
223
|
+
}
|
|
224
|
+
async function snapAspectRatio(apiId, requested, client, opts = {}) {
|
|
225
|
+
if (!requested) return void 0;
|
|
226
|
+
const props = await getFieldConstraints(apiId, client);
|
|
227
|
+
if (props === null) {
|
|
228
|
+
return opts.onUnknown === "omit" ? void 0 : requested;
|
|
229
|
+
}
|
|
230
|
+
const field = props.aspect_ratio;
|
|
231
|
+
if (!field) return void 0;
|
|
232
|
+
if (!field.enum || field.enum.length === 0) return requested;
|
|
233
|
+
return nearestAspectFromEnum(requested, field.enum);
|
|
234
|
+
}
|
|
235
|
+
async function snapDuration(apiId, requested, client, opts = {}) {
|
|
236
|
+
if (requested === void 0) return void 0;
|
|
237
|
+
const props = await getFieldConstraints(apiId, client);
|
|
238
|
+
if (props === null) {
|
|
239
|
+
return opts.onUnknown === "omit" ? void 0 : requested;
|
|
240
|
+
}
|
|
241
|
+
const field = props.duration;
|
|
242
|
+
if (!field) return void 0;
|
|
243
|
+
if (field.enum && field.enum.length > 0) {
|
|
244
|
+
const allowed = field.enum.map(Number).filter((n) => Number.isFinite(n));
|
|
245
|
+
if (allowed.length === 0) return requested;
|
|
246
|
+
if (allowed.includes(requested)) return requested;
|
|
247
|
+
return allowed.reduce(
|
|
248
|
+
(best, candidate) => Math.abs(candidate - requested) < Math.abs(best - requested) ? candidate : best
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (typeof field.minimum === "number" || typeof field.maximum === "number") {
|
|
252
|
+
let v = requested;
|
|
253
|
+
if (typeof field.minimum === "number") v = Math.max(field.minimum, v);
|
|
254
|
+
if (typeof field.maximum === "number") v = Math.min(field.maximum, v);
|
|
255
|
+
return v;
|
|
256
|
+
}
|
|
257
|
+
return requested;
|
|
258
|
+
}
|
|
259
|
+
async function resolveImageRefs(apiId, urls, client) {
|
|
260
|
+
if (urls.length === 0) return null;
|
|
261
|
+
const props = await getFieldConstraints(apiId, client);
|
|
262
|
+
if (props === null) return null;
|
|
263
|
+
const key = REF_KEYS.find((k) => props[k] !== void 0);
|
|
264
|
+
if (!key) return null;
|
|
265
|
+
const field = props[key];
|
|
266
|
+
if (field.type === "array") {
|
|
267
|
+
const cap = typeof field.maxItems === "number" ? field.maxItems : urls.length;
|
|
268
|
+
const kept = urls.slice(0, Math.max(1, cap));
|
|
269
|
+
return { key, value: kept, dropped: urls.length - kept.length };
|
|
270
|
+
}
|
|
271
|
+
return { key, value: urls[0], dropped: urls.length - 1 };
|
|
272
|
+
}
|
|
273
|
+
var schemaCache, REF_KEYS;
|
|
274
|
+
var init_schema_snap = __esm({
|
|
275
|
+
"src/skills/schema-snap.ts"() {
|
|
276
|
+
schemaCache = /* @__PURE__ */ new Map();
|
|
277
|
+
REF_KEYS = [
|
|
278
|
+
"image_urls",
|
|
279
|
+
"images",
|
|
280
|
+
"image_input",
|
|
281
|
+
"reference_images",
|
|
282
|
+
"reference_urls",
|
|
283
|
+
"image_url",
|
|
284
|
+
"input_image",
|
|
285
|
+
"image"
|
|
286
|
+
];
|
|
287
|
+
}
|
|
288
|
+
});
|
|
186
289
|
function buildPayload(apiId, input) {
|
|
187
290
|
const body = { prompt: input.prompt };
|
|
188
291
|
if (input.aspect_ratio) body.aspect_ratio = input.aspect_ratio;
|
|
@@ -235,6 +338,13 @@ async function run(input, ctx) {
|
|
|
235
338
|
};
|
|
236
339
|
}
|
|
237
340
|
}
|
|
341
|
+
if (!VIDEO_INPUT_MODELS.has(apiId)) {
|
|
342
|
+
const [aspect_ratio, duration] = await Promise.all([
|
|
343
|
+
snapAspectRatio(apiId, resolved.aspect_ratio, ctx.client),
|
|
344
|
+
snapDuration(apiId, resolved.duration, ctx.client)
|
|
345
|
+
]);
|
|
346
|
+
resolved = { ...resolved, aspect_ratio, duration };
|
|
347
|
+
}
|
|
238
348
|
const body = buildPayload(apiId, resolved);
|
|
239
349
|
const submitted = await ctx.client.run(apiId, body);
|
|
240
350
|
const result = await pollJobUntilDoneOrHandoff(
|
|
@@ -251,12 +361,13 @@ async function run(input, ctx) {
|
|
|
251
361
|
(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(", ")}`
|
|
252
362
|
);
|
|
253
363
|
}
|
|
254
|
-
var InputSchema, DEFAULT_MODEL, InputJsonSchema, generateSkill;
|
|
364
|
+
var InputSchema, DEFAULT_MODEL, VIDEO_INPUT_MODELS, InputJsonSchema, generateSkill;
|
|
255
365
|
var init_generate = __esm({
|
|
256
366
|
"src/skills/generate.ts"() {
|
|
257
367
|
init_poll();
|
|
258
368
|
init_types();
|
|
259
369
|
init_upload_helpers();
|
|
370
|
+
init_schema_snap();
|
|
260
371
|
InputSchema = zod.z.object({
|
|
261
372
|
prompt: zod.z.string().min(1).max(8e3).describe("Prompt describing what to generate."),
|
|
262
373
|
model: zod.z.string().optional().describe(
|
|
@@ -273,6 +384,7 @@ var init_generate = __esm({
|
|
|
273
384
|
)
|
|
274
385
|
});
|
|
275
386
|
DEFAULT_MODEL = "google-nano-banana";
|
|
387
|
+
VIDEO_INPUT_MODELS = /* @__PURE__ */ new Set(["grok-video-extend", "wan-video-edit", "seedance-video-edit"]);
|
|
276
388
|
InputJsonSchema = {
|
|
277
389
|
type: "object",
|
|
278
390
|
properties: {
|
|
@@ -323,12 +435,21 @@ function buildPayload2(apiId, input) {
|
|
|
323
435
|
...input.aspect_ratio ? { aspect_ratio: input.aspect_ratio } : {}
|
|
324
436
|
};
|
|
325
437
|
case "flux-edit":
|
|
326
|
-
|
|
438
|
+
return {
|
|
439
|
+
prompt: input.prompt,
|
|
440
|
+
input_image_1: urls[0],
|
|
441
|
+
...input.aspect_ratio ? { aspect_ratio: input.aspect_ratio } : {}
|
|
442
|
+
};
|
|
327
443
|
case "gpt-image-2-edit":
|
|
444
|
+
return {
|
|
445
|
+
prompt: input.prompt,
|
|
446
|
+
image_urls: urls.slice(0, 8),
|
|
447
|
+
...input.aspect_ratio ? { aspect_ratio: input.aspect_ratio } : {}
|
|
448
|
+
};
|
|
449
|
+
case "xai-image-edit":
|
|
328
450
|
return {
|
|
329
451
|
prompt: input.prompt,
|
|
330
452
|
image_url: urls[0],
|
|
331
|
-
...urls.length > 1 ? { image_urls: urls } : {},
|
|
332
453
|
...input.aspect_ratio ? { aspect_ratio: input.aspect_ratio } : {}
|
|
333
454
|
};
|
|
334
455
|
case "google-nano-banana":
|
|
@@ -344,6 +465,19 @@ function buildPayload2(apiId, input) {
|
|
|
344
465
|
};
|
|
345
466
|
}
|
|
346
467
|
}
|
|
468
|
+
async function buildBody(apiId, input, ctx) {
|
|
469
|
+
const urls = input.image_urls && input.image_urls.length > 0 ? input.image_urls : input.image_url ? [input.image_url] : [];
|
|
470
|
+
const refs = await resolveImageRefs(apiId, urls, ctx.client);
|
|
471
|
+
if (!refs) return { body: buildPayload2(apiId, input), dropped: 0 };
|
|
472
|
+
const fallback = buildPayload2(apiId, input);
|
|
473
|
+
const body = { ...fallback };
|
|
474
|
+
for (const key of ["image_url", "image_urls", "images", "image_input", "input_image_1"]) {
|
|
475
|
+
if (key !== refs.key) delete body[key];
|
|
476
|
+
}
|
|
477
|
+
body.prompt = input.prompt;
|
|
478
|
+
body[refs.key] = refs.value;
|
|
479
|
+
return { body, dropped: refs.dropped };
|
|
480
|
+
}
|
|
347
481
|
async function run2(input, ctx) {
|
|
348
482
|
let resolved = input;
|
|
349
483
|
try {
|
|
@@ -370,7 +504,12 @@ async function run2(input, ctx) {
|
|
|
370
504
|
};
|
|
371
505
|
}
|
|
372
506
|
const apiId = resolved.model ?? DEFAULT_MODEL2;
|
|
373
|
-
|
|
507
|
+
resolved = {
|
|
508
|
+
...resolved,
|
|
509
|
+
aspect_ratio: await snapAspectRatio(apiId, resolved.aspect_ratio, ctx.client)
|
|
510
|
+
};
|
|
511
|
+
const { body, dropped } = await buildBody(apiId, resolved, ctx);
|
|
512
|
+
const droppedNote = dropped > 0 ? ` (${dropped} reference${dropped === 1 ? "" : "s"} dropped \u2014 ${apiId} accepts fewer)` : "";
|
|
374
513
|
const submitted = await ctx.client.run(apiId, body);
|
|
375
514
|
const result = await pollJobUntilDoneOrHandoff(
|
|
376
515
|
submitted.jobId,
|
|
@@ -383,7 +522,7 @@ async function run2(input, ctx) {
|
|
|
383
522
|
);
|
|
384
523
|
return pollResultToSkillResult(
|
|
385
524
|
result,
|
|
386
|
-
(urls) => urls.length === 0 ? `Edit job ${submitted.jobId} completed but returned no assets.` : urls.length === 1 ? `Edit complete. Asset: ${urls[0]}` : `Edit produced ${urls.length} assets. URLs: ${urls.join(", ")}`
|
|
525
|
+
(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(", ")}`
|
|
387
526
|
);
|
|
388
527
|
}
|
|
389
528
|
var DEFAULT_MODEL2, ALLOWED_MODELS, InputSchema2, InputJsonSchema2, editSkill;
|
|
@@ -392,6 +531,7 @@ var init_edit = __esm({
|
|
|
392
531
|
init_poll();
|
|
393
532
|
init_types();
|
|
394
533
|
init_upload_helpers();
|
|
534
|
+
init_schema_snap();
|
|
395
535
|
DEFAULT_MODEL2 = "wan-2.7-image-edit";
|
|
396
536
|
ALLOWED_MODELS = [
|
|
397
537
|
"wan-2.7-image-edit",
|
|
@@ -487,9 +627,10 @@ async function run3(input, ctx) {
|
|
|
487
627
|
body.image_urls = [input.reference_image_url];
|
|
488
628
|
}
|
|
489
629
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
630
|
+
const snappedAspect = await snapAspectRatio(apiId, input.aspect_ratio, ctx.client, {
|
|
631
|
+
onUnknown: "omit"
|
|
632
|
+
});
|
|
633
|
+
if (snappedAspect !== void 0) body.aspect_ratio = snappedAspect;
|
|
493
634
|
const submitted = await ctx.client.run(apiId, body);
|
|
494
635
|
const result = await pollJobUntilDoneOrHandoff(
|
|
495
636
|
submitted.jobId,
|
|
@@ -505,28 +646,15 @@ async function run3(input, ctx) {
|
|
|
505
646
|
(urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Character shot ready. Asset: ${urls[0]}`
|
|
506
647
|
);
|
|
507
648
|
}
|
|
508
|
-
var ALLOWED, DEFAULT_MODEL3, REQUIRES_REFERENCE,
|
|
649
|
+
var ALLOWED, DEFAULT_MODEL3, REQUIRES_REFERENCE, InputSchema3, InputJsonSchema3, characterSkill;
|
|
509
650
|
var init_character = __esm({
|
|
510
651
|
"src/skills/character.ts"() {
|
|
511
652
|
init_poll();
|
|
512
653
|
init_types();
|
|
654
|
+
init_schema_snap();
|
|
513
655
|
ALLOWED = ["seedream-5", "seedream-4", "flux-edit", "gpt-image-2-edit"];
|
|
514
656
|
DEFAULT_MODEL3 = "seedream-5";
|
|
515
657
|
REQUIRES_REFERENCE = /* @__PURE__ */ new Set(["flux-edit", "gpt-image-2-edit"]);
|
|
516
|
-
FLUX_EDIT_ASPECT_RATIOS = /* @__PURE__ */ new Set([
|
|
517
|
-
"1:1",
|
|
518
|
-
"16:9",
|
|
519
|
-
"9:16",
|
|
520
|
-
"21:9",
|
|
521
|
-
"9:21",
|
|
522
|
-
"3:2",
|
|
523
|
-
"2:3",
|
|
524
|
-
"4:5",
|
|
525
|
-
"5:4",
|
|
526
|
-
"3:4",
|
|
527
|
-
"4:3",
|
|
528
|
-
"match_input_image"
|
|
529
|
-
]);
|
|
530
658
|
InputSchema3 = zod.z.object({
|
|
531
659
|
prompt: zod.z.string().min(1).max(8e3),
|
|
532
660
|
model: zod.z.string().optional().describe(`One of: ${ALLOWED.join(", ")}. Defaults to ${DEFAULT_MODEL3}.`),
|
|
@@ -569,9 +697,13 @@ async function run4(input, ctx) {
|
|
|
569
697
|
};
|
|
570
698
|
}
|
|
571
699
|
const apiId = requested;
|
|
700
|
+
const [snappedAspect, snappedDuration] = await Promise.all([
|
|
701
|
+
snapAspectRatio(apiId, input.aspect_ratio, ctx.client),
|
|
702
|
+
snapDuration(apiId, input.duration, ctx.client)
|
|
703
|
+
]);
|
|
572
704
|
const body = { prompt: input.brief };
|
|
573
|
-
if (
|
|
574
|
-
if (
|
|
705
|
+
if (snappedAspect !== void 0) body.aspect_ratio = snappedAspect;
|
|
706
|
+
if (snappedDuration !== void 0) body.duration = snappedDuration;
|
|
575
707
|
const submitted = await ctx.client.run(apiId, body);
|
|
576
708
|
const result = await pollJobUntilDoneOrHandoff(
|
|
577
709
|
submitted.jobId,
|
|
@@ -592,6 +724,7 @@ var init_storyboard = __esm({
|
|
|
592
724
|
"src/skills/storyboard.ts"() {
|
|
593
725
|
init_poll();
|
|
594
726
|
init_types();
|
|
727
|
+
init_schema_snap();
|
|
595
728
|
ALLOWED2 = [
|
|
596
729
|
"veo-3.1-fast",
|
|
597
730
|
"veo-3.1-standard",
|
|
@@ -849,7 +982,396 @@ var init_from_url = __esm({
|
|
|
849
982
|
};
|
|
850
983
|
}
|
|
851
984
|
});
|
|
985
|
+
async function filmApi(ctx, method, path, body) {
|
|
986
|
+
const res = await fetch(`${ctx.baseUrl}${path}`, {
|
|
987
|
+
method,
|
|
988
|
+
headers: {
|
|
989
|
+
Authorization: `Bearer ${ctx.apiKey}`,
|
|
990
|
+
"Content-Type": "application/json",
|
|
991
|
+
"x-pixeldojo-source": `mcp/${ctx.mcpVersion}`,
|
|
992
|
+
"x-pixeldojo-skill": "film"
|
|
993
|
+
},
|
|
994
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
995
|
+
});
|
|
996
|
+
const data = await res.json().catch(() => ({}));
|
|
997
|
+
return { status: res.status, data };
|
|
998
|
+
}
|
|
999
|
+
function progressLine(f) {
|
|
1000
|
+
const all = shots(f);
|
|
1001
|
+
const frames = all.filter((sh) => ["frame-ready", "approved", "clip-generating", "clip-ready"].includes(sh.status) || sh.frameUrl).length;
|
|
1002
|
+
const clips = all.filter((sh) => sh.status === "clip-ready").length;
|
|
1003
|
+
const rendering = all.filter((sh) => sh.status === "clip-generating").length;
|
|
1004
|
+
return `frames ${frames}/${all.length}, clips ${clips}/${all.length}${rendering > 0 ? ` (${rendering} rendering)` : ""}`;
|
|
1005
|
+
}
|
|
1006
|
+
async function plan(input, ctx) {
|
|
1007
|
+
if (!input.idea?.trim() && !input.script?.trim()) {
|
|
1008
|
+
return { kind: "text", text: "Provide an `idea` (we write the script, free) or a full `script` to plan a film." };
|
|
1009
|
+
}
|
|
1010
|
+
const created = await filmApi(ctx, "POST", "/api/films", {
|
|
1011
|
+
aspect: input.aspect,
|
|
1012
|
+
look: input.look,
|
|
1013
|
+
lengthTarget: input.length,
|
|
1014
|
+
assetIds: []
|
|
1015
|
+
});
|
|
1016
|
+
if (!created.data.film) return { kind: "text", text: `Could not create the film: ${created.data.error ?? created.status}` };
|
|
1017
|
+
const id = created.data.film.id;
|
|
1018
|
+
let script = input.script?.trim() ?? "";
|
|
1019
|
+
if (!script) {
|
|
1020
|
+
const wrote = await filmApi(ctx, "POST", `/api/films/${id}/script`, { idea: input.idea.trim() });
|
|
1021
|
+
script = wrote.data.film?.script ?? "";
|
|
1022
|
+
if (!script) return { kind: "text", text: `Script writing failed: ${wrote.data.error ?? wrote.status}` };
|
|
1023
|
+
}
|
|
1024
|
+
const planned = await filmApi(ctx, "POST", `/api/films/${id}/plan`, { script });
|
|
1025
|
+
const film = planned.data.film;
|
|
1026
|
+
if (!film) return { kind: "text", text: `Shot planning failed: ${planned.data.error ?? planned.status}` };
|
|
1027
|
+
const total = shots(film).length;
|
|
1028
|
+
const sceneLines = film.scenes.map((s, i) => ` Scene ${i + 1} "${s.title}"${s.location ? ` [${s.location}]` : ""}: ${s.shots.length} shots`);
|
|
1029
|
+
return {
|
|
1030
|
+
kind: "text",
|
|
1031
|
+
text: [
|
|
1032
|
+
`Planned "${film.title}" (film_id: ${film.id}) \u2014 ${film.scenes.length} scenes, ${total} shots, ${input.look}, ${input.aspect}.`,
|
|
1033
|
+
...sceneLines,
|
|
1034
|
+
"",
|
|
1035
|
+
`Nothing has been charged yet. Producing renders every shot (roughly 20-25 credits per shot at default engines, charged as each renders) plus a few credits for the soundtrack.`,
|
|
1036
|
+
`Review or edit at ${ctx.baseUrl}/film-studio/${film.id} \u2014 or call pixeldojo:film with {"film_id": "${film.id}", "action": "produce"} to start production.`
|
|
1037
|
+
].join("\n")
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
async function produce(filmId, ctx) {
|
|
1041
|
+
const { data } = await filmApi(ctx, "GET", `/api/films/${filmId}`);
|
|
1042
|
+
if (!data.film) return { kind: "text", text: `Film not found: ${data.error ?? filmId}` };
|
|
1043
|
+
const pending = shots(data.film).filter((sh) => sh.status === "idle" || sh.status === "failed" && !sh.frameUrl);
|
|
1044
|
+
let fired = 0;
|
|
1045
|
+
for (const sh of pending.slice(0, 6)) {
|
|
1046
|
+
const r = await filmApi(ctx, "POST", `/api/films/${filmId}/shots/${sh.id}/generate`, { kind: "frame" });
|
|
1047
|
+
if (r.status === 200) fired += 1;
|
|
1048
|
+
}
|
|
1049
|
+
return {
|
|
1050
|
+
kind: "text",
|
|
1051
|
+
text: `Production started on "${data.film.title}": ${fired} frame(s) dispatched (each is a normal frame charge; the server's Frame check reviews every one). Poll pixeldojo:film with {"film_id": "${filmId}", "action": "status"} every ~30 seconds \u2014 each poll advances the pipeline.`
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
function buildScorePlanBody(film) {
|
|
1055
|
+
const scenes = film.scenes.filter((s) => s.shots.length > 0);
|
|
1056
|
+
const sections = scenes.flatMap((scene, i) => {
|
|
1057
|
+
const runtime = Math.max(3e3, scene.shots.reduce((n, sh) => n + (sh.durationSec || 0), 0) * 1e3);
|
|
1058
|
+
const styles = i === 0 ? ARC.opening : i === scenes.length - 1 ? ARC.finale : ARC.middle;
|
|
1059
|
+
const parts = Math.max(1, Math.ceil(runtime / 12e4));
|
|
1060
|
+
const partMs = Math.round(runtime / parts);
|
|
1061
|
+
return Array.from({ length: parts }, (_, part) => ({
|
|
1062
|
+
section_name: parts > 1 ? `${scene.title} (part ${part + 1})` : scene.title,
|
|
1063
|
+
positive_local_styles: styles,
|
|
1064
|
+
negative_local_styles: [],
|
|
1065
|
+
duration_ms: part === parts - 1 ? runtime - partMs * (parts - 1) : partMs,
|
|
1066
|
+
lines: []
|
|
1067
|
+
}));
|
|
1068
|
+
});
|
|
1069
|
+
return {
|
|
1070
|
+
composition_plan: {
|
|
1071
|
+
positive_global_styles: (LOOK_MUSIC[film.look] ?? LOOK_MUSIC["live-action"]).split(", "),
|
|
1072
|
+
negative_global_styles: ["vocals", "singing", "lyrics"],
|
|
1073
|
+
sections
|
|
1074
|
+
},
|
|
1075
|
+
respect_sections_durations: true,
|
|
1076
|
+
output_format: "mp3_standard"
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
function buildFlatMusicBody(film, all) {
|
|
1080
|
+
const runtimeSec = all.reduce((n, sh) => n + (sh.durationSec || 0), 0);
|
|
1081
|
+
const ms = Math.min(6e5, Math.max(5e3, Math.ceil(runtimeSec * 1e3 / 5e3) * 5e3));
|
|
1082
|
+
return {
|
|
1083
|
+
prompt: `${LOOK_MUSIC[film.look] ?? LOOK_MUSIC["live-action"]}. Instrumental only, no vocals. One consistent theme, loopable as a background bed under dialogue.`,
|
|
1084
|
+
music_length_ms: ms,
|
|
1085
|
+
force_instrumental: true,
|
|
1086
|
+
output_format: "mp3_standard"
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
async function advance(filmId, soundtrack, musicJobId, ctx) {
|
|
1090
|
+
const { data } = await filmApi(ctx, "GET", `/api/films/${filmId}?refresh=1`);
|
|
1091
|
+
const film = data.film;
|
|
1092
|
+
if (!film) return { kind: "text", text: `Film not found: ${data.error ?? filmId}` };
|
|
1093
|
+
const all = shots(film);
|
|
1094
|
+
if (film.status === "exported" && film.exportUrl) {
|
|
1095
|
+
return {
|
|
1096
|
+
kind: "text",
|
|
1097
|
+
text: `Film finished: "${film.title}"
|
|
1098
|
+
Watch/download: ${film.exportUrl}
|
|
1099
|
+
Workspace: ${ctx.baseUrl}/film-studio/${film.id}
|
|
1100
|
+
It is saved in the Library folder "Film: ${film.title}".`
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
if (film.exportError) {
|
|
1104
|
+
return { kind: "text", text: `Export failed: ${film.exportError} Poll again with action "status" to retry.` };
|
|
1105
|
+
}
|
|
1106
|
+
const idleFrames = all.filter((sh) => sh.status === "idle");
|
|
1107
|
+
for (const sh of idleFrames.slice(0, 6)) {
|
|
1108
|
+
await filmApi(ctx, "POST", `/api/films/${filmId}/shots/${sh.id}/generate`, { kind: "frame" });
|
|
1109
|
+
}
|
|
1110
|
+
const framesDone = all.every((sh) => ["frame-ready", "clip-generating", "clip-ready"].includes(sh.status));
|
|
1111
|
+
if (framesDone && all.some((sh) => sh.status === "frame-ready" && !sh.approved)) {
|
|
1112
|
+
await filmApi(ctx, "PATCH", `/api/films/${filmId}`, {
|
|
1113
|
+
scenes: film.scenes.map((s) => ({
|
|
1114
|
+
id: s.id,
|
|
1115
|
+
title: s.title,
|
|
1116
|
+
location: s.location,
|
|
1117
|
+
shots: s.shots.map((sh) => ({
|
|
1118
|
+
id: sh.id,
|
|
1119
|
+
prompt: sh.prompt,
|
|
1120
|
+
characterIds: sh.characterIds ?? [],
|
|
1121
|
+
elementIds: sh.elementIds ?? [],
|
|
1122
|
+
engine: sh.engine,
|
|
1123
|
+
cameraPreset: sh.cameraPreset,
|
|
1124
|
+
approved: true,
|
|
1125
|
+
durationSec: sh.durationSec
|
|
1126
|
+
}))
|
|
1127
|
+
}))
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
const inFlight = count(film, (sh) => sh.status === "clip-generating");
|
|
1131
|
+
const clipQueue = all.filter((sh) => (sh.status === "frame-ready" || sh.status === "failed" && Boolean(sh.frameUrl)) && framesDone);
|
|
1132
|
+
for (const sh of clipQueue.slice(0, Math.max(0, 4 - inFlight))) {
|
|
1133
|
+
await filmApi(ctx, "POST", `/api/films/${filmId}/shots/${sh.id}/generate`, { kind: "clip" });
|
|
1134
|
+
}
|
|
1135
|
+
const clipsDone = all.length > 0 && all.every((sh) => sh.status === "clip-ready");
|
|
1136
|
+
if (clipsDone) {
|
|
1137
|
+
const hasMusic = film.audioStems.some((st) => st.kind === "music");
|
|
1138
|
+
if (soundtrack !== "none" && !hasMusic) {
|
|
1139
|
+
if (musicJobId) {
|
|
1140
|
+
const job = await ctx.client.getJob(musicJobId);
|
|
1141
|
+
const out = job.output;
|
|
1142
|
+
const outUrl = out?.audio ?? out?.video ?? out?.images?.[0];
|
|
1143
|
+
if (job.status === "completed" && typeof outUrl === "string") {
|
|
1144
|
+
await filmApi(ctx, "PATCH", `/api/films/${filmId}`, { audioStems: [{ kind: "music", url: outUrl }] });
|
|
1145
|
+
await filmApi(ctx, "POST", `/api/films/${filmId}/export`, {});
|
|
1146
|
+
return { kind: "text", text: `Score attached \u2014 export started. Poll again in ~60s with {"film_id": "${filmId}", "action": "status"} for the finished film URL.` };
|
|
1147
|
+
}
|
|
1148
|
+
if (job.status === "failed") {
|
|
1149
|
+
return { kind: "text", text: `The score failed (${job.error ?? "unknown"}) and was refunded. Poll again to retry, or pass soundtrack "none" to export without music.` };
|
|
1150
|
+
}
|
|
1151
|
+
return { kind: "text", text: `Score still composing. Poll again in ~20s with {"film_id": "${filmId}", "action": "status", "music_job_id": "${musicJobId}"}.` };
|
|
1152
|
+
}
|
|
1153
|
+
const body = soundtrack === "scene-synced" ? buildScorePlanBody(film) : buildFlatMusicBody(film, all);
|
|
1154
|
+
const dispatch = await ctx.client.run("text-to-music", body);
|
|
1155
|
+
return { kind: "text", text: `All ${all.length} clips ready \u2014 the ${soundtrack} score is composing (charged like any music generation). Poll again in ~20s with {"film_id": "${filmId}", "action": "status", "music_job_id": "${dispatch.jobId}"}.` };
|
|
1156
|
+
}
|
|
1157
|
+
await filmApi(ctx, "POST", `/api/films/${filmId}/export`, {});
|
|
1158
|
+
return { kind: "text", text: `All clips ready \u2014 export started (${progressLine(film)}). Poll again in ~60s with action "status" for the finished film URL.` };
|
|
1159
|
+
}
|
|
1160
|
+
const failed = all.filter((sh) => sh.status === "failed");
|
|
1161
|
+
return {
|
|
1162
|
+
kind: "text",
|
|
1163
|
+
text: [
|
|
1164
|
+
`In progress: ${progressLine(film)}.`,
|
|
1165
|
+
failed.length > 0 ? `${failed.length} shot(s) failed and were re-queued (${failed[0]?.errorMessage?.slice(0, 100) ?? ""}).` : "",
|
|
1166
|
+
`Poll again in ~30s with {"film_id": "${filmId}", "action": "status"}.`
|
|
1167
|
+
].filter(Boolean).join("\n")
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
852
1170
|
async function run8(input, ctx) {
|
|
1171
|
+
const action = input.action === "plan" && input.film_id ? "status" : input.action;
|
|
1172
|
+
if (action === "plan") return plan(input, ctx);
|
|
1173
|
+
if (!input.film_id) return { kind: "text", text: "produce/status need the film_id returned by the plan step." };
|
|
1174
|
+
if (action === "produce") return produce(input.film_id, ctx);
|
|
1175
|
+
return advance(input.film_id, input.soundtrack, input.music_job_id, ctx);
|
|
1176
|
+
}
|
|
1177
|
+
var LOOKS, InputSchema8, InputJsonSchema8, shots, count, LOOK_MUSIC, ARC, filmSkill;
|
|
1178
|
+
var init_film = __esm({
|
|
1179
|
+
"src/skills/film.ts"() {
|
|
1180
|
+
LOOKS = ["live-action", "anime", "3d-animation", "noir", "horror", "sci-fi", "documentary", "western"];
|
|
1181
|
+
InputSchema8 = zod.z.object({
|
|
1182
|
+
idea: zod.z.string().max(2e3).optional().describe("One-to-three sentence film idea. The studio writes the screenplay (free). Provide idea OR script for planning."),
|
|
1183
|
+
script: zod.z.string().max(2e4).optional().describe("A full screenplay to plan instead of an idea (sluglines, action lines, dialogue)."),
|
|
1184
|
+
look: zod.z.enum(LOOKS).default("live-action").describe("Visual style carried through frames, clips, and soundtrack."),
|
|
1185
|
+
aspect: zod.z.enum(["16:9", "9:16"]).default("16:9"),
|
|
1186
|
+
length: zod.z.enum(["30s", "60s", "2-3min"]).default("60s").describe("Target length; drives the shot count (~6/12/24 shots)."),
|
|
1187
|
+
film_id: zod.z.string().max(40).optional().describe("Existing film id (from a previous call) for produce/status."),
|
|
1188
|
+
action: zod.z.enum(["plan", "produce", "status"]).default("plan").describe("'plan' creates + storyboards (free). 'produce' starts charged generation. 'status' advances production and reports progress; poll it every ~30s until it returns the finished film."),
|
|
1189
|
+
soundtrack: zod.z.enum(["scene-synced", "flat", "none"]).default("scene-synced").describe("scene-synced composes one musical section per scene, pinned to the scene lengths (default). flat is a single cohesive bed. none skips music."),
|
|
1190
|
+
music_job_id: zod.z.string().max(80).optional().describe("Internal: pass back the music job id from a previous status reply so the composing score is attached instead of re-charged.")
|
|
1191
|
+
});
|
|
1192
|
+
InputJsonSchema8 = {
|
|
1193
|
+
type: "object",
|
|
1194
|
+
properties: {
|
|
1195
|
+
idea: { type: "string", description: "One-to-three sentence film idea. The studio writes the screenplay (free)." },
|
|
1196
|
+
script: { type: "string", description: "A full screenplay to plan instead of an idea." },
|
|
1197
|
+
look: { type: "string", enum: [...LOOKS], description: "Visual style. Default live-action." },
|
|
1198
|
+
aspect: { type: "string", enum: ["16:9", "9:16"], description: "Default 16:9." },
|
|
1199
|
+
length: { type: "string", enum: ["30s", "60s", "2-3min"], description: "Target length (~6/12/24 shots). Default 60s." },
|
|
1200
|
+
film_id: { type: "string", description: "Existing film id for produce/status." },
|
|
1201
|
+
action: { type: "string", enum: ["plan", "produce", "status"], description: "plan (free) | produce (charged) | status (poll every ~30s)." },
|
|
1202
|
+
soundtrack: { type: "string", enum: ["scene-synced", "flat", "none"], description: "Default scene-synced." },
|
|
1203
|
+
music_job_id: { type: "string", description: "Pass back from a previous status reply while the score is composing." }
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
shots = (f) => f.scenes.flatMap((s) => s.shots);
|
|
1207
|
+
count = (f, pred) => shots(f).filter(pred).length;
|
|
1208
|
+
LOOK_MUSIC = {
|
|
1209
|
+
"live-action": "modern cinematic underscore, warm strings and soft felt piano, subtle rhythmic pulse, slow emotional build",
|
|
1210
|
+
anime: "sweeping anime film score, lush strings and piano, taiko drum accents, soaring heartfelt theme",
|
|
1211
|
+
"3d-animation": "playful heartfelt family-animation score, pizzicato strings, celesta and warm woodwinds, gentle sense of wonder",
|
|
1212
|
+
noir: "smoky film-noir jazz underscore, brushed drums, upright bass, distant muted trumpet, slow simmering tension",
|
|
1213
|
+
horror: "sparse horror underscore, low cello drones, dissonant string swells, creeping heartbeat pulse, unsettling quiet",
|
|
1214
|
+
"sci-fi": "atmospheric science-fiction score, analog synth pads, deep sub-bass pulses, shimmering arpeggios, vast and cold",
|
|
1215
|
+
documentary: "understated documentary score, felt piano, soft string beds, organic percussion textures, reflective and true",
|
|
1216
|
+
western: "cinematic western score, lonely twangy guitar, dusty strings, distant horns, wide-open desert tension"
|
|
1217
|
+
};
|
|
1218
|
+
ARC = {
|
|
1219
|
+
opening: ["establishing the theme", "restrained", "quiet anticipation"],
|
|
1220
|
+
middle: ["building tension", "rising intensity", "forward motion"],
|
|
1221
|
+
finale: ["climactic", "full arrangement", "emotional peak resolving to stillness"]
|
|
1222
|
+
};
|
|
1223
|
+
filmSkill = {
|
|
1224
|
+
name: "pixeldojo:film",
|
|
1225
|
+
description: "Make a finished short film from one idea: screenplay, storyboard frames (AI-reviewed), video clips with spoken dialogue, scene-synced soundtrack, stitched export. Phased: plan is free and returns a cost estimate; produce starts charged generation; status advances production (poll every ~30s).",
|
|
1226
|
+
inputZod: InputSchema8,
|
|
1227
|
+
inputJsonSchema: InputJsonSchema8,
|
|
1228
|
+
allowedModels: [],
|
|
1229
|
+
defaultModel: "",
|
|
1230
|
+
run: run8
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
});
|
|
1234
|
+
async function run9(input, ctx) {
|
|
1235
|
+
const productImages = [
|
|
1236
|
+
...input.product_image_urls ?? [],
|
|
1237
|
+
...await Promise.all((input.product_image_paths ?? []).map((p) => uploadLocalFile(p, ctx.client)))
|
|
1238
|
+
];
|
|
1239
|
+
const characterImages = [
|
|
1240
|
+
...input.character_image_urls ?? [],
|
|
1241
|
+
...await Promise.all((input.character_image_paths ?? []).map((p) => uploadLocalFile(p, ctx.client)))
|
|
1242
|
+
];
|
|
1243
|
+
const body = {
|
|
1244
|
+
style: input.style,
|
|
1245
|
+
product_url: input.product_url,
|
|
1246
|
+
product: !input.product_url && input.product_name ? {
|
|
1247
|
+
name: input.product_name,
|
|
1248
|
+
description: input.product_description,
|
|
1249
|
+
images: productImages
|
|
1250
|
+
} : void 0,
|
|
1251
|
+
character_images: characterImages.length > 0 ? characterImages : void 0,
|
|
1252
|
+
location: input.location_description || input.location_image_url ? { description: input.location_description, image_url: input.location_image_url } : void 0,
|
|
1253
|
+
hook: input.hook,
|
|
1254
|
+
script: input.script,
|
|
1255
|
+
quality: input.quality,
|
|
1256
|
+
resolution: input.resolution,
|
|
1257
|
+
aspect_ratio: input.aspect_ratio,
|
|
1258
|
+
duration: input.duration
|
|
1259
|
+
};
|
|
1260
|
+
const client = ctx.client;
|
|
1261
|
+
const submitted = await client.runAd(body);
|
|
1262
|
+
const result = await pollJobUntilDoneOrHandoff(
|
|
1263
|
+
submitted.jobId,
|
|
1264
|
+
(id) => ctx.client.getJob(id),
|
|
1265
|
+
ctx.buildStatusUrl,
|
|
1266
|
+
{
|
|
1267
|
+
now: ctx.pollOverrides?.now,
|
|
1268
|
+
sleep: ctx.pollOverrides?.sleep
|
|
1269
|
+
}
|
|
1270
|
+
);
|
|
1271
|
+
const styleLabel = submitted.ad?.style ?? input.style ?? "ugc-testimonial";
|
|
1272
|
+
return pollResultToSkillResult(
|
|
1273
|
+
result,
|
|
1274
|
+
(urls) => urls.length === 0 ? `Ad job ${submitted.jobId} completed but returned no assets.` : `Ad rendered (style: ${styleLabel}${submitted.ad?.product?.name ? `, product: ${submitted.ad.product.name}` : ""}). Video: ${urls[0]}`
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
var InputSchema9, InputJsonSchema9, adSkill;
|
|
1278
|
+
var init_ad = __esm({
|
|
1279
|
+
"src/skills/ad.ts"() {
|
|
1280
|
+
init_types();
|
|
1281
|
+
init_poll();
|
|
1282
|
+
init_upload_helpers();
|
|
1283
|
+
InputSchema9 = zod.z.object({
|
|
1284
|
+
style: zod.z.string().max(80).optional().describe(
|
|
1285
|
+
"Ad style slug (default ugc-testimonial). Catalog: GET /api/v1/marketing/presets \u2014 e.g. ugc-testimonial, unboxing-reveal, unboxing-asmr, virtual-try-on, street-style, product-showcase, macro-details, crush-test, hyper-motion, before-after, day-in-the-life, the-gift, tv-spot."
|
|
1286
|
+
),
|
|
1287
|
+
product_url: zod.z.string().url().optional().describe("Product page URL. Name, description, and images are extracted automatically."),
|
|
1288
|
+
product_name: zod.z.string().max(200).optional(),
|
|
1289
|
+
product_description: zod.z.string().max(2e3).optional(),
|
|
1290
|
+
product_image_urls: zod.z.array(zod.z.string().url()).max(5).optional(),
|
|
1291
|
+
product_image_paths: zod.z.array(zod.z.string()).max(5).optional().describe("Absolute local paths; uploaded to 24h temp storage automatically."),
|
|
1292
|
+
character_image_urls: zod.z.array(zod.z.string().url()).max(4).optional().describe("Presenter reference images for a consistent on-camera character."),
|
|
1293
|
+
character_image_paths: zod.z.array(zod.z.string()).max(4).optional(),
|
|
1294
|
+
location_description: zod.z.string().max(1500).optional().describe('Where the ad takes place ("a sunlit Parisian cafe"). Text is enough; no image needed.'),
|
|
1295
|
+
location_image_url: zod.z.string().url().optional(),
|
|
1296
|
+
hook: zod.z.string().max(1e3).optional().describe("The opening visual beat (first 1-2 seconds). Omit to use the style default."),
|
|
1297
|
+
script: zod.z.string().max(1e3).optional().describe("The exact spoken line, voiced word for word. Omit and the presenter improvises."),
|
|
1298
|
+
quality: zod.z.enum(["mini", "fast", "high"]).optional(),
|
|
1299
|
+
resolution: zod.z.enum(["480p", "720p", "1080p", "4k"]).optional().describe('1080p/4k require quality "high".'),
|
|
1300
|
+
aspect_ratio: zod.z.enum(["16:9", "4:3", "1:1", "3:4", "9:16", "21:9"]).optional(),
|
|
1301
|
+
duration: zod.z.number().int().min(4).max(15).optional()
|
|
1302
|
+
}).refine(
|
|
1303
|
+
(v) => v.product_url || (v.product_image_urls?.length || v.product_image_paths?.length) && v.product_name,
|
|
1304
|
+
{
|
|
1305
|
+
message: "Pass product_url, OR product_name plus product_image_urls/product_image_paths.",
|
|
1306
|
+
path: ["product_url"]
|
|
1307
|
+
}
|
|
1308
|
+
);
|
|
1309
|
+
InputJsonSchema9 = {
|
|
1310
|
+
type: "object",
|
|
1311
|
+
properties: {
|
|
1312
|
+
style: {
|
|
1313
|
+
type: "string",
|
|
1314
|
+
description: "Ad style slug (default ugc-testimonial). Catalog: GET /api/v1/marketing/presets \u2014 e.g. ugc-testimonial, unboxing-reveal, unboxing-asmr, virtual-try-on, street-style, mirror-fit-check, pro-try-on, product-showcase, macro-details, secret-hack-reveal, crush-test, hyper-motion, before-after, day-in-the-life, the-gift, tv-spot, gadget-saved-me, selfie-testimonial, direct-to-camera, pack-an-order, mystery-box."
|
|
1315
|
+
},
|
|
1316
|
+
product_url: {
|
|
1317
|
+
type: "string",
|
|
1318
|
+
format: "uri",
|
|
1319
|
+
description: "Product page URL. Name, description, and images are extracted automatically."
|
|
1320
|
+
},
|
|
1321
|
+
product_name: { type: "string" },
|
|
1322
|
+
product_description: { type: "string" },
|
|
1323
|
+
product_image_urls: { type: "array", items: { type: "string", format: "uri" }, maxItems: 5 },
|
|
1324
|
+
product_image_paths: {
|
|
1325
|
+
type: "array",
|
|
1326
|
+
items: { type: "string" },
|
|
1327
|
+
maxItems: 5,
|
|
1328
|
+
description: "Absolute local file paths; uploaded to 24h temp storage automatically."
|
|
1329
|
+
},
|
|
1330
|
+
character_image_urls: {
|
|
1331
|
+
type: "array",
|
|
1332
|
+
items: { type: "string", format: "uri" },
|
|
1333
|
+
maxItems: 4,
|
|
1334
|
+
description: "Presenter reference images for a consistent on-camera character."
|
|
1335
|
+
},
|
|
1336
|
+
character_image_paths: { type: "array", items: { type: "string" }, maxItems: 4 },
|
|
1337
|
+
location_description: {
|
|
1338
|
+
type: "string",
|
|
1339
|
+
description: 'Where the ad takes place ("a sunlit Parisian cafe"). Text is enough.'
|
|
1340
|
+
},
|
|
1341
|
+
location_image_url: { type: "string", format: "uri" },
|
|
1342
|
+
hook: {
|
|
1343
|
+
type: "string",
|
|
1344
|
+
description: "The opening visual beat (first 1-2 seconds). Omit to use the style default."
|
|
1345
|
+
},
|
|
1346
|
+
script: {
|
|
1347
|
+
type: "string",
|
|
1348
|
+
description: "The exact spoken line, voiced word for word. Omit and the presenter improvises."
|
|
1349
|
+
},
|
|
1350
|
+
quality: { type: "string", enum: ["mini", "fast", "high"] },
|
|
1351
|
+
resolution: {
|
|
1352
|
+
type: "string",
|
|
1353
|
+
enum: ["480p", "720p", "1080p", "4k"],
|
|
1354
|
+
description: '1080p/4k require quality "high".'
|
|
1355
|
+
},
|
|
1356
|
+
aspect_ratio: { type: "string", enum: ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9"] },
|
|
1357
|
+
duration: { type: "integer", minimum: 4, maximum: 15 }
|
|
1358
|
+
}
|
|
1359
|
+
// "product_url OR product_name+images" is enforced by the server and the
|
|
1360
|
+
// Zod refine; no root-level oneOf (Grok rejects non-object root unions).
|
|
1361
|
+
};
|
|
1362
|
+
adSkill = {
|
|
1363
|
+
name: "pixeldojo:ad",
|
|
1364
|
+
description: "Marketing Studio: turn any product into a video ad. Pass a product URL (auto-extracted) or product images, pick an ad style (UGC testimonial, unboxing, try-on, showcase, TV spot, and more via GET /api/v1/marketing/presets), optionally add presenter reference images, a location, a hook, and the exact spoken script. Renders with voiceover; returns the video or a statusUrl handoff to poll via pixeldojo:status. Ideal for automated generate-and-post workflows.",
|
|
1365
|
+
inputZod: InputSchema9,
|
|
1366
|
+
inputJsonSchema: InputJsonSchema9,
|
|
1367
|
+
// Composition skill — the server routes to the ad engine internally.
|
|
1368
|
+
allowedModels: [],
|
|
1369
|
+
defaultModel: "",
|
|
1370
|
+
run: run9
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
});
|
|
1374
|
+
async function run10(input, ctx) {
|
|
853
1375
|
const client = ctx.client;
|
|
854
1376
|
const result = await client.runCampaign(input);
|
|
855
1377
|
if (result.kind === "campaign") {
|
|
@@ -876,10 +1398,10 @@ async function run8(input, ctx) {
|
|
|
876
1398
|
statusUrl: result.statusUrl ?? ctx.buildStatusUrl(result.campaignId)
|
|
877
1399
|
};
|
|
878
1400
|
}
|
|
879
|
-
var
|
|
1401
|
+
var InputSchema10, InputJsonSchema10, campaignSkill;
|
|
880
1402
|
var init_campaign = __esm({
|
|
881
1403
|
"src/skills/campaign.ts"() {
|
|
882
|
-
|
|
1404
|
+
InputSchema10 = zod.z.object({
|
|
883
1405
|
productUrl: zod.z.string().url().optional().describe("Product page URL. We extract name + description + images automatically."),
|
|
884
1406
|
product: zod.z.object({
|
|
885
1407
|
name: zod.z.string().min(1).max(200),
|
|
@@ -895,7 +1417,7 @@ var init_campaign = __esm({
|
|
|
895
1417
|
message: "productUrl or product is required",
|
|
896
1418
|
path: ["productUrl"]
|
|
897
1419
|
});
|
|
898
|
-
|
|
1420
|
+
InputJsonSchema10 = {
|
|
899
1421
|
type: "object",
|
|
900
1422
|
properties: {
|
|
901
1423
|
productUrl: {
|
|
@@ -926,17 +1448,17 @@ var init_campaign = __esm({
|
|
|
926
1448
|
campaignSkill = {
|
|
927
1449
|
name: "pixeldojo:campaign",
|
|
928
1450
|
description: "Generate a coherent product launch package (hero image + N lifestyle variants + optional vertical video) from one URL or product profile. Composes hero / lifestyle / video models server-side; returns assets in one call or a handoff with campaignId to poll via pixeldojo:campaign_status.",
|
|
929
|
-
inputZod:
|
|
930
|
-
inputJsonSchema:
|
|
1451
|
+
inputZod: InputSchema10,
|
|
1452
|
+
inputJsonSchema: InputJsonSchema10,
|
|
931
1453
|
// Composition skill — internal sub-jobs route to per-tool models, no
|
|
932
1454
|
// single allowed-models list applies here.
|
|
933
1455
|
allowedModels: [],
|
|
934
1456
|
defaultModel: "",
|
|
935
|
-
run:
|
|
1457
|
+
run: run10
|
|
936
1458
|
};
|
|
937
1459
|
}
|
|
938
1460
|
});
|
|
939
|
-
async function
|
|
1461
|
+
async function run11(input, ctx) {
|
|
940
1462
|
const client = ctx.client;
|
|
941
1463
|
const result = await client.getCampaign(input.campaignId);
|
|
942
1464
|
if (result.kind === "campaign") {
|
|
@@ -966,13 +1488,13 @@ async function run9(input, ctx) {
|
|
|
966
1488
|
statusUrl: result.statusUrl ?? ctx.buildStatusUrl(result.campaignId)
|
|
967
1489
|
};
|
|
968
1490
|
}
|
|
969
|
-
var
|
|
1491
|
+
var InputSchema11, InputJsonSchema11, campaignStatusSkill;
|
|
970
1492
|
var init_campaign_status = __esm({
|
|
971
1493
|
"src/skills/campaign-status.ts"() {
|
|
972
|
-
|
|
1494
|
+
InputSchema11 = zod.z.object({
|
|
973
1495
|
campaignId: zod.z.string().min(1).max(200).describe("Campaign id returned from a prior pixeldojo:campaign handoff.")
|
|
974
1496
|
});
|
|
975
|
-
|
|
1497
|
+
InputJsonSchema11 = {
|
|
976
1498
|
type: "object",
|
|
977
1499
|
properties: {
|
|
978
1500
|
campaignId: {
|
|
@@ -985,15 +1507,15 @@ var init_campaign_status = __esm({
|
|
|
985
1507
|
campaignStatusSkill = {
|
|
986
1508
|
name: "pixeldojo:campaign_status",
|
|
987
1509
|
description: "Poll a campaign by campaignId returned from pixeldojo:campaign. Returns assets when every sub-job is done, or a handoff describing what is still in flight.",
|
|
988
|
-
inputZod:
|
|
989
|
-
inputJsonSchema:
|
|
1510
|
+
inputZod: InputSchema11,
|
|
1511
|
+
inputJsonSchema: InputJsonSchema11,
|
|
990
1512
|
allowedModels: [],
|
|
991
1513
|
defaultModel: "",
|
|
992
|
-
run:
|
|
1514
|
+
run: run11
|
|
993
1515
|
};
|
|
994
1516
|
}
|
|
995
1517
|
});
|
|
996
|
-
async function
|
|
1518
|
+
async function run12(input, ctx) {
|
|
997
1519
|
let url;
|
|
998
1520
|
try {
|
|
999
1521
|
url = await uploadLocalFile(input.path, ctx.client);
|
|
@@ -1009,16 +1531,16 @@ async function run10(input, ctx) {
|
|
|
1009
1531
|
(temp storage; auto-deletes in ~24h \u2014 pass this URL as image_url to :generate or :edit)`
|
|
1010
1532
|
};
|
|
1011
1533
|
}
|
|
1012
|
-
var
|
|
1534
|
+
var InputSchema12, InputJsonSchema12, uploadSkill;
|
|
1013
1535
|
var init_upload = __esm({
|
|
1014
1536
|
"src/skills/upload.ts"() {
|
|
1015
1537
|
init_upload_helpers();
|
|
1016
|
-
|
|
1538
|
+
InputSchema12 = zod.z.object({
|
|
1017
1539
|
path: zod.z.string().min(1).max(4096).describe(
|
|
1018
1540
|
"Absolute local file path to upload. JPEG, PNG, WebP, GIF (\u226450MB) or MP4, WebM, MOV (\u2264200MB). Returns a public URL on the temp CDN; storage auto-expires after 24 hours."
|
|
1019
1541
|
)
|
|
1020
1542
|
});
|
|
1021
|
-
|
|
1543
|
+
InputJsonSchema12 = {
|
|
1022
1544
|
type: "object",
|
|
1023
1545
|
properties: {
|
|
1024
1546
|
path: {
|
|
@@ -1031,15 +1553,15 @@ var init_upload = __esm({
|
|
|
1031
1553
|
uploadSkill = {
|
|
1032
1554
|
name: "pixeldojo:upload",
|
|
1033
1555
|
description: "Upload a local image or video file and get back a public URL you can pass to :generate or :edit as image_url. Use when the user has a reference image on disk (drag-dropped into Claude Code, screenshot, etc.) and you need a URL. Storage auto-expires after 24 hours.",
|
|
1034
|
-
inputZod:
|
|
1035
|
-
inputJsonSchema:
|
|
1556
|
+
inputZod: InputSchema12,
|
|
1557
|
+
inputJsonSchema: InputJsonSchema12,
|
|
1036
1558
|
allowedModels: [],
|
|
1037
1559
|
defaultModel: "",
|
|
1038
|
-
run:
|
|
1560
|
+
run: run12
|
|
1039
1561
|
};
|
|
1040
1562
|
}
|
|
1041
1563
|
});
|
|
1042
|
-
async function
|
|
1564
|
+
async function run13(input, ctx) {
|
|
1043
1565
|
const requested = input.model ?? DEFAULT_MODEL6;
|
|
1044
1566
|
if (!ALLOWED4.includes(requested)) {
|
|
1045
1567
|
return {
|
|
@@ -1088,7 +1610,7 @@ async function run11(input, ctx) {
|
|
|
1088
1610
|
(urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Audio ready. Asset: ${urls[0]}`
|
|
1089
1611
|
);
|
|
1090
1612
|
}
|
|
1091
|
-
var ALLOWED4, DEFAULT_MODEL6,
|
|
1613
|
+
var ALLOWED4, DEFAULT_MODEL6, InputSchema13, InputJsonSchema13, audioSkill;
|
|
1092
1614
|
var init_audio = __esm({
|
|
1093
1615
|
"src/skills/audio.ts"() {
|
|
1094
1616
|
init_poll();
|
|
@@ -1096,7 +1618,7 @@ var init_audio = __esm({
|
|
|
1096
1618
|
init_types();
|
|
1097
1619
|
ALLOWED4 = ["seed-audio", "text-to-speech"];
|
|
1098
1620
|
DEFAULT_MODEL6 = "seed-audio";
|
|
1099
|
-
|
|
1621
|
+
InputSchema13 = zod.z.object({
|
|
1100
1622
|
prompt: zod.z.string().min(1).describe(
|
|
1101
1623
|
"A script for speech, or a scene description for cinematic audio (dialogue + effects + music). Reference uploaded clips as @Audio1..@Audio3."
|
|
1102
1624
|
),
|
|
@@ -1107,7 +1629,7 @@ var init_audio = __esm({
|
|
|
1107
1629
|
output_format: zod.z.string().optional().describe("seed-audio: mp3 (default), wav, pcm, ogg_opus. text-to-speech: mp3, wav, flac, pcm."),
|
|
1108
1630
|
speed: zod.z.number().min(0.5).max(2).optional().describe("Speech speed multiplier (0.5\u20132).")
|
|
1109
1631
|
});
|
|
1110
|
-
|
|
1632
|
+
InputJsonSchema13 = {
|
|
1111
1633
|
type: "object",
|
|
1112
1634
|
properties: {
|
|
1113
1635
|
prompt: { type: "string" },
|
|
@@ -1123,15 +1645,15 @@ var init_audio = __esm({
|
|
|
1123
1645
|
audioSkill = {
|
|
1124
1646
|
name: "pixeldojo:audio",
|
|
1125
1647
|
description: "Generate audio: speech, multi-character dialogue, sound effects, and background music from one prompt (Seed Audio 1.0 by default; plain TTS available).",
|
|
1126
|
-
inputZod:
|
|
1127
|
-
inputJsonSchema:
|
|
1648
|
+
inputZod: InputSchema13,
|
|
1649
|
+
inputJsonSchema: InputJsonSchema13,
|
|
1128
1650
|
allowedModels: [...ALLOWED4],
|
|
1129
1651
|
defaultModel: DEFAULT_MODEL6,
|
|
1130
|
-
run:
|
|
1652
|
+
run: run13
|
|
1131
1653
|
};
|
|
1132
1654
|
}
|
|
1133
1655
|
});
|
|
1134
|
-
async function
|
|
1656
|
+
async function run14(input, ctx) {
|
|
1135
1657
|
const res = await ctx.client.listLibrary({
|
|
1136
1658
|
q: input.q,
|
|
1137
1659
|
model: input.model,
|
|
@@ -1162,16 +1684,16 @@ ${lines.join("\n")}
|
|
|
1162
1684
|
Pass any URL back into :generate, :edit, :character, or :upscale as a reference input.`
|
|
1163
1685
|
};
|
|
1164
1686
|
}
|
|
1165
|
-
var
|
|
1687
|
+
var InputSchema14, InputJsonSchema14, librarySkill;
|
|
1166
1688
|
var init_library = __esm({
|
|
1167
1689
|
"src/skills/library.ts"() {
|
|
1168
|
-
|
|
1690
|
+
InputSchema14 = zod.z.object({
|
|
1169
1691
|
q: zod.z.string().optional().describe("Substring match against the prompt that produced the asset."),
|
|
1170
1692
|
model: zod.z.string().optional().describe("Restrict to a single model (apiId)."),
|
|
1171
1693
|
modality: zod.z.enum(["image", "video", "audio"]).optional().describe("Restrict to one output type."),
|
|
1172
1694
|
limit: zod.z.number().int().min(1).max(50).optional().describe("Max results (default 10).")
|
|
1173
1695
|
});
|
|
1174
|
-
|
|
1696
|
+
InputJsonSchema14 = {
|
|
1175
1697
|
type: "object",
|
|
1176
1698
|
properties: {
|
|
1177
1699
|
q: { type: "string" },
|
|
@@ -1184,15 +1706,15 @@ var init_library = __esm({
|
|
|
1184
1706
|
librarySkill = {
|
|
1185
1707
|
name: "pixeldojo:library",
|
|
1186
1708
|
description: "Search your media library: saved My Media items (permanent) and recent API/MCP generations (expire after 24h), newest first. Returns asset URLs to reuse as reference inputs. Keep a result forever with pixeldojo:save.",
|
|
1187
|
-
inputZod:
|
|
1188
|
-
inputJsonSchema:
|
|
1709
|
+
inputZod: InputSchema14,
|
|
1710
|
+
inputJsonSchema: InputJsonSchema14,
|
|
1189
1711
|
allowedModels: [],
|
|
1190
1712
|
defaultModel: "",
|
|
1191
|
-
run:
|
|
1713
|
+
run: run14
|
|
1192
1714
|
};
|
|
1193
1715
|
}
|
|
1194
1716
|
});
|
|
1195
|
-
async function
|
|
1717
|
+
async function run15(input, ctx) {
|
|
1196
1718
|
const res = await ctx.client.saveMedia({
|
|
1197
1719
|
url: input.url,
|
|
1198
1720
|
prompt: input.prompt,
|
|
@@ -1206,17 +1728,17 @@ async function run13(input, ctx) {
|
|
|
1206
1728
|
It will appear at https://pixeldojo.ai/my-images and in pixeldojo:library searches.`
|
|
1207
1729
|
};
|
|
1208
1730
|
}
|
|
1209
|
-
var
|
|
1731
|
+
var InputSchema15, InputJsonSchema15, saveSkill;
|
|
1210
1732
|
var init_save = __esm({
|
|
1211
1733
|
"src/skills/save.ts"() {
|
|
1212
|
-
|
|
1734
|
+
InputSchema15 = zod.z.object({
|
|
1213
1735
|
url: zod.z.string().url().describe("The asset URL to save (image or video)."),
|
|
1214
1736
|
prompt: zod.z.string().optional().describe("The prompt that produced it \u2014 makes it findable in the library later."),
|
|
1215
1737
|
tool_name: zod.z.string().optional().describe("Tool/model slug that produced it."),
|
|
1216
1738
|
modality: zod.z.enum(["image", "video", "audio"]).optional(),
|
|
1217
1739
|
job_id: zod.z.string().optional().describe("The jobId that produced this asset, when known.")
|
|
1218
1740
|
});
|
|
1219
|
-
|
|
1741
|
+
InputJsonSchema15 = {
|
|
1220
1742
|
type: "object",
|
|
1221
1743
|
properties: {
|
|
1222
1744
|
url: { type: "string", format: "uri" },
|
|
@@ -1230,11 +1752,11 @@ var init_save = __esm({
|
|
|
1230
1752
|
saveSkill = {
|
|
1231
1753
|
name: "pixeldojo:save",
|
|
1232
1754
|
description: "Save a generated image or video into the user's My Media library permanently. Generation outputs otherwise expire after 24 hours \u2014 call this when the user wants to keep a result.",
|
|
1233
|
-
inputZod:
|
|
1234
|
-
inputJsonSchema:
|
|
1755
|
+
inputZod: InputSchema15,
|
|
1756
|
+
inputJsonSchema: InputJsonSchema15,
|
|
1235
1757
|
allowedModels: [],
|
|
1236
1758
|
defaultModel: "",
|
|
1237
|
-
run:
|
|
1759
|
+
run: run15
|
|
1238
1760
|
};
|
|
1239
1761
|
}
|
|
1240
1762
|
});
|
|
@@ -1446,11 +1968,13 @@ var init_workflows = __esm({
|
|
|
1446
1968
|
var skills_exports = {};
|
|
1447
1969
|
__export(skills_exports, {
|
|
1448
1970
|
ALL_SKILLS: () => ALL_SKILLS,
|
|
1971
|
+
adSkill: () => adSkill,
|
|
1449
1972
|
audioSkill: () => audioSkill,
|
|
1450
1973
|
campaignSkill: () => campaignSkill,
|
|
1451
1974
|
campaignStatusSkill: () => campaignStatusSkill,
|
|
1452
1975
|
characterSkill: () => characterSkill,
|
|
1453
1976
|
editSkill: () => editSkill,
|
|
1977
|
+
filmSkill: () => filmSkill,
|
|
1454
1978
|
findSkill: () => findSkill,
|
|
1455
1979
|
fromUrlSkill: () => fromUrlSkill,
|
|
1456
1980
|
generateSkill: () => generateSkill,
|
|
@@ -1477,6 +2001,8 @@ var init_skills = __esm({
|
|
|
1477
2001
|
init_upscale();
|
|
1478
2002
|
init_status();
|
|
1479
2003
|
init_from_url();
|
|
2004
|
+
init_film();
|
|
2005
|
+
init_ad();
|
|
1480
2006
|
init_campaign();
|
|
1481
2007
|
init_campaign_status();
|
|
1482
2008
|
init_upload();
|
|
@@ -1499,8 +2025,10 @@ var init_skills = __esm({
|
|
|
1499
2025
|
statusSkill,
|
|
1500
2026
|
fromUrlSkill,
|
|
1501
2027
|
uploadSkill,
|
|
2028
|
+
adSkill,
|
|
1502
2029
|
campaignSkill,
|
|
1503
|
-
campaignStatusSkill
|
|
2030
|
+
campaignStatusSkill,
|
|
2031
|
+
filmSkill
|
|
1504
2032
|
];
|
|
1505
2033
|
}
|
|
1506
2034
|
});
|
|
@@ -1555,7 +2083,7 @@ function makeRedactedLogger(target = console) {
|
|
|
1555
2083
|
}
|
|
1556
2084
|
|
|
1557
2085
|
// src/server.ts
|
|
1558
|
-
var PACKAGE_VERSION = "0.
|
|
2086
|
+
var PACKAGE_VERSION = "0.9.0" ;
|
|
1559
2087
|
function buildServer(options) {
|
|
1560
2088
|
if (!options.apiKey) {
|
|
1561
2089
|
throw new Error(
|
|
@@ -1622,6 +2150,8 @@ function buildServer(options) {
|
|
|
1622
2150
|
});
|
|
1623
2151
|
const ctx = {
|
|
1624
2152
|
client,
|
|
2153
|
+
apiKey: options.apiKey,
|
|
2154
|
+
baseUrl,
|
|
1625
2155
|
mcpVersion: version,
|
|
1626
2156
|
logger,
|
|
1627
2157
|
buildStatusUrl: (jobId) => `${baseUrl}/api/v1/jobs/${encodeURIComponent(jobId)}`
|