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