@pixeldojo/mcp 0.25.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/cli.cjs +1184 -75
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1184 -75
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1184 -75
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1184 -75
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -239,11 +239,11 @@ function nearestAspectFromEnum(requested, allowed) {
|
|
|
239
239
|
}
|
|
240
240
|
async function snapAspectRatio(apiId, requested, client, opts = {}) {
|
|
241
241
|
if (!requested) return void 0;
|
|
242
|
-
const
|
|
243
|
-
if (
|
|
242
|
+
const props2 = await getFieldConstraints(apiId, client);
|
|
243
|
+
if (props2 === null) {
|
|
244
244
|
return opts.onUnknown === "omit" ? void 0 : requested;
|
|
245
245
|
}
|
|
246
|
-
const field =
|
|
246
|
+
const field = props2.aspect_ratio;
|
|
247
247
|
if (!field) return void 0;
|
|
248
248
|
if (!field.enum || field.enum.length === 0) return requested;
|
|
249
249
|
return nearestAspectFromEnum(requested, field.enum);
|
|
@@ -274,11 +274,11 @@ function resolutionHeight(value) {
|
|
|
274
274
|
}
|
|
275
275
|
async function snapResolution(apiId, requested, client, opts = {}) {
|
|
276
276
|
if (!requested) return void 0;
|
|
277
|
-
const
|
|
278
|
-
if (
|
|
277
|
+
const props2 = await getFieldConstraints(apiId, client);
|
|
278
|
+
if (props2 === null) {
|
|
279
279
|
return opts.onUnknown === "omit" ? void 0 : requested;
|
|
280
280
|
}
|
|
281
|
-
const field =
|
|
281
|
+
const field = props2.resolution;
|
|
282
282
|
if (!field) return void 0;
|
|
283
283
|
if (!field.enum || field.enum.length === 0) return requested;
|
|
284
284
|
if (field.enum.includes(requested)) return requested;
|
|
@@ -299,11 +299,11 @@ async function snapResolution(apiId, requested, client, opts = {}) {
|
|
|
299
299
|
}
|
|
300
300
|
async function snapDuration(apiId, requested, client, opts = {}) {
|
|
301
301
|
if (requested === void 0) return void 0;
|
|
302
|
-
const
|
|
303
|
-
if (
|
|
302
|
+
const props2 = await getFieldConstraints(apiId, client);
|
|
303
|
+
if (props2 === null) {
|
|
304
304
|
return opts.onUnknown === "omit" ? void 0 : requested;
|
|
305
305
|
}
|
|
306
|
-
const field =
|
|
306
|
+
const field = props2.duration;
|
|
307
307
|
if (!field) return void 0;
|
|
308
308
|
if (field.enum && field.enum.length > 0) {
|
|
309
309
|
const allowed = field.enum.map(Number).filter((n) => Number.isFinite(n));
|
|
@@ -331,11 +331,11 @@ async function resolveImageRefs(apiId, urls, client) {
|
|
|
331
331
|
if (urls.length === 0) return null;
|
|
332
332
|
const info = await getModelInfo(apiId, client);
|
|
333
333
|
if (info === null) return null;
|
|
334
|
-
const
|
|
335
|
-
const key = info.media ? info.media.image?.key : REF_KEYS.find((k) =>
|
|
334
|
+
const props2 = info.props;
|
|
335
|
+
const key = info.media ? info.media.image?.key : REF_KEYS.find((k) => props2[k] !== void 0);
|
|
336
336
|
if (!key) return null;
|
|
337
337
|
const also = info.media?.image?.also;
|
|
338
|
-
const field =
|
|
338
|
+
const field = props2[key] ?? {};
|
|
339
339
|
if (field.type === "array") {
|
|
340
340
|
const cap = typeof field.maxItems === "number" ? field.maxItems : urls.length;
|
|
341
341
|
const kept = urls.slice(0, Math.max(1, cap));
|
|
@@ -1673,7 +1673,7 @@ function needsSave(url) {
|
|
|
1673
1673
|
return Boolean(url && url.startsWith("https://temp.pixeldojo.ai/"));
|
|
1674
1674
|
}
|
|
1675
1675
|
async function filmApi(ctx, method, path, body) {
|
|
1676
|
-
const res = await fetch(`${ctx.baseUrl}${path}`, {
|
|
1676
|
+
const res = await (ctx.fetch ?? fetch)(`${ctx.baseUrl}${path}`, {
|
|
1677
1677
|
method,
|
|
1678
1678
|
headers: {
|
|
1679
1679
|
Authorization: `Bearer ${ctx.apiKey}`,
|
|
@@ -1839,14 +1839,17 @@ async function plan(input, ctx) {
|
|
|
1839
1839
|
look: input.look ?? (music ? MUSIC_VIDEO_LOOKS[0] : "live-action"),
|
|
1840
1840
|
lengthTarget: input.length,
|
|
1841
1841
|
...!music && input.pacing === "trailer" ? { pacing: "trailer" } : {},
|
|
1842
|
+
...input.quality === "final" ? { clipQuality: "final" } : {},
|
|
1842
1843
|
assetIds: [],
|
|
1843
1844
|
...music ? {
|
|
1844
1845
|
// No script step names a music video, so it would stay "Untitled film".
|
|
1845
1846
|
...input.idea?.trim() ? { title: input.idea.trim().slice(0, 80) } : {},
|
|
1846
1847
|
musicVideoMode: true,
|
|
1847
1848
|
musicUrl: input.song_url,
|
|
1848
|
-
...input.song_duration_sec ? { musicDurationSec: input.song_duration_sec } : {}
|
|
1849
|
-
|
|
1849
|
+
...input.song_duration_sec ? { musicDurationSec: input.song_duration_sec } : {}
|
|
1850
|
+
// Engine left on Auto, like the web composer's music mode, so the
|
|
1851
|
+
// planner routes a mature song to H3 and the rest to P-Video 2 Pro
|
|
1852
|
+
// Reference (tests/contracts/mcp-film-music-parity.test.ts).
|
|
1850
1853
|
} : {}
|
|
1851
1854
|
});
|
|
1852
1855
|
if (!created.data.film) return { kind: "text", text: `Could not create the film: ${created.data.error ?? created.status}` };
|
|
@@ -2248,14 +2251,13 @@ async function run10(input, ctx) {
|
|
|
2248
2251
|
if (action === "redo") return redoShots(input, ctx);
|
|
2249
2252
|
return advance(input.film_id, input.soundtrack, input.music_job_id, ctx);
|
|
2250
2253
|
}
|
|
2251
|
-
var STORY_LOOKS, MUSIC_VIDEO_LOOKS, LOOKS,
|
|
2254
|
+
var STORY_LOOKS, MUSIC_VIDEO_LOOKS, LOOKS, InputSchema10, InputJsonSchema10, shots, benignDispatchRefusal, count, frameDone, RETRY_CLEARS, REFUSED, isRefused, PLANNER_POLL_MS, PLANNER_MAX_POLLS, isMusicVideo, needsSlice, stillPlanningText, LOOK_MUSIC, ARC, filmSkill;
|
|
2252
2255
|
var init_film = __esm({
|
|
2253
2256
|
"src/skills/film.ts"() {
|
|
2254
2257
|
init_upload_helpers();
|
|
2255
|
-
STORY_LOOKS = ["live-action", "anime", "3d-animation", "noir", "horror", "sci-fi", "documentary", "western", "romance", "romantic-comedy", "comedy"];
|
|
2258
|
+
STORY_LOOKS = ["live-action", "action", "anime", "3d-animation", "noir", "horror", "sci-fi", "documentary", "western", "romance", "romantic-comedy", "comedy"];
|
|
2256
2259
|
MUSIC_VIDEO_LOOKS = ["rap-video", "neon-club", "stage-performance", "retro-camcorder", "y2k-pop", "moody-rnb", "rock-grit", "dream-pop", "k-pop"];
|
|
2257
2260
|
LOOKS = [...STORY_LOOKS, ...MUSIC_VIDEO_LOOKS];
|
|
2258
|
-
MUSIC_CLIP_ENGINE_ID = "minimax-h3-reference";
|
|
2259
2261
|
InputSchema10 = zod.z.object({
|
|
2260
2262
|
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."),
|
|
2261
2263
|
script: zod.z.string().max(2e4).optional().describe("A full screenplay to plan instead of an idea (sluglines, action lines, dialogue)."),
|
|
@@ -2266,6 +2268,7 @@ var init_film = __esm({
|
|
|
2266
2268
|
lyrics: zod.z.string().max(1e4).optional().describe("Music video mode: the song lyrics, so shots land on the right lines. Optional."),
|
|
2267
2269
|
aspect: zod.z.enum(["16:9", "9:16"]).default("16:9"),
|
|
2268
2270
|
pacing: zod.z.enum(["story", "trailer"]).default("story").describe("'story' (default) plays every shot's action whole. 'trailer' cuts it like a trailer or promo: most shots keep only their first 1.5-3 seconds."),
|
|
2271
|
+
quality: zod.z.enum(["standard", "final"]).default("standard").describe("'standard' (default) renders 720p clips and a 720p film. 'final' renders 1080p clips where the engine supports it and exports the film at 1080p; it costs more per second."),
|
|
2269
2272
|
length: zod.z.enum(["30s", "60s", "2-3min", "5min"]).default("60s").describe("Target length of the finished film."),
|
|
2270
2273
|
film_id: zod.z.string().max(40).optional().describe("Existing film id (from a previous call) for produce/status/cast/redo."),
|
|
2271
2274
|
action: zod.z.enum(["plan", "produce", "status", "cast", "redo"]).default("plan").describe("'plan' creates + storyboards (free). 'cast' casts, replaces or removes characters on an existing film. 'produce' starts charged generation. 'status' advances production and reports progress; poll it every ~30s until it returns the finished film. 'redo' renders a new take of the listed shots (charged), then status re-saves and re-exports."),
|
|
@@ -2305,6 +2308,11 @@ var init_film = __esm({
|
|
|
2305
2308
|
lyrics: { type: "string", description: "Plan step, music video mode (optional): the song lyrics, so shots land on the right lines." },
|
|
2306
2309
|
aspect: { type: "string", enum: ["16:9", "9:16"], description: "Plan step: frame aspect ratio. Default 16:9." },
|
|
2307
2310
|
length: { type: "string", enum: ["30s", "60s", "2-3min", "5min"], description: "Plan step: target length. Default 60s. Ignored for a music video, whose length is the song." },
|
|
2311
|
+
quality: {
|
|
2312
|
+
type: "string",
|
|
2313
|
+
enum: ["standard", "final"],
|
|
2314
|
+
description: "Plan step: 'standard' (default) is 720p. 'final' renders 1080p clips where the engine supports it and exports at 1080p, at a higher price per second."
|
|
2315
|
+
},
|
|
2308
2316
|
pacing: {
|
|
2309
2317
|
type: "string",
|
|
2310
2318
|
enum: ["story", "trailer"],
|
|
@@ -2671,7 +2679,7 @@ var init_video = __esm({
|
|
|
2671
2679
|
}
|
|
2672
2680
|
});
|
|
2673
2681
|
async function episodeApi(ctx, method, path, body) {
|
|
2674
|
-
const res = await fetch(`${ctx.baseUrl}/api/faceless-studio/episodes${path}`, {
|
|
2682
|
+
const res = await (ctx.fetch ?? fetch)(`${ctx.baseUrl}/api/faceless-studio/episodes${path}`, {
|
|
2675
2683
|
method,
|
|
2676
2684
|
headers: {
|
|
2677
2685
|
Authorization: `Bearer ${ctx.apiKey}`,
|
|
@@ -2853,7 +2861,7 @@ var init_episode = __esm({
|
|
|
2853
2861
|
"src/skills/episode.ts"() {
|
|
2854
2862
|
STYLES2 = ["editorial-motion", "paper-diorama", "hand-drawn", "storybook", "poster-vector", "soft-3d"];
|
|
2855
2863
|
CATEGORIES = ["education", "history", "kids", "storytelling", "custom"];
|
|
2856
|
-
ENGINES = ["omni-draft", "omni-final", "still-motion"];
|
|
2864
|
+
ENGINES = ["omni-draft", "omni-final", "omni-1080", "still-motion"];
|
|
2857
2865
|
MIN_SECONDS = 60;
|
|
2858
2866
|
MAX_SECONDS = 300;
|
|
2859
2867
|
MAX_IN_FLIGHT = 4;
|
|
@@ -2888,7 +2896,7 @@ var init_episode = __esm({
|
|
|
2888
2896
|
engine: {
|
|
2889
2897
|
type: "string",
|
|
2890
2898
|
enum: [...ENGINES],
|
|
2891
|
-
description: "Create step (optional): render every scene with one engine. omni-draft: fast, cheap motion preview. omni-final: full quality motion with sound. still-motion: one illustration per scene, gently panned, cheapest. Default: the style picks."
|
|
2899
|
+
description: "Create step (optional): render every scene with one engine. omni-draft: fast, cheap motion preview. omni-final: full quality motion with sound (720p). omni-1080: the same at 1080p, and the episode exports at 1080p. still-motion: one illustration per scene, gently panned, cheapest. Default: the style picks."
|
|
2892
2900
|
},
|
|
2893
2901
|
music: { type: "boolean", description: "Create step: add a music bed under the narration. Default true." },
|
|
2894
2902
|
title: { type: "string", description: "Create step (optional): episode title. Defaults to the topic." }
|
|
@@ -2911,6 +2919,479 @@ var init_episode = __esm({
|
|
|
2911
2919
|
};
|
|
2912
2920
|
}
|
|
2913
2921
|
});
|
|
2922
|
+
async function dramaApi(ctx, method, path, body) {
|
|
2923
|
+
const res = await (ctx.fetch ?? fetch)(`${ctx.baseUrl}/api/v1/drama${path}`, {
|
|
2924
|
+
method,
|
|
2925
|
+
headers: {
|
|
2926
|
+
Authorization: `Bearer ${ctx.apiKey}`,
|
|
2927
|
+
"Content-Type": "application/json",
|
|
2928
|
+
"x-pixeldojo-source": `mcp/${ctx.mcpVersion}`,
|
|
2929
|
+
"x-pixeldojo-skill": "drama"
|
|
2930
|
+
},
|
|
2931
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
2932
|
+
});
|
|
2933
|
+
const data = await res.json().catch(() => ({}));
|
|
2934
|
+
return { status: res.status, data };
|
|
2935
|
+
}
|
|
2936
|
+
function describeDrama(v, o = DRAMA_OPTS) {
|
|
2937
|
+
const id = v.id;
|
|
2938
|
+
const stubborn = Array.from(new Set(v.retaken.filter((b, i, all) => all.indexOf(b) !== i)));
|
|
2939
|
+
const simplify = stubborn.length ? `${o.unit[0].toUpperCase()}${o.unit.slice(1)}s ${stubborn.join(", ")} have been filmed three times: suggest simplifying them (drop an action, split a busy shot, change the angle) rather than rewording the note.` : "";
|
|
2940
|
+
const next2 = (action) => `${o.tool} with {"${o.idField}": "${id}", "action": "${action}"}`;
|
|
2941
|
+
const music = o.unit === "section";
|
|
2942
|
+
const what = music ? "music video" : "episode";
|
|
2943
|
+
if (v.status === "failed") return { kind: "text", text: `The ${what} stopped: ${v.error ?? "unknown error"}. Each render that failed was refunded.${v.beats.some((b) => b.url) ? ` Re-film ${o.unit}s with action "redo".` : ""}` };
|
|
2944
|
+
if (v.status === "quote") {
|
|
2945
|
+
const cost = music ? `Filming costs about ${v.estimate_credits} credits (cast portraits and ${v.sections?.length ?? "several"} sections lip-synced to the song).` : `Filming costs about ${v.estimate_credits} credits (cast portraits, 12-second scenes with their dialogue and sound${v.score === false ? ", no music" : ", and one score over the finished edit"}); every scene is checked, and a scene that drops a line or shows a clear fault waits for the user's OK before it is filmed again, at the scene's own price (a render that fails is tried again free).`;
|
|
2946
|
+
return {
|
|
2947
|
+
kind: "text",
|
|
2948
|
+
text: [`Planned (${o.idField}: ${id}), ${v.format === "9:16" ? "vertical 9:16" : "widescreen 16:9"} (to switch, create it again in the other format before producing):`, v.plan ?? "", "", `Nothing has been charged yet. ${cost} Show this to the user and, once they agree, call ${next2("produce")}. (In Sensei: put it in pixeldojo_plan as ONE step with skill "${music ? "music_video" : "drama"}" and params {"${o.idField}": "${id}"}, which prices it from this quote.)`].join("\n")
|
|
2949
|
+
};
|
|
2950
|
+
}
|
|
2951
|
+
if (v.status === "notes") {
|
|
2952
|
+
const flagged = v.beats.filter((b) => b.note && (b.needs_redo ?? true));
|
|
2953
|
+
const other = v.beats.filter((b) => b.note && b.needs_redo === false);
|
|
2954
|
+
return {
|
|
2955
|
+
kind: "text",
|
|
2956
|
+
text: [
|
|
2957
|
+
`Every ${o.unit} is filmed. Before the cut, the review flagged ${o.unit}s ${flagged.map((b) => b.beat).join(", ")}:`,
|
|
2958
|
+
...flagged.map((b) => `- ${o.unit} ${b.beat}: ${b.note}${b.redo_films ? ` The redo films: ${b.redo_films}` : ""}`),
|
|
2959
|
+
other.length ? `Also noted, no redo needed: ${other.map((b) => `${o.unit} ${b.beat}: ${b.note}`).join(" ")}` : "",
|
|
2960
|
+
simplify,
|
|
2961
|
+
`Show these to the user and ask which they want: redo those ${o.unit}s with these notes (${o.tool} with {"${o.idField}": "${id}", "action": "redo", "beats": [${flagged.map((b) => b.beat).join(", ")}], "notes": [the notes, same order]}, each at its own price), or cut it as it is (action "cut", free). Nothing more is filmed until they choose.`
|
|
2962
|
+
].filter(Boolean).join("\n")
|
|
2963
|
+
};
|
|
2964
|
+
}
|
|
2965
|
+
if (v.status === "done" && v.final_url) {
|
|
2966
|
+
const redo = v.beats.filter((b) => (b.note || b.missing_lines.length) && (b.needs_redo ?? true));
|
|
2967
|
+
const notes = redo.map((b) => `${o.unit} ${b.beat}: ${b.note ?? `did not say ${b.missing_lines.map((l) => `"${l}"`).join(", ")}`}${b.redo_films ? ` The redo films: ${b.redo_films}` : ""}`);
|
|
2968
|
+
const other = v.beats.filter((b) => b.note && b.needs_redo === false).map((b) => `${o.unit} ${b.beat}: ${b.note}`);
|
|
2969
|
+
return {
|
|
2970
|
+
kind: "assets",
|
|
2971
|
+
text: [
|
|
2972
|
+
`"${v.title}" is finished and saved to the Library: ${v.final_url}`,
|
|
2973
|
+
v.retaken.length ? `Filmed again: ${o.unit} ${v.retaken.join(", ")}.` : "",
|
|
2974
|
+
notes.length ? `Review notes: ${notes.join("; ")}. Offer a redo of those ${o.unit}s (action "redo" with beats and notes).` : "",
|
|
2975
|
+
other.length ? `Also noted, no redo needed: ${other.join("; ")}.` : "",
|
|
2976
|
+
simplify
|
|
2977
|
+
].filter(Boolean).join("\n"),
|
|
2978
|
+
assetUrls: [v.final_url]
|
|
2979
|
+
};
|
|
2980
|
+
}
|
|
2981
|
+
const doing = music ? { scripting: "listening to the song and writing the plan", shots: "filming the sections", cutting: "cutting it to the song" }[v.status] ?? STEP[v.status] ?? v.status : STEP[v.status] ?? v.status;
|
|
2982
|
+
const ready = v.beats.filter((b) => b.url).length;
|
|
2983
|
+
const total = v.beats.length || (music ? v.sections?.length ?? 0 : 5);
|
|
2984
|
+
const again = v.rendering.map((n) => v.beats.find((b) => b.beat === n)).filter((b) => Boolean(b?.url)).map((b) => `${o.unit} ${b.beat} is being filmed again${b.missing_lines.length ? ` (it dropped the line "${b.missing_lines[0]}")` : b.note ? ` (the review asked: ${b.note.split(/(?<=\.) /)[0]})` : ""}`);
|
|
2985
|
+
return {
|
|
2986
|
+
kind: "text",
|
|
2987
|
+
text: `Still ${doing}${v.status === "shots" ? ` (${ready} of ${total} ${o.unit}s ready${v.rendering.length ? `, filming ${o.unit} ${v.rendering.join(", ")}` : ""})` : ""}.${again.length ? ` ${again.join("; ")}.` : ""} It takes about 10 to 15 minutes in all. Call ${next2("status")} in about a minute.`,
|
|
2988
|
+
followUp: { input: { action: "status", [o.idField]: id }, afterSec: v.status === "scripting" ? 20 : 45 }
|
|
2989
|
+
};
|
|
2990
|
+
}
|
|
2991
|
+
async function waitForPlan2(ctx, id) {
|
|
2992
|
+
const sleep = ctx.pollOverrides?.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
2993
|
+
let last = await dramaApi(ctx, "GET", `/${id}`);
|
|
2994
|
+
for (let i = 0; i < 8 && last.data.status === "scripting"; i += 1) {
|
|
2995
|
+
await sleep(3e3);
|
|
2996
|
+
last = await dramaApi(ctx, "GET", `/${id}`);
|
|
2997
|
+
}
|
|
2998
|
+
return last;
|
|
2999
|
+
}
|
|
3000
|
+
var KINDS, LOOKS2, STORY_TYPES, InputSchema13, InputJsonSchema13, errorText, STEP, DRAMA_OPTS, dramaSkill;
|
|
3001
|
+
var init_drama = __esm({
|
|
3002
|
+
"src/skills/drama.ts"() {
|
|
3003
|
+
KINDS = ["micro-drama", "short-film", "trailer"];
|
|
3004
|
+
LOOKS2 = ["live-action", "action", "anime", "3d-animation", "noir", "horror", "sci-fi", "documentary", "western", "romance", "romantic-comedy", "comedy"];
|
|
3005
|
+
STORY_TYPES = ["billionaire", "werewolf", "mafia", "revenge", "secret-identity", "vampire", "second-chance"];
|
|
3006
|
+
InputSchema13 = zod.z.object({
|
|
3007
|
+
action: zod.z.enum(["create", "produce", "status", "redo", "cut"]).default("create"),
|
|
3008
|
+
idea: zod.z.string().min(8).max(1500).optional(),
|
|
3009
|
+
format: zod.z.enum(["9:16", "16:9"]).default("9:16"),
|
|
3010
|
+
mature: zod.z.boolean().optional(),
|
|
3011
|
+
kind: zod.z.enum(KINDS).default("micro-drama"),
|
|
3012
|
+
look: zod.z.enum(LOOKS2).optional(),
|
|
3013
|
+
story_type: zod.z.enum(STORY_TYPES).optional(),
|
|
3014
|
+
title_card: zod.z.boolean().optional(),
|
|
3015
|
+
name_tags: zod.z.boolean().optional(),
|
|
3016
|
+
end_screen: zod.z.boolean().optional(),
|
|
3017
|
+
end_text: zod.z.string().max(60).optional(),
|
|
3018
|
+
brand_color: zod.z.string().regex(/^#[0-9a-fA-F]{6}$/).optional(),
|
|
3019
|
+
logo_url: zod.z.string().url().optional(),
|
|
3020
|
+
cast: zod.z.array(zod.z.object({ name: zod.z.string().min(1).max(40), image_url: zod.z.string().url() })).max(5).optional(),
|
|
3021
|
+
scenes: zod.z.number().int().min(1).max(15).optional(),
|
|
3022
|
+
score: zod.z.boolean().optional(),
|
|
3023
|
+
drama_id: zod.z.string().max(60).optional(),
|
|
3024
|
+
beats: zod.z.array(zod.z.number().int().min(1).max(15)).min(1).max(15).optional(),
|
|
3025
|
+
notes: zod.z.array(zod.z.string().max(300)).max(15).optional()
|
|
3026
|
+
});
|
|
3027
|
+
InputJsonSchema13 = {
|
|
3028
|
+
type: "object",
|
|
3029
|
+
properties: {
|
|
3030
|
+
action: {
|
|
3031
|
+
type: "string",
|
|
3032
|
+
enum: ["create", "produce", "status", "redo", "cut"],
|
|
3033
|
+
description: "create (default, free): writes the episode plan (5 scenes unless scenes says, cast, lines) and returns drama_id and the price. Show the plan and price to the user and wait for their OK. produce (charged): films it. status (free): progress; call every ~60s until it returns the episode. redo (charged): re-film chosen beats of a finished episode or one waiting at the review, then it cuts again. cut (free): when the review flagged beats and the user wants it as it is, cut it now."
|
|
3034
|
+
},
|
|
3035
|
+
idea: { type: "string", description: "Create: the story in a line or two (who, the conflict, the twist)." },
|
|
3036
|
+
format: { type: "string", enum: ["9:16", "16:9"], description: "Create: vertical 9:16 (default) or widescreen 16:9." },
|
|
3037
|
+
mature: { type: "boolean", description: "Create: true for adult work (the server also detects it from the idea). Runs on the less-restricted engines." },
|
|
3038
|
+
kind: {
|
|
3039
|
+
type: "string",
|
|
3040
|
+
enum: [...KINDS],
|
|
3041
|
+
description: "Create: micro-drama (default: episode one with a hook and a cliffhanger), short-film (a complete 60 s story) or trailer (sells a film that does not exist, never shows the ending)."
|
|
3042
|
+
},
|
|
3043
|
+
look: { type: "string", enum: [...LOOKS2], description: "Create (optional): the visual style and genre. Default live-action (a micro-drama story type brings its own)." },
|
|
3044
|
+
story_type: {
|
|
3045
|
+
type: "string",
|
|
3046
|
+
enum: [...STORY_TYPES],
|
|
3047
|
+
description: "Create (optional, micro-drama only): the genre, which sets its promises (a hidden heir, a fated mate, a debt of blood...). Pick the one the idea fits; omit for anything else."
|
|
3048
|
+
},
|
|
3049
|
+
title_card: { type: "boolean", description: "Create: the episode title over the opening hook. Default on." },
|
|
3050
|
+
name_tags: { type: "boolean", description: "Create: a name and role tag the first time each character speaks. Default on." },
|
|
3051
|
+
end_screen: { type: "boolean", description: 'Create: a 3.5-second "To be continued" end screen after the last line. Default on.' },
|
|
3052
|
+
end_text: { type: "string", description: `Create: the end screen's line, up to 60 characters. Default "Episode 2 coming soon".` },
|
|
3053
|
+
brand_color: { type: "string", description: "Create: accent colour for the graphics, #RRGGBB. Default a warm gold." },
|
|
3054
|
+
logo_url: { type: "string", description: "Create: a logo on the PixelDojo CDN for the end screen." },
|
|
3055
|
+
cast: {
|
|
3056
|
+
type: "array",
|
|
3057
|
+
items: {
|
|
3058
|
+
type: "object",
|
|
3059
|
+
properties: {
|
|
3060
|
+
name: { type: "string", description: "The character's name as the story uses it." },
|
|
3061
|
+
image_url: { type: "string", description: "A clear front-facing portrait on the PixelDojo CDN." }
|
|
3062
|
+
},
|
|
3063
|
+
required: ["name", "image_url"]
|
|
3064
|
+
},
|
|
3065
|
+
description: "Create (optional): up to 5 characters to star (people, animals or creatures), each with a portrait on the PixelDojo CDN (a saved character from pixeldojo_library). Others get a portrait made for them."
|
|
3066
|
+
},
|
|
3067
|
+
scenes: { type: "integer", minimum: 1, maximum: 15, description: "Create (optional): how many 12-second scenes, default 5. Pass it when the user names a count or a length (about 12 s a scene)." },
|
|
3068
|
+
score: { type: "boolean", description: "Create (optional): one score laid over the finished edit, on by default and included in the price. Pass false only when the user wants no music (dialogue and ambience only)." },
|
|
3069
|
+
drama_id: { type: "string", description: "From create. Required for produce, status and redo." },
|
|
3070
|
+
beats: { type: "array", items: { type: "integer", minimum: 1, maximum: 15 }, description: "Redo: the beat numbers to film again." },
|
|
3071
|
+
notes: { type: "array", items: { type: "string" }, description: "Redo (optional): what to fix in each of those beats, in the same order." }
|
|
3072
|
+
}
|
|
3073
|
+
};
|
|
3074
|
+
errorText = (d, status2) => (typeof d.error === "string" ? d.error : d.error?.message) ?? `HTTP ${status2}`;
|
|
3075
|
+
STEP = {
|
|
3076
|
+
scripting: "writing the episode",
|
|
3077
|
+
keyframes: "making the cast portraits and the location",
|
|
3078
|
+
shots: "filming the beats",
|
|
3079
|
+
cutting: "cutting the episode"
|
|
3080
|
+
};
|
|
3081
|
+
DRAMA_OPTS = { tool: "pixeldojo_drama", idField: "drama_id", unit: "beat" };
|
|
3082
|
+
dramaSkill = {
|
|
3083
|
+
name: "pixeldojo_drama",
|
|
3084
|
+
description: "Make a 60-second micro-drama episode (hook, twist, cliffhanger; optionally a classic genre such as billionaire, werewolf or mafia), short film (a complete story) or movie trailer from an idea, vertical 9:16 or 16:9, in any of Film Studio's looks (live-action, anime, 3D animation, noir, horror, sci-fi, western, romance, comedy and more). Five 12-second scenes, each filmed in one pass with several cuts, the cast's faces kept from portraits and every line spoken and lip-synced, cut together with their own sound and one score over the finished edit, with free motion graphics (title card, name tags, end screen; each can be turned off), and saved to the Library. Not for music videos (pixeldojo_music_video) create is free and returns the plan and the exact price (about 380 credits for a regular episode, about 160 for mature work); produce films it; then call status until it is finished (10-15 minutes).",
|
|
3085
|
+
inputZod: InputSchema13,
|
|
3086
|
+
inputJsonSchema: InputJsonSchema13,
|
|
3087
|
+
allowedModels: ["seedance-2-5", "seedream-4"],
|
|
3088
|
+
defaultModel: "seedance-2-5",
|
|
3089
|
+
async run(input, ctx) {
|
|
3090
|
+
if (input.action === "create") {
|
|
3091
|
+
if (!input.idea?.trim()) return { kind: "text", text: "Provide an `idea` (a line or two) to plan an episode." };
|
|
3092
|
+
const created = await dramaApi(ctx, "POST", "", {
|
|
3093
|
+
idea: input.idea.trim(),
|
|
3094
|
+
format: input.format,
|
|
3095
|
+
kind: input.kind,
|
|
3096
|
+
...input.mature ? { mature: true } : {},
|
|
3097
|
+
...input.look ? { look: input.look } : {},
|
|
3098
|
+
...input.story_type ? { story_type: input.story_type } : {},
|
|
3099
|
+
...input.scenes ? { scenes: input.scenes } : {},
|
|
3100
|
+
...input.score === false ? { score: false } : {},
|
|
3101
|
+
...Object.fromEntries(
|
|
3102
|
+
["title_card", "name_tags", "end_screen", "end_text", "brand_color", "logo_url"].filter((k) => input[k] !== void 0).map((k) => [k, input[k]])
|
|
3103
|
+
),
|
|
3104
|
+
...input.cast?.length ? { cast: input.cast } : {},
|
|
3105
|
+
// Stop before the cut when the review flags a scene: the user decides, not the agent.
|
|
3106
|
+
review: true
|
|
3107
|
+
});
|
|
3108
|
+
if (!created.data.id) return { kind: "text", text: `Could not start the episode: ${errorText(created.data, created.status)}` };
|
|
3109
|
+
const planned = await waitForPlan2(ctx, created.data.id);
|
|
3110
|
+
return describeDrama(planned.data);
|
|
3111
|
+
}
|
|
3112
|
+
if (!input.drama_id) return { kind: "text", text: `${input.action} needs drama_id (from create).` };
|
|
3113
|
+
const res = input.action === "status" ? await dramaApi(ctx, "GET", `/${input.drama_id}`) : input.action === "produce" || input.action === "cut" ? await dramaApi(ctx, "POST", `/${input.drama_id}`, { action: input.action }) : input.beats?.length ? await dramaApi(ctx, "POST", `/${input.drama_id}`, { action: "redo", beats: input.beats, ...input.notes?.length ? { notes: input.notes } : {} }) : null;
|
|
3114
|
+
if (!res) return { kind: "text", text: "redo needs beats (the beat numbers to film again)." };
|
|
3115
|
+
if (!res.data.id) return { kind: "text", text: errorText(res.data, res.status) };
|
|
3116
|
+
return describeDrama(res.data);
|
|
3117
|
+
}
|
|
3118
|
+
};
|
|
3119
|
+
}
|
|
3120
|
+
});
|
|
3121
|
+
async function api(ctx, method, path, body) {
|
|
3122
|
+
const res = await (ctx.fetch ?? fetch)(`${ctx.baseUrl}/api/v1/music-video${path}`, {
|
|
3123
|
+
method,
|
|
3124
|
+
headers: {
|
|
3125
|
+
Authorization: `Bearer ${ctx.apiKey}`,
|
|
3126
|
+
"Content-Type": "application/json",
|
|
3127
|
+
"x-pixeldojo-source": `mcp/${ctx.mcpVersion}`,
|
|
3128
|
+
"x-pixeldojo-skill": "music_video"
|
|
3129
|
+
},
|
|
3130
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
3131
|
+
});
|
|
3132
|
+
return { status: res.status, data: await res.json().catch(() => ({})) };
|
|
3133
|
+
}
|
|
3134
|
+
var MUSIC_LOOKS, InputSchema14, InputJsonSchema14, errorText2, describe, musicVideoSkill;
|
|
3135
|
+
var init_music_video = __esm({
|
|
3136
|
+
"src/skills/music-video.ts"() {
|
|
3137
|
+
init_drama();
|
|
3138
|
+
MUSIC_LOOKS = [
|
|
3139
|
+
"live-action",
|
|
3140
|
+
"action",
|
|
3141
|
+
"anime",
|
|
3142
|
+
"3d-animation",
|
|
3143
|
+
"noir",
|
|
3144
|
+
"horror",
|
|
3145
|
+
"sci-fi",
|
|
3146
|
+
"documentary",
|
|
3147
|
+
"western",
|
|
3148
|
+
"romance",
|
|
3149
|
+
"romantic-comedy",
|
|
3150
|
+
"comedy",
|
|
3151
|
+
"rap-video",
|
|
3152
|
+
"neon-club",
|
|
3153
|
+
"stage-performance",
|
|
3154
|
+
"retro-camcorder",
|
|
3155
|
+
"y2k-pop",
|
|
3156
|
+
"moody-rnb",
|
|
3157
|
+
"rock-grit",
|
|
3158
|
+
"dream-pop",
|
|
3159
|
+
"k-pop"
|
|
3160
|
+
];
|
|
3161
|
+
InputSchema14 = zod.z.object({
|
|
3162
|
+
action: zod.z.enum(["create", "produce", "status", "redo"]).default("create"),
|
|
3163
|
+
song_url: zod.z.string().url().optional(),
|
|
3164
|
+
idea: zod.z.string().min(8).max(1500).optional(),
|
|
3165
|
+
format: zod.z.enum(["9:16", "16:9"]).default("9:16"),
|
|
3166
|
+
mature: zod.z.boolean().optional(),
|
|
3167
|
+
look: zod.z.enum(MUSIC_LOOKS).optional(),
|
|
3168
|
+
start_sec: zod.z.number().min(0).optional(),
|
|
3169
|
+
length_sec: zod.z.number().int().min(10).max(60).optional(),
|
|
3170
|
+
artist: zod.z.string().max(60).optional(),
|
|
3171
|
+
song_title: zod.z.string().max(80).optional(),
|
|
3172
|
+
title_card: zod.z.boolean().optional(),
|
|
3173
|
+
end_screen: zod.z.boolean().optional(),
|
|
3174
|
+
end_text: zod.z.string().max(60).optional(),
|
|
3175
|
+
cast: zod.z.array(zod.z.object({ name: zod.z.string().min(1).max(40), image_url: zod.z.string().url() })).max(3).optional(),
|
|
3176
|
+
music_video_id: zod.z.string().max(60).optional(),
|
|
3177
|
+
beats: zod.z.array(zod.z.number().int().min(1).max(12)).min(1).max(12).optional(),
|
|
3178
|
+
notes: zod.z.array(zod.z.string().max(300)).max(12).optional()
|
|
3179
|
+
});
|
|
3180
|
+
InputJsonSchema14 = {
|
|
3181
|
+
type: "object",
|
|
3182
|
+
properties: {
|
|
3183
|
+
action: {
|
|
3184
|
+
type: "string",
|
|
3185
|
+
enum: ["create", "produce", "status", "redo"],
|
|
3186
|
+
description: "create (default, free): hears the song, splits it into sections and writes the plan; returns music_video_id and the price. Show both and wait for the user's OK. produce (charged): films it. status (free): progress; call every ~60s until it returns the video. redo (charged): re-film chosen sections, then it cuts again."
|
|
3187
|
+
},
|
|
3188
|
+
song_url: { type: "string", description: "Create: the song on the PixelDojo CDN (attach or upload it first)." },
|
|
3189
|
+
idea: { type: "string", description: "Create: the story or performance concept in a line or two." },
|
|
3190
|
+
format: { type: "string", enum: ["9:16", "16:9"], description: "Create: vertical 9:16 (default) or widescreen 16:9." },
|
|
3191
|
+
mature: { type: "boolean", description: "Create: true for adult work (the server also detects it from the idea). Runs on the less-restricted engines." },
|
|
3192
|
+
look: { type: "string", enum: [...MUSIC_LOOKS], description: "Create (optional): the visual style, e.g. neon-club, rap-video, stage-performance, retro-camcorder, k-pop." },
|
|
3193
|
+
start_sec: { type: "number", description: "Create (optional): where in the song to start, in seconds. Default 0." },
|
|
3194
|
+
length_sec: { type: "integer", minimum: 10, maximum: 60, description: "Create (optional): seconds of the song to film, 10 to 60. Default 60 or the rest of the song." },
|
|
3195
|
+
artist: { type: "string", description: "Create (optional): the artist name for the title card." },
|
|
3196
|
+
song_title: { type: "string", description: "Create (optional): the song title for the title card." },
|
|
3197
|
+
title_card: { type: "boolean", description: "Create: the song title over the opening. Default on." },
|
|
3198
|
+
end_screen: { type: "boolean", description: "Create: an end screen after the song. Default off." },
|
|
3199
|
+
end_text: { type: "string", description: "Create: the end screen's line, up to 60 characters." },
|
|
3200
|
+
cast: {
|
|
3201
|
+
type: "array",
|
|
3202
|
+
items: {
|
|
3203
|
+
type: "object",
|
|
3204
|
+
properties: {
|
|
3205
|
+
name: { type: "string", description: "The character's name; the first one is the performer." },
|
|
3206
|
+
image_url: { type: "string", description: "A clear front-facing portrait on the PixelDojo CDN." }
|
|
3207
|
+
},
|
|
3208
|
+
required: ["name", "image_url"]
|
|
3209
|
+
},
|
|
3210
|
+
description: "Create (optional): up to 3 characters, the performer first, each with a portrait on the PixelDojo CDN."
|
|
3211
|
+
},
|
|
3212
|
+
music_video_id: { type: "string", description: "From create. Required for produce, status and redo." },
|
|
3213
|
+
beats: { type: "array", items: { type: "integer", minimum: 1 }, description: "Redo: the section numbers to film again." },
|
|
3214
|
+
notes: { type: "array", items: { type: "string" }, description: "Redo (optional): what to fix in each of those sections, in the same order." }
|
|
3215
|
+
}
|
|
3216
|
+
};
|
|
3217
|
+
errorText2 = (d, status2) => (typeof d.error === "string" ? d.error : d.error?.message) ?? `HTTP ${status2}`;
|
|
3218
|
+
describe = (v) => describeDrama(v, { tool: "pixeldojo_music_video", idField: "music_video_id", unit: "section" });
|
|
3219
|
+
musicVideoSkill = {
|
|
3220
|
+
name: "pixeldojo_music_video",
|
|
3221
|
+
description: "Make a music video for the user's song (up to 60 seconds of it), vertical 9:16 or 16:9, in any Film Studio look (neon club, rap video, stage performance, retro camcorder, k-pop, anime and more). The song is split into sections at lyric gaps and each section is filmed in one pass to its own slice of the song, so the performer's lips follow every word, with the cast's faces kept from portraits; then it is cut under the original song with a title card and saved to the Library. create is free and returns the plan and price (about 1.25 credits per second of song plus portraits); produce films it; then call status until it is finished (10-15 minutes).",
|
|
3222
|
+
inputZod: InputSchema14,
|
|
3223
|
+
inputJsonSchema: InputJsonSchema14,
|
|
3224
|
+
allowedModels: ["p-video-2-pro-reference", "seedream-4"],
|
|
3225
|
+
defaultModel: "p-video-2-pro-reference",
|
|
3226
|
+
async run(input, ctx) {
|
|
3227
|
+
if (input.action === "create") {
|
|
3228
|
+
if (!input.song_url || !input.idea?.trim()) return { kind: "text", text: "Provide `song_url` (the song on the PixelDojo CDN) and an `idea` to plan a music video." };
|
|
3229
|
+
const { action: _a, music_video_id: _m, beats: _b, notes: _n, ...rest } = input;
|
|
3230
|
+
const created = await api(ctx, "POST", "", { ...Object.fromEntries(Object.entries(rest).filter(([, v]) => v !== void 0)), idea: input.idea.trim() });
|
|
3231
|
+
if (!created.data.id) return { kind: "text", text: `Could not start the music video: ${errorText2(created.data, created.status)}` };
|
|
3232
|
+
const sleep = ctx.pollOverrides?.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
3233
|
+
let last = await api(ctx, "GET", `/${created.data.id}`);
|
|
3234
|
+
for (let i = 0; i < 8 && last.data.status === "scripting"; i += 1) {
|
|
3235
|
+
await sleep(3e3);
|
|
3236
|
+
last = await api(ctx, "GET", `/${created.data.id}`);
|
|
3237
|
+
}
|
|
3238
|
+
return describe(last.data);
|
|
3239
|
+
}
|
|
3240
|
+
if (!input.music_video_id) return { kind: "text", text: `${input.action} needs music_video_id (from create).` };
|
|
3241
|
+
const id = input.music_video_id;
|
|
3242
|
+
const res = input.action === "status" ? await api(ctx, "GET", `/${id}`) : input.action === "produce" ? await api(ctx, "POST", `/${id}`, { action: "produce" }) : input.beats?.length ? await api(ctx, "POST", `/${id}`, { action: "redo", beats: input.beats, ...input.notes?.length ? { notes: input.notes } : {} }) : null;
|
|
3243
|
+
if (!res) return { kind: "text", text: "redo needs beats (the section numbers to film again)." };
|
|
3244
|
+
if (!res.data.id) return { kind: "text", text: errorText2(res.data, res.status) };
|
|
3245
|
+
return describe(res.data);
|
|
3246
|
+
}
|
|
3247
|
+
};
|
|
3248
|
+
}
|
|
3249
|
+
});
|
|
3250
|
+
async function kataApi(ctx, method, path, body) {
|
|
3251
|
+
const res = await (ctx.fetch ?? fetch)(`${ctx.baseUrl}/api/v1/kata${path}`, {
|
|
3252
|
+
method,
|
|
3253
|
+
headers: {
|
|
3254
|
+
Authorization: `Bearer ${ctx.apiKey}`,
|
|
3255
|
+
"Content-Type": "application/json",
|
|
3256
|
+
"x-pixeldojo-source": `mcp/${ctx.mcpVersion}`,
|
|
3257
|
+
"x-pixeldojo-skill": "kata"
|
|
3258
|
+
},
|
|
3259
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
3260
|
+
});
|
|
3261
|
+
const data = await res.json().catch(() => ({}));
|
|
3262
|
+
return { status: res.status, data };
|
|
3263
|
+
}
|
|
3264
|
+
function describeKata(v) {
|
|
3265
|
+
const next2 = (action) => `pixeldojo_kata with {"kata_id": "${v.id}", "action": "${action}"}`;
|
|
3266
|
+
if (v.status === "failed") return { kind: "text", text: `The Kata stopped: ${v.error ?? "unknown error"}. Anything that failed was refunded.` };
|
|
3267
|
+
if (v.status === "quote") {
|
|
3268
|
+
const scenes = v.scenes.map((s, i) => `${i + 1}. ${KIND[s.kind] ?? s.kind}${s.cast.length ? ` with ${s.cast.join(" and ")}` : ""}${s.prompt ? `: ${s.prompt.slice(0, 140)}` : ""}`);
|
|
3269
|
+
const words = v.shots.filter((s) => s.text).map((s) => `${s.from.toFixed(1)}s "${s.text}"`);
|
|
3270
|
+
return {
|
|
3271
|
+
kind: "text",
|
|
3272
|
+
text: [
|
|
3273
|
+
`Planned "${v.title}" (kata_id: ${v.id}): ${v.duration_sec}s, ${v.format}, ${v.shots.length} cuts kept to the frame, ${v.scenes.length} setups:`,
|
|
3274
|
+
...scenes,
|
|
3275
|
+
words.length ? `On screen: ${words.join(", ")}.` : "",
|
|
3276
|
+
`Price: about ${v.estimate_credits} credits (${(v.quote ?? []).map((l) => `${l.label} ${l.credits}`).join(", ")}); up to ${v.max_credits_with_retakes} if bad takes are made again. Nothing has been charged yet. Show this to the user and, once they agree, call ${next2("produce")}. (In Sensei: put it in pixeldojo_plan as ONE step with skill "kata" and params {"kata_id": "${v.id}"}, which prices it from this quote.)`
|
|
3277
|
+
].filter(Boolean).join("\n")
|
|
3278
|
+
};
|
|
3279
|
+
}
|
|
3280
|
+
if (v.status === "done" && v.final_url) {
|
|
3281
|
+
return { kind: "assets", text: `"${v.title}" is finished and saved to the Library: ${v.final_url}`, assetUrls: [v.final_url] };
|
|
3282
|
+
}
|
|
3283
|
+
return {
|
|
3284
|
+
kind: "text",
|
|
3285
|
+
text: `Still ${STEP2[v.status] ?? v.status}. It takes about 5 to 15 minutes in all. Call ${next2("status")} in about a minute.`,
|
|
3286
|
+
followUp: { input: { action: "status", kata_id: v.id }, afterSec: v.status === "analyzing" || v.status === "scripting" ? 20 : 45 }
|
|
3287
|
+
};
|
|
3288
|
+
}
|
|
3289
|
+
async function waitForPlan3(ctx, id) {
|
|
3290
|
+
const sleep = ctx.pollOverrides?.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
3291
|
+
let last = await kataApi(ctx, "GET", `/${id}`);
|
|
3292
|
+
for (let i = 0; i < 10 && (last.data.status === "analyzing" || last.data.status === "scripting"); i += 1) {
|
|
3293
|
+
await sleep(4e3);
|
|
3294
|
+
last = await kataApi(ctx, "GET", `/${id}`);
|
|
3295
|
+
}
|
|
3296
|
+
return last;
|
|
3297
|
+
}
|
|
3298
|
+
var InputSchema15, InputJsonSchema15, errorText3, STEP2, KIND, kataSkill;
|
|
3299
|
+
var init_kata = __esm({
|
|
3300
|
+
"src/skills/kata.ts"() {
|
|
3301
|
+
InputSchema15 = zod.z.object({
|
|
3302
|
+
action: zod.z.enum(["create", "produce", "status", "presets"]).default("create"),
|
|
3303
|
+
reference_url: zod.z.string().url().optional(),
|
|
3304
|
+
preset: zod.z.string().max(80).optional(),
|
|
3305
|
+
cast: zod.z.array(zod.z.object({ name: zod.z.string().min(1).max(40), image_url: zod.z.string().url().optional(), description: zod.z.string().max(200).optional() })).max(3).optional(),
|
|
3306
|
+
photos: zod.z.array(zod.z.object({ url: zod.z.string().url(), note: zod.z.string().max(80).optional() })).max(4).optional(),
|
|
3307
|
+
instructions: zod.z.string().max(1500).optional(),
|
|
3308
|
+
look: zod.z.string().max(80).optional(),
|
|
3309
|
+
kata_id: zod.z.string().max(60).optional()
|
|
3310
|
+
});
|
|
3311
|
+
InputJsonSchema15 = {
|
|
3312
|
+
type: "object",
|
|
3313
|
+
properties: {
|
|
3314
|
+
action: {
|
|
3315
|
+
type: "string",
|
|
3316
|
+
enum: ["create", "produce", "status", "presets"],
|
|
3317
|
+
description: "create (default, free): reads the reference edit and plans the remake; returns kata_id and the price. Show the plan and price and wait for the user's OK. produce (charged): makes it. status (free): progress; call every ~60s until it returns the edit. presets (free): the Kata library, ready-made edits to recreate."
|
|
3318
|
+
},
|
|
3319
|
+
reference_url: { type: "string", description: "Create: the 4 to 60 second edit to recreate: an upload on the PixelDojo CDN or a direct link to the video file. TikTok, Instagram, YouTube and X posts cannot be fetched: ask the user to save the video from the app and upload it. Or give preset instead." },
|
|
3320
|
+
preset: { type: "string", description: 'Create: a Kata library preset slug (from action "presets") instead of a reference.' },
|
|
3321
|
+
cast: {
|
|
3322
|
+
type: "array",
|
|
3323
|
+
items: {
|
|
3324
|
+
type: "object",
|
|
3325
|
+
properties: {
|
|
3326
|
+
name: { type: "string", description: "Their name. Without image_url, the saved Character Studio character of that name." },
|
|
3327
|
+
image_url: { type: "string", description: "A clear photo of them on the PixelDojo CDN." },
|
|
3328
|
+
description: { type: "string", description: "Optional: who they are, in a few words." }
|
|
3329
|
+
},
|
|
3330
|
+
required: ["name"]
|
|
3331
|
+
},
|
|
3332
|
+
description: 'Create: up to 3 people who replace the people in the edit. Use saved characters by name (pixeldojo_library source "characters").'
|
|
3333
|
+
},
|
|
3334
|
+
photos: {
|
|
3335
|
+
type: "array",
|
|
3336
|
+
items: { type: "object", properties: { url: { type: "string" }, note: { type: "string", description: 'What it is (e.g. "the product").' } }, required: ["url"] },
|
|
3337
|
+
description: "Create: up to 4 products or photos to put in the edit, on the PixelDojo CDN."
|
|
3338
|
+
},
|
|
3339
|
+
instructions: { type: "string", description: "Create (optional): what to change, e.g. the setting, the words on screen, where the product goes." },
|
|
3340
|
+
look: { type: "string", description: 'Create (optional): style transfer, one look for every new shot, e.g. "hand-drawn anime".' },
|
|
3341
|
+
kata_id: { type: "string", description: "From create. Required for produce and status." }
|
|
3342
|
+
}
|
|
3343
|
+
};
|
|
3344
|
+
errorText3 = (d, status2) => d.message ?? (typeof d.error === "string" ? d.error : d.error?.message) ?? `HTTP ${status2}`;
|
|
3345
|
+
STEP2 = {
|
|
3346
|
+
analyzing: "reading the edit cut by cut",
|
|
3347
|
+
scripting: "planning the remake",
|
|
3348
|
+
music: "scoring a new track on the same tempo",
|
|
3349
|
+
beats: "finding the beat",
|
|
3350
|
+
keyframes: "making the keyframes",
|
|
3351
|
+
animating: "filming the shots",
|
|
3352
|
+
cutting: "cutting it where the reference cuts",
|
|
3353
|
+
rendering: "setting the words and transitions",
|
|
3354
|
+
reviewing: "checking the final cut"
|
|
3355
|
+
};
|
|
3356
|
+
KIND = { clip: "new shot", still: "still", swap: "motion transfer", photo: "your photo" };
|
|
3357
|
+
kataSkill = {
|
|
3358
|
+
name: "pixeldojo_kata",
|
|
3359
|
+
description: `Kata: recreate a reference edit (a viral short, an ad, a montage, a trailer, 4 to 60 s) with the user's own characters, products and photos. It keeps the same pacing and cut points to the frame, the transitions, the camera moves, the caption style and the music feel (an original score on the same tempo, never the reference's audio); dance moves come across by motion transfer, and a look can restyle every shot. Start from a reference_url or a Kata library preset (action "presets"). create is free and returns the plan and the price; produce makes it; then call status until it is finished. Saved to the Library.`,
|
|
3360
|
+
inputZod: InputSchema15,
|
|
3361
|
+
inputJsonSchema: InputJsonSchema15,
|
|
3362
|
+
allowedModels: ["seedance-2-5", "google-nano-banana", "kling-motion-control"],
|
|
3363
|
+
defaultModel: "seedance-2-5",
|
|
3364
|
+
async run(input, ctx) {
|
|
3365
|
+
if (input.action === "presets") {
|
|
3366
|
+
const res2 = await kataApi(ctx, "GET", "/presets");
|
|
3367
|
+
const list = res2.data.presets ?? [];
|
|
3368
|
+
if (!list.length) return { kind: "text", text: "The Kata library is empty right now. Recreate any edit with a reference_url." };
|
|
3369
|
+
return {
|
|
3370
|
+
kind: "text",
|
|
3371
|
+
text: ["Kata library (pass the slug as preset):", ...list.map((p) => `- ${p.slug}: ${p.title} (${p.category}, ${Math.round(p.durationSec)}s, ${p.shots} cuts). ${p.blurb}`)].join("\n")
|
|
3372
|
+
};
|
|
3373
|
+
}
|
|
3374
|
+
if (input.action === "create") {
|
|
3375
|
+
if (!input.reference_url && !input.preset) return { kind: "text", text: 'Give a reference_url (the edit to recreate, on the PixelDojo CDN) or a preset from action "presets".' };
|
|
3376
|
+
const created = await kataApi(ctx, "POST", "", {
|
|
3377
|
+
...input.preset ? { preset: input.preset } : { reference_url: input.reference_url },
|
|
3378
|
+
...input.cast?.length ? { cast: input.cast } : {},
|
|
3379
|
+
...input.photos?.length ? { photos: input.photos } : {},
|
|
3380
|
+
...input.instructions ? { instructions: input.instructions } : {},
|
|
3381
|
+
...input.look ? { look: input.look } : {}
|
|
3382
|
+
});
|
|
3383
|
+
if (!created.data.id) return { kind: "text", text: `Could not start the Kata: ${errorText3(created.data, created.status)}` };
|
|
3384
|
+
const planned = await waitForPlan3(ctx, created.data.id);
|
|
3385
|
+
return describeKata(planned.data);
|
|
3386
|
+
}
|
|
3387
|
+
if (!input.kata_id) return { kind: "text", text: `${input.action} needs kata_id (from create).` };
|
|
3388
|
+
const res = input.action === "status" ? await kataApi(ctx, "GET", `/${input.kata_id}`) : await kataApi(ctx, "POST", `/${input.kata_id}`, { action: "produce" });
|
|
3389
|
+
if (!res.data.id) return { kind: "text", text: errorText3(res.data, res.status) };
|
|
3390
|
+
return describeKata(res.data);
|
|
3391
|
+
}
|
|
3392
|
+
};
|
|
3393
|
+
}
|
|
3394
|
+
});
|
|
2914
3395
|
async function run13(input, ctx) {
|
|
2915
3396
|
const productImages = [
|
|
2916
3397
|
...input.product_image_urls ?? [],
|
|
@@ -2935,7 +3416,9 @@ async function run13(input, ctx) {
|
|
|
2935
3416
|
quality: input.quality,
|
|
2936
3417
|
resolution: input.resolution,
|
|
2937
3418
|
aspect_ratio: input.aspect_ratio,
|
|
2938
|
-
duration: input.duration
|
|
3419
|
+
duration: input.duration,
|
|
3420
|
+
reference_video_url: input.reference_video_url,
|
|
3421
|
+
brand_kit: input.brand_kit
|
|
2939
3422
|
};
|
|
2940
3423
|
const client = ctx.client;
|
|
2941
3424
|
const submitted = await client.runAd(body);
|
|
@@ -2951,16 +3434,16 @@ async function run13(input, ctx) {
|
|
|
2951
3434
|
const styleLabel = submitted.ad?.style ?? input.style ?? "ugc-testimonial";
|
|
2952
3435
|
return pollResultToSkillResult(
|
|
2953
3436
|
result,
|
|
2954
|
-
(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]}
|
|
3437
|
+
(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]} Add the brand end card (logo, tagline, button) with pixeldojo_marketing action end_card and this video_url.`
|
|
2955
3438
|
);
|
|
2956
3439
|
}
|
|
2957
|
-
var
|
|
3440
|
+
var InputSchema16, InputJsonSchema16, adSkill;
|
|
2958
3441
|
var init_ad = __esm({
|
|
2959
3442
|
"src/skills/ad.ts"() {
|
|
2960
3443
|
init_types();
|
|
2961
3444
|
init_poll();
|
|
2962
3445
|
init_upload_helpers();
|
|
2963
|
-
|
|
3446
|
+
InputSchema16 = zod.z.object({
|
|
2964
3447
|
style: zod.z.string().max(80).optional().describe(
|
|
2965
3448
|
"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."
|
|
2966
3449
|
),
|
|
@@ -2978,7 +3461,9 @@ var init_ad = __esm({
|
|
|
2978
3461
|
quality: zod.z.enum(["mini", "fast", "high"]).optional(),
|
|
2979
3462
|
resolution: zod.z.enum(["480p", "720p", "1080p", "4k"]).optional().describe('1080p/4k require quality "high".'),
|
|
2980
3463
|
aspect_ratio: zod.z.enum(["16:9", "4:3", "1:1", "3:4", "9:16", "21:9"]).optional(),
|
|
2981
|
-
duration: zod.z.number().int().min(4).max(15).optional()
|
|
3464
|
+
duration: zod.z.number().int().min(4).max(15).optional(),
|
|
3465
|
+
reference_video_url: zod.z.string().url().optional(),
|
|
3466
|
+
brand_kit: zod.z.boolean().optional()
|
|
2982
3467
|
}).refine(
|
|
2983
3468
|
(v) => v.product_url || (v.product_image_urls?.length || v.product_image_paths?.length) && v.product_name,
|
|
2984
3469
|
{
|
|
@@ -2986,7 +3471,7 @@ var init_ad = __esm({
|
|
|
2986
3471
|
path: ["product_url"]
|
|
2987
3472
|
}
|
|
2988
3473
|
);
|
|
2989
|
-
|
|
3474
|
+
InputJsonSchema16 = {
|
|
2990
3475
|
type: "object",
|
|
2991
3476
|
properties: {
|
|
2992
3477
|
style: {
|
|
@@ -3064,6 +3549,15 @@ var init_ad = __esm({
|
|
|
3064
3549
|
minimum: 4,
|
|
3065
3550
|
maximum: 15,
|
|
3066
3551
|
description: "Ad length in seconds. Defaults to the style's length; clamped to what the engine allows."
|
|
3552
|
+
},
|
|
3553
|
+
reference_video_url: {
|
|
3554
|
+
type: "string",
|
|
3555
|
+
format: "uri",
|
|
3556
|
+
description: "Ad multiplier: an ad that already works (https mp4, up to 30 s). The new ad copies its hook, pacing and camera moves with this product and presenter, never its product, people or text."
|
|
3557
|
+
},
|
|
3558
|
+
brand_kit: {
|
|
3559
|
+
type: "boolean",
|
|
3560
|
+
description: "The account's brand kit (palette, voice, selling points; see pixeldojo_brand_kit) rides on every ad. false skips it."
|
|
3067
3561
|
}
|
|
3068
3562
|
}
|
|
3069
3563
|
// "product_url OR product_name+images" is enforced by the server and the
|
|
@@ -3073,8 +3567,8 @@ var init_ad = __esm({
|
|
|
3073
3567
|
name: "pixeldojo_ad",
|
|
3074
3568
|
description: "Marketing Studio: turn a product into a rendered video ad with voiceover. Requires `product_url` (name, description and images are extracted), or `product_name` plus product_image_urls / product_image_paths. Pick a `style` slug (default ugc-testimonial; full catalog at GET https://pixeldojo.ai/api/v1/marketing/presets), and optionally add presenter reference images, a location, a hook, and the exact spoken script. Returns the video URL, or a jobId to check with pixeldojo_status.",
|
|
3075
3569
|
hostedDescription: "Marketing Studio: turn a product into a rendered video ad with voiceover. Requires `product_url` (name, description and images are extracted), or `product_name` plus product_image_urls (public http(s) URLs). Pick a `style` slug (default ugc-testimonial; full catalog at GET https://pixeldojo.ai/api/v1/marketing/presets), and optionally add presenter reference images, a location, a hook, and the exact spoken script. Returns the video URL, or a jobId to check with pixeldojo_status.",
|
|
3076
|
-
inputZod:
|
|
3077
|
-
inputJsonSchema:
|
|
3570
|
+
inputZod: InputSchema16,
|
|
3571
|
+
inputJsonSchema: InputJsonSchema16,
|
|
3078
3572
|
// Composition skill — the server routes to the ad engine internally.
|
|
3079
3573
|
allowedModels: [],
|
|
3080
3574
|
defaultModel: "",
|
|
@@ -3109,10 +3603,10 @@ async function run14(input, ctx) {
|
|
|
3109
3603
|
statusUrl: result.statusUrl ?? ctx.buildStatusUrl(result.campaignId)
|
|
3110
3604
|
};
|
|
3111
3605
|
}
|
|
3112
|
-
var
|
|
3606
|
+
var InputSchema17, InputJsonSchema17, campaignSkill;
|
|
3113
3607
|
var init_campaign = __esm({
|
|
3114
3608
|
"src/skills/campaign.ts"() {
|
|
3115
|
-
|
|
3609
|
+
InputSchema17 = zod.z.object({
|
|
3116
3610
|
productUrl: zod.z.string().url().optional().describe("Product page URL. We extract name + description + images automatically."),
|
|
3117
3611
|
product: zod.z.object({
|
|
3118
3612
|
name: zod.z.string().min(1).max(200),
|
|
@@ -3128,7 +3622,7 @@ var init_campaign = __esm({
|
|
|
3128
3622
|
message: "productUrl or product is required",
|
|
3129
3623
|
path: ["productUrl"]
|
|
3130
3624
|
});
|
|
3131
|
-
|
|
3625
|
+
InputJsonSchema17 = {
|
|
3132
3626
|
type: "object",
|
|
3133
3627
|
properties: {
|
|
3134
3628
|
productUrl: {
|
|
@@ -3179,8 +3673,8 @@ var init_campaign = __esm({
|
|
|
3179
3673
|
campaignSkill = {
|
|
3180
3674
|
name: "pixeldojo_campaign",
|
|
3181
3675
|
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.",
|
|
3182
|
-
inputZod:
|
|
3183
|
-
inputJsonSchema:
|
|
3676
|
+
inputZod: InputSchema17,
|
|
3677
|
+
inputJsonSchema: InputJsonSchema17,
|
|
3184
3678
|
// Composition skill — internal sub-jobs route to per-tool models, no
|
|
3185
3679
|
// single allowed-models list applies here.
|
|
3186
3680
|
allowedModels: [],
|
|
@@ -3219,13 +3713,13 @@ async function run15(input, ctx) {
|
|
|
3219
3713
|
statusUrl: result.statusUrl ?? ctx.buildStatusUrl(result.campaignId)
|
|
3220
3714
|
};
|
|
3221
3715
|
}
|
|
3222
|
-
var
|
|
3716
|
+
var InputSchema18, InputJsonSchema18, campaignStatusSkill;
|
|
3223
3717
|
var init_campaign_status = __esm({
|
|
3224
3718
|
"src/skills/campaign-status.ts"() {
|
|
3225
|
-
|
|
3719
|
+
InputSchema18 = zod.z.object({
|
|
3226
3720
|
campaignId: zod.z.string().min(1).max(200).describe("Campaign id returned from a prior pixeldojo_campaign handoff.")
|
|
3227
3721
|
});
|
|
3228
|
-
|
|
3722
|
+
InputJsonSchema18 = {
|
|
3229
3723
|
type: "object",
|
|
3230
3724
|
properties: {
|
|
3231
3725
|
campaignId: {
|
|
@@ -3238,8 +3732,8 @@ var init_campaign_status = __esm({
|
|
|
3238
3732
|
campaignStatusSkill = {
|
|
3239
3733
|
name: "pixeldojo_campaign_status",
|
|
3240
3734
|
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.",
|
|
3241
|
-
inputZod:
|
|
3242
|
-
inputJsonSchema:
|
|
3735
|
+
inputZod: InputSchema18,
|
|
3736
|
+
inputJsonSchema: InputJsonSchema18,
|
|
3243
3737
|
allowedModels: [],
|
|
3244
3738
|
defaultModel: "",
|
|
3245
3739
|
run: run15
|
|
@@ -3262,16 +3756,16 @@ async function run16(input, ctx) {
|
|
|
3262
3756
|
(temp storage; auto-deletes in ~24h. Pass this URL as image_url, video_url for a clip, or song_url on pixeldojo_film for a song, to any pixeldojo tool that takes one.)`
|
|
3263
3757
|
};
|
|
3264
3758
|
}
|
|
3265
|
-
var
|
|
3759
|
+
var InputSchema19, InputJsonSchema19, uploadSkill;
|
|
3266
3760
|
var init_upload = __esm({
|
|
3267
3761
|
"src/skills/upload.ts"() {
|
|
3268
3762
|
init_upload_helpers();
|
|
3269
|
-
|
|
3763
|
+
InputSchema19 = zod.z.object({
|
|
3270
3764
|
path: zod.z.string().min(1).max(4096).describe(
|
|
3271
3765
|
"Absolute local file path to upload. JPEG, PNG, WebP, GIF (\u226450MB), MP4, WebM, MOV (\u2264200MB) or MP3, WAV, M4A, AAC, OGG, FLAC audio (\u226450MB). Returns a public URL on the temp CDN; storage auto-expires after 24 hours."
|
|
3272
3766
|
)
|
|
3273
3767
|
});
|
|
3274
|
-
|
|
3768
|
+
InputJsonSchema19 = {
|
|
3275
3769
|
type: "object",
|
|
3276
3770
|
properties: {
|
|
3277
3771
|
path: {
|
|
@@ -3284,8 +3778,8 @@ var init_upload = __esm({
|
|
|
3284
3778
|
uploadSkill = {
|
|
3285
3779
|
name: "pixeldojo_upload",
|
|
3286
3780
|
description: "Upload a local image, video or audio file (JPEG, PNG, WebP, GIF up to 50MB; MP4, WebM, MOV up to 200MB; MP3, WAV, M4A, AAC, OGG, FLAC up to 50MB) and get back a public URL that expires after 24 hours. Free. Pass an audio URL as song_url to pixeldojo_film for a music video. Pass the URL as image_url (stills) or video_url (clips) to tools that take hosted URLs only, such as pixeldojo_character, pixeldojo_storyboard and pixeldojo_upscale. pixeldojo_generate, pixeldojo_edit and pixeldojo_ad take local paths directly and need no separate upload.",
|
|
3287
|
-
inputZod:
|
|
3288
|
-
inputJsonSchema:
|
|
3781
|
+
inputZod: InputSchema19,
|
|
3782
|
+
inputJsonSchema: InputJsonSchema19,
|
|
3289
3783
|
allowedModels: [],
|
|
3290
3784
|
defaultModel: "",
|
|
3291
3785
|
run: run16
|
|
@@ -3330,14 +3824,14 @@ async function run17(input, ctx) {
|
|
|
3330
3824
|
(urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Audio ready. Asset: ${urls[0]}`
|
|
3331
3825
|
);
|
|
3332
3826
|
}
|
|
3333
|
-
var ALLOWED4, DEFAULT_MODEL6,
|
|
3827
|
+
var ALLOWED4, DEFAULT_MODEL6, InputSchema20, InputJsonSchema20, audioSkill;
|
|
3334
3828
|
var init_audio = __esm({
|
|
3335
3829
|
"src/skills/audio.ts"() {
|
|
3336
3830
|
init_poll();
|
|
3337
3831
|
init_types();
|
|
3338
3832
|
ALLOWED4 = ["seed-audio", "text-to-speech"];
|
|
3339
3833
|
DEFAULT_MODEL6 = "seed-audio";
|
|
3340
|
-
|
|
3834
|
+
InputSchema20 = zod.z.object({
|
|
3341
3835
|
prompt: zod.z.string().min(1).describe(
|
|
3342
3836
|
"seed-audio: a script to speak, or a scene description (dialogue, sound effects, music), up to 2048 characters; refer to audio_urls clips in order as @Audio1..@Audio3. text-to-speech: the exact text to speak, up to 10,000 characters."
|
|
3343
3837
|
),
|
|
@@ -3347,7 +3841,7 @@ var init_audio = __esm({
|
|
|
3347
3841
|
output_format: zod.z.string().optional().describe("Output format. seed-audio: mp3 (default), wav, pcm, ogg_opus. text-to-speech: mp3 (default), wav, flac, pcm."),
|
|
3348
3842
|
speed: zod.z.number().min(0.5).max(2).optional().describe("Speech speed multiplier, 0.5-2 (default 1). Both models.")
|
|
3349
3843
|
});
|
|
3350
|
-
|
|
3844
|
+
InputJsonSchema20 = {
|
|
3351
3845
|
type: "object",
|
|
3352
3846
|
properties: {
|
|
3353
3847
|
prompt: {
|
|
@@ -3385,8 +3879,8 @@ var init_audio = __esm({
|
|
|
3385
3879
|
audioSkill = {
|
|
3386
3880
|
name: "pixeldojo_audio",
|
|
3387
3881
|
description: "Generate audio: speech, multi-character dialogue, sound effects, and background music from one prompt (Seed Audio 1.0 by default; plain text-to-speech available). Voice cloning takes hosted reference clip URLs in audio_urls; local audio files are not accepted. Returns an audio URL, or a jobId for pixeldojo_status.",
|
|
3388
|
-
inputZod:
|
|
3389
|
-
inputJsonSchema:
|
|
3882
|
+
inputZod: InputSchema20,
|
|
3883
|
+
inputJsonSchema: InputJsonSchema20,
|
|
3390
3884
|
allowedModels: [...ALLOWED4],
|
|
3391
3885
|
defaultModel: DEFAULT_MODEL6,
|
|
3392
3886
|
run: run17
|
|
@@ -3444,17 +3938,17 @@ ${lines.join("\n")}
|
|
|
3444
3938
|
Pass any URL back into pixeldojo_generate, pixeldojo_edit, pixeldojo_character, or pixeldojo_upscale as a reference input.`
|
|
3445
3939
|
};
|
|
3446
3940
|
}
|
|
3447
|
-
var
|
|
3941
|
+
var InputSchema21, InputJsonSchema21, librarySkill;
|
|
3448
3942
|
var init_library = __esm({
|
|
3449
3943
|
"src/skills/library.ts"() {
|
|
3450
|
-
|
|
3944
|
+
InputSchema21 = zod.z.object({
|
|
3451
3945
|
source: zod.z.enum(["media", "characters"]).optional().describe("media (default): saved and recent results. characters: saved Character Studio characters and elements, usable as @Name in prompts."),
|
|
3452
3946
|
q: zod.z.string().optional().describe("Substring match against the prompt that produced the asset."),
|
|
3453
3947
|
model: zod.z.string().optional().describe("Restrict to a single model (apiId)."),
|
|
3454
3948
|
modality: zod.z.enum(["image", "video", "audio"]).optional().describe("Restrict to one output type."),
|
|
3455
3949
|
limit: zod.z.number().int().min(1).max(50).optional().describe("Max results (default 10).")
|
|
3456
3950
|
});
|
|
3457
|
-
|
|
3951
|
+
InputJsonSchema21 = {
|
|
3458
3952
|
type: "object",
|
|
3459
3953
|
properties: {
|
|
3460
3954
|
source: {
|
|
@@ -3479,8 +3973,8 @@ var init_library = __esm({
|
|
|
3479
3973
|
librarySkill = {
|
|
3480
3974
|
name: "pixeldojo_library",
|
|
3481
3975
|
description: "Search your Library, newest first. source=media (default): saved media (permanent) and recent API/MCP generations (deleted 1 hour after they finish), as URLs to reuse as references. source=characters: saved Character Studio characters and elements, each usable as @Name in any prompt.",
|
|
3482
|
-
inputZod:
|
|
3483
|
-
inputJsonSchema:
|
|
3976
|
+
inputZod: InputSchema21,
|
|
3977
|
+
inputJsonSchema: InputJsonSchema21,
|
|
3484
3978
|
allowedModels: [],
|
|
3485
3979
|
defaultModel: "",
|
|
3486
3980
|
run: run18
|
|
@@ -3501,17 +3995,17 @@ async function run19(input, ctx) {
|
|
|
3501
3995
|
It will appear at https://pixeldojo.ai/my-images and in pixeldojo_library searches.`
|
|
3502
3996
|
};
|
|
3503
3997
|
}
|
|
3504
|
-
var
|
|
3998
|
+
var InputSchema22, InputJsonSchema22, saveSkill;
|
|
3505
3999
|
var init_save = __esm({
|
|
3506
4000
|
"src/skills/save.ts"() {
|
|
3507
|
-
|
|
4001
|
+
InputSchema22 = zod.z.object({
|
|
3508
4002
|
url: zod.z.string().url().describe("The asset URL to save (image or video)."),
|
|
3509
4003
|
prompt: zod.z.string().optional().describe("The prompt that produced it \u2014 makes it findable in the library later."),
|
|
3510
4004
|
tool_name: zod.z.string().optional().describe("Tool/model slug that produced it."),
|
|
3511
4005
|
modality: zod.z.enum(["image", "video", "audio"]).optional(),
|
|
3512
4006
|
job_id: zod.z.string().optional().describe("The jobId that produced this asset, when known.")
|
|
3513
4007
|
});
|
|
3514
|
-
|
|
4008
|
+
InputJsonSchema22 = {
|
|
3515
4009
|
type: "object",
|
|
3516
4010
|
properties: {
|
|
3517
4011
|
url: { type: "string", format: "uri", description: "The result URL to keep (image or video from a PixelDojo job)." },
|
|
@@ -3525,8 +4019,8 @@ var init_save = __esm({
|
|
|
3525
4019
|
saveSkill = {
|
|
3526
4020
|
name: "pixeldojo_save",
|
|
3527
4021
|
description: `Save a generated image or video into the user's My Media library permanently ("save this", "keep it"). Results otherwise expire 1 hour after they finish. Pass the result URL; it is safety-checked before it is kept.`,
|
|
3528
|
-
inputZod:
|
|
3529
|
-
inputJsonSchema:
|
|
4022
|
+
inputZod: InputSchema22,
|
|
4023
|
+
inputJsonSchema: InputJsonSchema22,
|
|
3530
4024
|
allowedModels: [],
|
|
3531
4025
|
defaultModel: "",
|
|
3532
4026
|
run: run19
|
|
@@ -3545,21 +4039,21 @@ async function run20(input, ctx) {
|
|
|
3545
4039
|
text: `Saved ${res.kind} "${res.name}" to Character Studio (${res.image_urls.length} image${res.image_urls.length === 1 ? "" : "s"}). Use it in any prompt as ${res.mention}, for example "${res.mention} walking on a beach at sunset". It also appears at https://pixeldojo.ai/characters.`
|
|
3546
4040
|
};
|
|
3547
4041
|
}
|
|
3548
|
-
var
|
|
4042
|
+
var KINDS2, InputSchema23, InputJsonSchema23, saveCharacterSkill;
|
|
3549
4043
|
var init_save_character = __esm({
|
|
3550
4044
|
"src/skills/save-character.ts"() {
|
|
3551
|
-
|
|
3552
|
-
|
|
4045
|
+
KINDS2 = ["character", "product", "outfit", "location", "object", "style"];
|
|
4046
|
+
InputSchema23 = zod.z.object({
|
|
3553
4047
|
name: zod.z.string().min(1).max(60).describe("What to call it. Used as @Name in prompts."),
|
|
3554
|
-
kind: zod.z.enum(
|
|
4048
|
+
kind: zod.z.enum(KINDS2).optional().describe("Default character."),
|
|
3555
4049
|
image_urls: zod.z.array(zod.z.string().url()).max(8).optional().describe("3 to 5 photos from different angles works best for a character."),
|
|
3556
4050
|
description: zod.z.string().max(1500).optional().describe("Appearance notes, or the scene text for an element.")
|
|
3557
4051
|
});
|
|
3558
|
-
|
|
4052
|
+
InputJsonSchema23 = {
|
|
3559
4053
|
type: "object",
|
|
3560
4054
|
properties: {
|
|
3561
4055
|
name: { type: "string", description: "What to call it. Used as @Name in prompts afterwards." },
|
|
3562
|
-
kind: { type: "string", enum:
|
|
4056
|
+
kind: { type: "string", enum: KINDS2, description: "character (default), product, outfit, location, object (a prop) or style." },
|
|
3563
4057
|
image_urls: {
|
|
3564
4058
|
type: "array",
|
|
3565
4059
|
items: { type: "string", format: "uri" },
|
|
@@ -3573,14 +4067,596 @@ var init_save_character = __esm({
|
|
|
3573
4067
|
saveCharacterSkill = {
|
|
3574
4068
|
name: "pixeldojo_save_character",
|
|
3575
4069
|
description: "Save a character (a person, from 3-5 photos) or an element (product, outfit, location, prop, style) to the user's Character Studio library. Afterwards write @Name in any prompt to use it. List saved ones with pixeldojo_library source=characters.",
|
|
3576
|
-
inputZod:
|
|
3577
|
-
inputJsonSchema:
|
|
4070
|
+
inputZod: InputSchema23,
|
|
4071
|
+
inputJsonSchema: InputJsonSchema23,
|
|
3578
4072
|
allowedModels: [],
|
|
3579
4073
|
defaultModel: "",
|
|
3580
4074
|
run: run20
|
|
3581
4075
|
};
|
|
3582
4076
|
}
|
|
3583
4077
|
});
|
|
4078
|
+
function priceOf(e) {
|
|
4079
|
+
if (!e.creditsByEngine) return `${e.credits ?? "?"} credits`;
|
|
4080
|
+
return Object.entries(e.creditsByEngine).map(([engine, c]) => `${engine}${engine === e.defaultEngine ? " (default)" : ""} ${c ?? "?"} credits`).join(" / ");
|
|
4081
|
+
}
|
|
4082
|
+
async function listRun(input, ctx) {
|
|
4083
|
+
const q = new URLSearchParams();
|
|
4084
|
+
for (const [k, v] of Object.entries(input)) if (v !== void 0 && v !== "") q.set(k, String(v));
|
|
4085
|
+
if (!input.limit) q.set("limit", "20");
|
|
4086
|
+
const res = await (ctx.fetch ?? fetch)(`${ctx.baseUrl}/api/v1/effects?${q}`, { headers: headers(ctx, "pixeldojo_effects") });
|
|
4087
|
+
const data = await res.json().catch(() => ({}));
|
|
4088
|
+
if (!res.ok) return { kind: "text", text: `Could not list effects: ${data.error ?? `HTTP ${res.status}`}` };
|
|
4089
|
+
const effects = data.effects ?? [];
|
|
4090
|
+
const cats = Object.entries(data.categories ?? {}).map(([fam, list]) => `${fam}: ${list.join(", ")}`);
|
|
4091
|
+
if (!effects.length) {
|
|
4092
|
+
return { kind: "text", text: ["No effects match. Categories:", ...cats].join("\n") };
|
|
4093
|
+
}
|
|
4094
|
+
const lines = effects.map((e) => {
|
|
4095
|
+
const ins = e.inputs.map(
|
|
4096
|
+
(i) => i.kind === "image" ? `photo "${i.label}"${i.optional ? " (optional)" : ""}` : `text "${i.label}"${i.options ? ` [${i.options.join(" | ")}]` : ""}${i.optional ? " (optional)" : ""}`
|
|
4097
|
+
);
|
|
4098
|
+
return `- ${e.slug} (${e.family}, ${e.output}, ${priceOf(e)}): ${e.title}. ${e.tagline} Inputs: ${ins.join(", ")}.`;
|
|
4099
|
+
});
|
|
4100
|
+
const shown = data.total && data.total > effects.length ? `, showing ${effects.length}` : "";
|
|
4101
|
+
return {
|
|
4102
|
+
kind: "text",
|
|
4103
|
+
text: [
|
|
4104
|
+
`${data.total ?? effects.length} effects${shown}. Run one with pixeldojo_effect (slug, image_urls in the order listed, texts for text inputs).`,
|
|
4105
|
+
...lines,
|
|
4106
|
+
...input.search || input.family || input.category ? [] : ["", "Categories:", ...cats]
|
|
4107
|
+
].join("\n")
|
|
4108
|
+
};
|
|
4109
|
+
}
|
|
4110
|
+
async function effectRun(input, ctx) {
|
|
4111
|
+
const images = input.image_paths?.length ? await uploadLocalFiles(input.image_paths, ctx) : input.image_urls ?? [];
|
|
4112
|
+
const res = await (ctx.fetch ?? fetch)(`${ctx.baseUrl}/api/v1/effects/${encodeURIComponent(input.slug)}/run`, {
|
|
4113
|
+
method: "POST",
|
|
4114
|
+
headers: headers(ctx, "pixeldojo_effect"),
|
|
4115
|
+
body: JSON.stringify({
|
|
4116
|
+
images,
|
|
4117
|
+
...input.texts ? { texts: input.texts } : {},
|
|
4118
|
+
...input.family ? { family: input.family } : {},
|
|
4119
|
+
...input.engine ? { engine: input.engine } : {}
|
|
4120
|
+
})
|
|
4121
|
+
});
|
|
4122
|
+
const data = await res.json().catch(() => ({}));
|
|
4123
|
+
if (!res.ok || !data.jobId) {
|
|
4124
|
+
return { kind: "text", text: `Effect not started (nothing charged): ${data.error ?? `HTTP ${res.status}`}` };
|
|
4125
|
+
}
|
|
4126
|
+
const result = await pollJobUntilDoneOrHandoff(data.jobId, (id) => ctx.client.getJob(id), ctx.buildStatusUrl, {
|
|
4127
|
+
now: ctx.pollOverrides?.now,
|
|
4128
|
+
sleep: ctx.pollOverrides?.sleep
|
|
4129
|
+
});
|
|
4130
|
+
return pollResultToSkillResult(
|
|
4131
|
+
result,
|
|
4132
|
+
(urls) => urls.length === 0 ? `Job ${data.jobId} completed but returned no assets.` : `Done (${data.creditCost ?? "?"} credits). Asset: ${urls[0]}`
|
|
4133
|
+
);
|
|
4134
|
+
}
|
|
4135
|
+
var FAMILIES, ENGINES2, headers, ListSchema, effectsListSkill, RunSchema, runProperties, effectRunSkill;
|
|
4136
|
+
var init_effects = __esm({
|
|
4137
|
+
"src/skills/effects.ts"() {
|
|
4138
|
+
init_poll();
|
|
4139
|
+
init_upload_helpers();
|
|
4140
|
+
init_types();
|
|
4141
|
+
FAMILIES = ["video-effect", "photo-effect", "tool"];
|
|
4142
|
+
ENGINES2 = ["klein", "qwen"];
|
|
4143
|
+
headers = (ctx, skill) => ({
|
|
4144
|
+
Authorization: `Bearer ${ctx.apiKey}`,
|
|
4145
|
+
"Content-Type": "application/json",
|
|
4146
|
+
"x-pixeldojo-source": `mcp/${ctx.mcpVersion}`,
|
|
4147
|
+
"x-pixeldojo-skill": skill
|
|
4148
|
+
});
|
|
4149
|
+
ListSchema = zod.z.object({
|
|
4150
|
+
search: zod.z.string().max(200).optional(),
|
|
4151
|
+
family: zod.z.enum(FAMILIES).optional(),
|
|
4152
|
+
category: zod.z.string().max(60).optional(),
|
|
4153
|
+
limit: zod.z.number().int().min(1).max(100).optional()
|
|
4154
|
+
});
|
|
4155
|
+
effectsListSkill = {
|
|
4156
|
+
name: "pixeldojo_effects",
|
|
4157
|
+
description: "List PixelDojo's one-click effects: 283 video effects (a photo or two in, a 5 s clip out: AI Hug, Earth Zoom In, AI Dance), 30 photo effects (a photo in, a styled image out: action figure, Ghibli style, sticker, aging filter) and one-job tools (remove a watermark, object or background, headshots, photo restoration, relight, logo, thumbnail and poster makers). Search by words, family or category; each result gives the slug, inputs and credit price. Free. Run one with pixeldojo_effect.",
|
|
4158
|
+
inputZod: ListSchema,
|
|
4159
|
+
inputJsonSchema: {
|
|
4160
|
+
type: "object",
|
|
4161
|
+
properties: {
|
|
4162
|
+
search: { type: "string", description: 'Words like "hug", "action figure", "ghibli", "remove background".' },
|
|
4163
|
+
family: {
|
|
4164
|
+
type: "string",
|
|
4165
|
+
enum: FAMILIES,
|
|
4166
|
+
description: "video-effect = photo to clip, photo-effect = photo to styled image, tool = one-job edit."
|
|
4167
|
+
},
|
|
4168
|
+
category: { type: "string", description: 'A category name from a previous listing, e.g. "Viral", "Toys & 3D".' },
|
|
4169
|
+
limit: { type: "integer", minimum: 1, maximum: 100, description: "Default 20." }
|
|
4170
|
+
}
|
|
4171
|
+
},
|
|
4172
|
+
allowedModels: [],
|
|
4173
|
+
defaultModel: "",
|
|
4174
|
+
run: listRun
|
|
4175
|
+
};
|
|
4176
|
+
RunSchema = zod.z.object({
|
|
4177
|
+
slug: zod.z.string().min(1).max(80),
|
|
4178
|
+
family: zod.z.enum(FAMILIES).optional(),
|
|
4179
|
+
image_urls: zod.z.array(zod.z.string().url()).max(4).optional(),
|
|
4180
|
+
image_paths: zod.z.array(zod.z.string()).max(4).optional(),
|
|
4181
|
+
texts: zod.z.array(zod.z.string().max(200)).max(4).optional(),
|
|
4182
|
+
engine: zod.z.enum(ENGINES2).optional()
|
|
4183
|
+
});
|
|
4184
|
+
runProperties = {
|
|
4185
|
+
slug: { type: "string", description: 'The effect slug from pixeldojo_effects, e.g. "ai-hug", "action-figure", "watermark-remover".' },
|
|
4186
|
+
family: {
|
|
4187
|
+
type: "string",
|
|
4188
|
+
enum: FAMILIES,
|
|
4189
|
+
description: "Needed only for action-figure, comic-book and red-carpet, which are both a video and a photo effect."
|
|
4190
|
+
},
|
|
4191
|
+
image_urls: {
|
|
4192
|
+
type: "array",
|
|
4193
|
+
items: { type: "string", format: "uri" },
|
|
4194
|
+
description: "The photos as https URLs, in the order the listing names them (most take one; duo effects take two)."
|
|
4195
|
+
},
|
|
4196
|
+
image_paths: {
|
|
4197
|
+
type: "array",
|
|
4198
|
+
items: { type: "string" },
|
|
4199
|
+
description: "Absolute local photo paths instead of image_urls; uploaded first (24h temp storage)."
|
|
4200
|
+
},
|
|
4201
|
+
texts: {
|
|
4202
|
+
type: "array",
|
|
4203
|
+
items: { type: "string" },
|
|
4204
|
+
description: "Text inputs in listing order, for entries that list them (object to remove, new background, brand name)."
|
|
4205
|
+
},
|
|
4206
|
+
engine: { type: "string", enum: ENGINES2, description: "Photo effects only: leave out for the default engine." }
|
|
4207
|
+
};
|
|
4208
|
+
effectRunSkill = {
|
|
4209
|
+
name: "pixeldojo_effect",
|
|
4210
|
+
description: "Run one of PixelDojo's one-click effects on your photos: the prompt is built in, so pass only the slug from pixeldojo_effects and the photos (image_urls, or image_paths for local files), plus texts for tools that ask for them. Video effects return a 5 s clip (usually handed off; poll with pixeldojo_status), photo effects and tools an image. Costs the credits the listing shows.",
|
|
4211
|
+
hostedDescription: "Run one of PixelDojo's one-click effects on your photos: the prompt is built in, so pass only the slug from pixeldojo_effects and the photos as https image_urls, plus texts for tools that ask for them. Video effects return a 5 s clip (usually handed off; poll with pixeldojo_status), photo effects and tools an image. Costs the credits the listing shows.",
|
|
4212
|
+
inputZod: RunSchema,
|
|
4213
|
+
inputJsonSchema: { type: "object", required: ["slug"], properties: runProperties },
|
|
4214
|
+
allowedModels: [],
|
|
4215
|
+
defaultModel: "",
|
|
4216
|
+
run: effectRun
|
|
4217
|
+
};
|
|
4218
|
+
}
|
|
4219
|
+
});
|
|
4220
|
+
async function call2(ctx, skill, path, init = {}) {
|
|
4221
|
+
const res = await (ctx.fetch ?? fetch)(`${ctx.baseUrl}${path}`, {
|
|
4222
|
+
method: init.method ?? "GET",
|
|
4223
|
+
headers: headers2(ctx, skill),
|
|
4224
|
+
...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {}
|
|
4225
|
+
});
|
|
4226
|
+
const data = await res.json().catch(() => ({}));
|
|
4227
|
+
const err = data.error;
|
|
4228
|
+
return { ok: res.ok, data, error: typeof err === "string" ? err : err?.message ?? `HTTP ${res.status}` };
|
|
4229
|
+
}
|
|
4230
|
+
async function kitRun(input, ctx) {
|
|
4231
|
+
const path = "/api/v1/marketing/brand-kit";
|
|
4232
|
+
if (input.action === "import") {
|
|
4233
|
+
if (!input.url) return { kind: "text", text: "Pass url: the brand's website, like yourbrand.com." };
|
|
4234
|
+
const r2 = await call2(ctx, "pixeldojo_brand_kit", path, { method: "POST", body: { url: input.url, save: input.save } });
|
|
4235
|
+
if (!r2.ok) return { kind: "text", text: `Brand kit not imported: ${r2.error}` };
|
|
4236
|
+
return { kind: "text", text: `${r2.data.saved === false ? "Imported (not saved)." : "Imported and saved."}
|
|
4237
|
+
${describeKit(r2.data.brand_kit)}` };
|
|
4238
|
+
}
|
|
4239
|
+
if (input.action === "update") {
|
|
4240
|
+
const patch = {
|
|
4241
|
+
...input.name !== void 0 ? { name: input.name } : {},
|
|
4242
|
+
...input.tagline !== void 0 ? { tagline: input.tagline } : {},
|
|
4243
|
+
...input.voice !== void 0 ? { voice: input.voice } : {},
|
|
4244
|
+
...input.colors ? { colors: input.colors.map((c) => c.trim().toLowerCase()) } : {},
|
|
4245
|
+
...input.fonts ? { fonts: input.fonts } : {},
|
|
4246
|
+
...input.selling_points ? { sellingPoints: input.selling_points } : {},
|
|
4247
|
+
...input.logo_url ? { logoUrl: input.logo_url } : {}
|
|
4248
|
+
};
|
|
4249
|
+
const r2 = await call2(ctx, "pixeldojo_brand_kit", path, { method: "PATCH", body: patch });
|
|
4250
|
+
if (!r2.ok) return { kind: "text", text: `Brand kit not updated: ${r2.error}` };
|
|
4251
|
+
return { kind: "text", text: `Updated.
|
|
4252
|
+
${describeKit(r2.data.brand_kit)}` };
|
|
4253
|
+
}
|
|
4254
|
+
const r = await call2(ctx, "pixeldojo_brand_kit", path);
|
|
4255
|
+
if (!r.ok) return { kind: "text", text: `Could not read the brand kit: ${r.error}` };
|
|
4256
|
+
return { kind: "text", text: describeKit(r.data.brand_kit ?? null) };
|
|
4257
|
+
}
|
|
4258
|
+
async function templatesRun(input, ctx) {
|
|
4259
|
+
const q = new URLSearchParams();
|
|
4260
|
+
if (input.kind) q.set("kind", input.kind);
|
|
4261
|
+
if (input.category) q.set("category", input.category);
|
|
4262
|
+
const r = await call2(ctx, "pixeldojo_marketing", `/api/v1/marketing/templates?${q}`);
|
|
4263
|
+
if (!r.ok) return { kind: "text", text: `Could not list templates: ${r.error}` };
|
|
4264
|
+
const rows = r.data.templates ?? [];
|
|
4265
|
+
const lines = rows.map((t) => {
|
|
4266
|
+
const how = t.kind === "image" ? "action image" : t.kind === "motion" ? `action motion, fields ${t.fields?.join(", ")}, ${t.credits} credits` : "pixeldojo_ad style";
|
|
4267
|
+
return `- ${t.id} (${t.label}, ${t.category}): ${t.name}. ${how}. Example: ${t.preview ?? "none"}`;
|
|
4268
|
+
});
|
|
4269
|
+
const engines = r.data.image_engines ?? [];
|
|
4270
|
+
return {
|
|
4271
|
+
kind: "text",
|
|
4272
|
+
text: [
|
|
4273
|
+
`${rows.length} templates. Image templates run with action image (template = id), motion templates with action motion, video ad styles with pixeldojo_ad (style = id).`,
|
|
4274
|
+
...lines,
|
|
4275
|
+
"",
|
|
4276
|
+
`Image engines: ${engines.map((e) => `${e.id} ${e.credits_per_image} credits per image (${e.note})`).join("; ")}.`,
|
|
4277
|
+
...r.data.notes ?? []
|
|
4278
|
+
].join("\n")
|
|
4279
|
+
};
|
|
4280
|
+
}
|
|
4281
|
+
async function imageRun(input, ctx) {
|
|
4282
|
+
const local = input.product_image_paths?.length ? await uploadLocalFiles(input.product_image_paths, ctx) : [];
|
|
4283
|
+
const images = [...input.product_image_urls ?? [], ...local];
|
|
4284
|
+
if (!input.product_url && !(input.product_name && images.length)) {
|
|
4285
|
+
return { kind: "text", text: "Pass product_url, or product_name plus product_image_urls (or product_image_paths)." };
|
|
4286
|
+
}
|
|
4287
|
+
const r = await call2(ctx, "pixeldojo_marketing", "/api/v1/marketing/image", {
|
|
4288
|
+
method: "POST",
|
|
4289
|
+
body: {
|
|
4290
|
+
template: input.template,
|
|
4291
|
+
product_url: input.product_url,
|
|
4292
|
+
...!input.product_url ? { product: { name: input.product_name, description: input.product_description, images } } : {},
|
|
4293
|
+
character_images: input.character_image_urls,
|
|
4294
|
+
headline: input.headline,
|
|
4295
|
+
points: input.points,
|
|
4296
|
+
cta: input.cta,
|
|
4297
|
+
direction: input.direction,
|
|
4298
|
+
aspect_ratio: input.aspect_ratio,
|
|
4299
|
+
engine: input.engine,
|
|
4300
|
+
count: input.count,
|
|
4301
|
+
reference_ad_url: input.reference_ad_url,
|
|
4302
|
+
brand_kit: input.brand_kit
|
|
4303
|
+
}
|
|
4304
|
+
});
|
|
4305
|
+
if (!r.ok) return { kind: "text", text: `Images not started: ${r.error}` };
|
|
4306
|
+
const jobs = r.data.jobs ?? [];
|
|
4307
|
+
const polled = await Promise.all(
|
|
4308
|
+
jobs.map(
|
|
4309
|
+
(j) => pollJobUntilDoneOrHandoff(j.jobId, (id) => ctx.client.getJob(id), ctx.buildStatusUrl, {
|
|
4310
|
+
now: ctx.pollOverrides?.now,
|
|
4311
|
+
sleep: ctx.pollOverrides?.sleep
|
|
4312
|
+
})
|
|
4313
|
+
)
|
|
4314
|
+
);
|
|
4315
|
+
const urls = polled.flatMap((p) => p.kind === "completed" ? extractAssetUrls(p.job) : []);
|
|
4316
|
+
const pending = polled.filter((p) => p.kind === "handoff").map((p) => p.jobId);
|
|
4317
|
+
const failed = polled.filter((p) => p.kind === "failed").length;
|
|
4318
|
+
const credits = jobs.reduce((n, j) => n + (j.creditCost ?? 0), 0);
|
|
4319
|
+
const head = `${r.data.template} on ${r.data.engine}: ${urls.length} ready${pending.length ? `, ${pending.length} still rendering` : ""}${failed ? `, ${failed} failed (refunded)` : ""}${credits ? `, ${credits} credits` : ""}.`;
|
|
4320
|
+
if (pending.length === 1 && !urls.length) {
|
|
4321
|
+
return { kind: "handoff", text: `${head} Check it with pixeldojo_status.`, jobId: pending[0], statusUrl: ctx.buildStatusUrl(pending[0]) };
|
|
4322
|
+
}
|
|
4323
|
+
const tail = pending.length ? ` Still rendering: ${pending.join(", ")} (check each with pixeldojo_status).` : "";
|
|
4324
|
+
if (urls.length) return { kind: "assets", text: `${head}${tail}`, assetUrls: urls };
|
|
4325
|
+
return { kind: "text", text: `${head}${tail}` };
|
|
4326
|
+
}
|
|
4327
|
+
async function settleMotion(view, ctx) {
|
|
4328
|
+
const now = ctx.pollOverrides?.now ?? Date.now;
|
|
4329
|
+
const sleep = ctx.pollOverrides?.sleep ?? ((ms) => new Promise((res) => setTimeout(res, ms)));
|
|
4330
|
+
const until = now() + 7e4;
|
|
4331
|
+
let v = view;
|
|
4332
|
+
while (v.status === "processing" && now() < until) {
|
|
4333
|
+
await sleep(5e3);
|
|
4334
|
+
const r = await call2(ctx, "pixeldojo_marketing", `/api/v1/marketing/motion/${encodeURIComponent(v.id)}`);
|
|
4335
|
+
if (r.ok) v = r.data;
|
|
4336
|
+
}
|
|
4337
|
+
if (v.status === "completed" && v.video_url) {
|
|
4338
|
+
return { kind: "assets", text: `${v.template === "end-card" ? "End card added" : "Motion graphic rendered"} (${v.credits ?? "?"} credits). Video: ${v.video_url}`, assetUrls: [v.video_url] };
|
|
4339
|
+
}
|
|
4340
|
+
if (v.status === "failed") return { kind: "text", text: `Render failed, nothing charged: ${v.error}` };
|
|
4341
|
+
return {
|
|
4342
|
+
kind: "text",
|
|
4343
|
+
text: `Still rendering (${v.step}). Check it with pixeldojo_marketing action status motion_id ${v.id} in about 15 seconds.`,
|
|
4344
|
+
followUp: { input: { action: "status", motion_id: v.id }, afterSec: 15 }
|
|
4345
|
+
};
|
|
4346
|
+
}
|
|
4347
|
+
async function motionRun(input, ctx) {
|
|
4348
|
+
if (input.action === "status") {
|
|
4349
|
+
if (!input.motion_id) return { kind: "text", text: "Pass motion_id." };
|
|
4350
|
+
const r2 = await call2(ctx, "pixeldojo_marketing", `/api/v1/marketing/motion/${encodeURIComponent(input.motion_id)}`);
|
|
4351
|
+
if (!r2.ok) return { kind: "text", text: `Could not read that render: ${r2.error}` };
|
|
4352
|
+
return settleMotion(r2.data, ctx);
|
|
4353
|
+
}
|
|
4354
|
+
const endCard = input.action === "end_card";
|
|
4355
|
+
const product = input.product_image_urls?.[0] ?? (input.product_image_paths?.length ? (await uploadLocalFiles(input.product_image_paths.slice(0, 1), ctx))[0] : void 0);
|
|
4356
|
+
const r = await call2(ctx, "pixeldojo_marketing", "/api/v1/marketing/motion", {
|
|
4357
|
+
method: "POST",
|
|
4358
|
+
body: {
|
|
4359
|
+
template: endCard ? "end-card" : input.template,
|
|
4360
|
+
aspect_ratio: input.aspect_ratio === "4:5" ? "9:16" : input.aspect_ratio,
|
|
4361
|
+
headline: input.headline,
|
|
4362
|
+
points: input.points,
|
|
4363
|
+
cta: input.cta,
|
|
4364
|
+
stat: input.stat,
|
|
4365
|
+
stat_label: input.stat_label,
|
|
4366
|
+
price_was: input.price_was,
|
|
4367
|
+
price_now: input.price_now,
|
|
4368
|
+
product_image_url: product,
|
|
4369
|
+
video_url: input.video_url,
|
|
4370
|
+
brand_name: input.brand_name,
|
|
4371
|
+
colors: input.colors?.map((c) => c.trim().toLowerCase()),
|
|
4372
|
+
brand_kit: input.brand_kit
|
|
4373
|
+
}
|
|
4374
|
+
});
|
|
4375
|
+
if (!r.ok) return { kind: "text", text: `Not started: ${r.error}` };
|
|
4376
|
+
return settleMotion(r.data, ctx);
|
|
4377
|
+
}
|
|
4378
|
+
async function marketingRun(input, ctx) {
|
|
4379
|
+
if (input.action === "templates") return templatesRun(input, ctx);
|
|
4380
|
+
if (input.action === "image") return imageRun(input, ctx);
|
|
4381
|
+
return motionRun(input, ctx);
|
|
4382
|
+
}
|
|
4383
|
+
var headers2, KitSchema, describeKit, brandKitSkill, ACTIONS, MOTION, MarketingSchema, props, marketingSkill;
|
|
4384
|
+
var init_marketing = __esm({
|
|
4385
|
+
"src/skills/marketing.ts"() {
|
|
4386
|
+
init_poll();
|
|
4387
|
+
init_upload_helpers();
|
|
4388
|
+
init_types();
|
|
4389
|
+
headers2 = (ctx, skill) => ({
|
|
4390
|
+
Authorization: `Bearer ${ctx.apiKey}`,
|
|
4391
|
+
"Content-Type": "application/json",
|
|
4392
|
+
"x-pixeldojo-source": `mcp/${ctx.mcpVersion}`,
|
|
4393
|
+
"x-pixeldojo-skill": skill
|
|
4394
|
+
});
|
|
4395
|
+
KitSchema = zod.z.object({
|
|
4396
|
+
action: zod.z.enum(["get", "import", "update"]).optional(),
|
|
4397
|
+
url: zod.z.string().max(2048).optional(),
|
|
4398
|
+
save: zod.z.boolean().optional(),
|
|
4399
|
+
name: zod.z.string().max(60).optional(),
|
|
4400
|
+
tagline: zod.z.string().max(80).optional(),
|
|
4401
|
+
voice: zod.z.string().max(240).optional(),
|
|
4402
|
+
colors: zod.z.array(zod.z.string()).max(5).optional(),
|
|
4403
|
+
fonts: zod.z.array(zod.z.string()).max(2).optional(),
|
|
4404
|
+
selling_points: zod.z.array(zod.z.string()).max(5).optional(),
|
|
4405
|
+
logo_url: zod.z.string().url().optional()
|
|
4406
|
+
});
|
|
4407
|
+
describeKit = (k) => !k ? "No brand kit yet. Import one from the website with action import and url." : [
|
|
4408
|
+
`Brand kit: ${k.name || "(no name)"}${k.url ? ` (${k.url})` : ""}`,
|
|
4409
|
+
`Logo: ${k.logoUrl ?? "none (add logo_url, a PixelDojo upload)"}`,
|
|
4410
|
+
`Colors: ${k.colors.join(" ") || "none"}`,
|
|
4411
|
+
`Fonts: ${k.fonts.join(", ") || "none"}`,
|
|
4412
|
+
`Tagline: ${k.tagline || "none"}`,
|
|
4413
|
+
`Voice: ${k.voice || "none"}`,
|
|
4414
|
+
`Selling points: ${k.sellingPoints.join("; ") || "none"}`,
|
|
4415
|
+
"Every ad, campaign, image and motion template uses it unless that call passes brand_kit: false."
|
|
4416
|
+
].join("\n");
|
|
4417
|
+
brandKitSkill = {
|
|
4418
|
+
name: "pixeldojo_brand_kit",
|
|
4419
|
+
description: "The account's brand kit for Marketing Studio: logo, colors, fonts, voice, tagline and selling points. action import + url reads them from the brand's website (logo, palette and fonts from the page, the voice and claims from its words) and saves it; get shows it; update changes fields (colors as #RRGGBB, logo_url must be a PixelDojo upload). Free. Once set, every pixeldojo_ad, pixeldojo_campaign and pixeldojo_marketing call applies it automatically.",
|
|
4420
|
+
inputZod: KitSchema,
|
|
4421
|
+
inputJsonSchema: {
|
|
4422
|
+
type: "object",
|
|
4423
|
+
properties: {
|
|
4424
|
+
action: { type: "string", enum: ["get", "import", "update"], description: "Default get." },
|
|
4425
|
+
url: { type: "string", description: "import: the brand website, e.g. yourbrand.com." },
|
|
4426
|
+
save: { type: "boolean", description: "import: false returns the kit without saving it. Default true." },
|
|
4427
|
+
name: { type: "string" },
|
|
4428
|
+
tagline: { type: "string" },
|
|
4429
|
+
voice: { type: "string", description: "How the brand talks, one or two sentences." },
|
|
4430
|
+
colors: { type: "array", items: { type: "string" }, maxItems: 5, description: "#RRGGBB, lead color first." },
|
|
4431
|
+
fonts: { type: "array", items: { type: "string" }, maxItems: 2 },
|
|
4432
|
+
selling_points: { type: "array", items: { type: "string" }, maxItems: 5, description: "Short product claims, 2-7 words each." },
|
|
4433
|
+
logo_url: { type: "string", format: "uri", description: "A logo on PixelDojo (upload it first); PNG with transparency is best." }
|
|
4434
|
+
}
|
|
4435
|
+
},
|
|
4436
|
+
allowedModels: [],
|
|
4437
|
+
defaultModel: "",
|
|
4438
|
+
run: kitRun
|
|
4439
|
+
};
|
|
4440
|
+
ACTIONS = ["templates", "image", "motion", "end_card", "status"];
|
|
4441
|
+
MOTION = ["kinetic-headline", "feature-callouts", "count-up", "price-drop", "logo-sting"];
|
|
4442
|
+
MarketingSchema = zod.z.object({
|
|
4443
|
+
action: zod.z.enum(ACTIONS),
|
|
4444
|
+
kind: zod.z.enum(["image", "motion", "video"]).optional(),
|
|
4445
|
+
category: zod.z.string().max(40).optional(),
|
|
4446
|
+
template: zod.z.string().max(60).optional(),
|
|
4447
|
+
product_url: zod.z.string().url().optional(),
|
|
4448
|
+
product_name: zod.z.string().max(200).optional(),
|
|
4449
|
+
product_description: zod.z.string().max(2e3).optional(),
|
|
4450
|
+
product_image_urls: zod.z.array(zod.z.string().url()).max(5).optional(),
|
|
4451
|
+
product_image_paths: zod.z.array(zod.z.string()).max(5).optional(),
|
|
4452
|
+
character_image_urls: zod.z.array(zod.z.string().url()).max(2).optional(),
|
|
4453
|
+
headline: zod.z.string().max(80).optional(),
|
|
4454
|
+
points: zod.z.array(zod.z.string().max(70)).max(3).optional(),
|
|
4455
|
+
cta: zod.z.string().max(30).optional(),
|
|
4456
|
+
direction: zod.z.string().max(1500).optional(),
|
|
4457
|
+
aspect_ratio: zod.z.enum(["1:1", "4:5", "9:16", "16:9"]).optional(),
|
|
4458
|
+
engine: zod.z.enum(["nano-banana-pro", "nano-banana-2-1", "ideogram-4-5"]).optional(),
|
|
4459
|
+
count: zod.z.number().int().min(1).max(8).optional(),
|
|
4460
|
+
reference_ad_url: zod.z.string().url().optional(),
|
|
4461
|
+
stat: zod.z.string().max(16).optional(),
|
|
4462
|
+
stat_label: zod.z.string().max(40).optional(),
|
|
4463
|
+
price_was: zod.z.string().max(16).optional(),
|
|
4464
|
+
price_now: zod.z.string().max(16).optional(),
|
|
4465
|
+
video_url: zod.z.string().url().optional(),
|
|
4466
|
+
motion_id: zod.z.string().max(80).optional(),
|
|
4467
|
+
brand_name: zod.z.string().max(60).optional(),
|
|
4468
|
+
colors: zod.z.array(zod.z.string()).max(5).optional(),
|
|
4469
|
+
brand_kit: zod.z.boolean().optional()
|
|
4470
|
+
});
|
|
4471
|
+
props = {
|
|
4472
|
+
action: {
|
|
4473
|
+
type: "string",
|
|
4474
|
+
enum: ACTIONS,
|
|
4475
|
+
description: "templates = free list (filter by kind, category). image = image template on a product. motion = code-rendered motion graphic. end_card = brand end card after a finished ad (video_url). status = check a motion render (motion_id)."
|
|
4476
|
+
},
|
|
4477
|
+
kind: { type: "string", enum: ["image", "motion", "video"], description: "templates: filter." },
|
|
4478
|
+
category: { type: "string", description: "templates: product-shot, motion, ugc, tiktok, ads, posters or marketplace." },
|
|
4479
|
+
template: {
|
|
4480
|
+
type: "string",
|
|
4481
|
+
description: `image: an image template id from templates (e.g. stacked-shout, pure-white, ice-float). motion: ${MOTION.join(", ")}.`
|
|
4482
|
+
},
|
|
4483
|
+
product_url: { type: "string", format: "uri", description: "image: product page; name, description and photos are read from it." },
|
|
4484
|
+
product_name: { type: "string" },
|
|
4485
|
+
product_description: { type: "string" },
|
|
4486
|
+
product_image_urls: { type: "array", items: { type: "string", format: "uri" }, maxItems: 5, description: "Product photos. motion uses the first, cut out automatically." },
|
|
4487
|
+
product_image_paths: { type: "array", items: { type: "string" }, maxItems: 5, description: "Local product photos instead of URLs; uploaded first." },
|
|
4488
|
+
character_image_urls: { type: "array", items: { type: "string", format: "uri" }, maxItems: 2, description: "image: your own character (e.g. from pixeldojo_library source characters) for templates with a person." },
|
|
4489
|
+
headline: { type: "string", description: "Exact words on the ad. Blank uses the brand kit tagline." },
|
|
4490
|
+
points: { type: "array", items: { type: "string" }, maxItems: 3, description: "Selling points on the ad. Blank uses the brand kit." },
|
|
4491
|
+
cta: { type: "string", description: 'Button text. Default "Shop now".' },
|
|
4492
|
+
direction: { type: "string", description: "image: extra art direction added to the template." },
|
|
4493
|
+
aspect_ratio: { type: "string", enum: ["1:1", "4:5", "9:16", "16:9"], description: "Default: the template's own (motion 9:16; 4:5 renders 9:16 there)." },
|
|
4494
|
+
engine: { type: "string", enum: ["nano-banana-pro", "nano-banana-2-1", "ideogram-4-5"], description: "image: default nano-banana-pro (4.5 credits); nano-banana-2-1 is 1.5; ideogram-4-5 2 for poster type." },
|
|
4495
|
+
count: { type: "integer", minimum: 1, maximum: 8, description: "image: how many (each charged)." },
|
|
4496
|
+
reference_ad_url: { type: "string", format: "uri", description: "image ad multiplier: an image ad to rebuild with this product and your words. For a video ad use pixeldojo_ad reference_video_url." },
|
|
4497
|
+
stat: { type: "string", description: 'count-up: the number, e.g. "10,000+", "4.9", "98%".' },
|
|
4498
|
+
stat_label: { type: "string", description: "count-up: what it counts." },
|
|
4499
|
+
price_was: { type: "string", description: 'price-drop: old price, e.g. "$140".' },
|
|
4500
|
+
price_now: { type: "string", description: 'price-drop: new price, e.g. "$98".' },
|
|
4501
|
+
video_url: { type: "string", format: "uri", description: "end_card: the finished ad (a PixelDojo result)." },
|
|
4502
|
+
motion_id: { type: "string", description: "status: the id a motion or end_card call returned." },
|
|
4503
|
+
brand_name: { type: "string", description: "motion: the brand shown when there is no logo. Default: the brand kit." },
|
|
4504
|
+
colors: { type: "array", items: { type: "string" }, maxItems: 5, description: "motion: #RRGGBB, lead color first. Default: the brand kit." },
|
|
4505
|
+
brand_kit: { type: "boolean", description: "false skips the brand kit for this call." }
|
|
4506
|
+
};
|
|
4507
|
+
marketingSkill = {
|
|
4508
|
+
name: "pixeldojo_marketing",
|
|
4509
|
+
description: "Marketing Studio templates beyond video ads. templates lists them free with real example outputs. image applies an image template to a product: static ads, product shots, posters, marketplace listings (pure white for Amazon/Shopify), UGC photos, headline spelled exactly, 1-8 at a time at the engine price; with reference_ad_url it rebuilds an existing ad with this product (ad multiplier). motion renders code-built motion graphics with exact words, brand colors and logo: kinetic-headline, feature-callouts, count-up, price-drop, logo-sting (2 credits each). end_card adds the brand logo, tagline and button after a finished video ad (1 credit per 30 s). The brand kit (pixeldojo_brand_kit) fills colors, logo, headline and selling points unless brand_kit: false.",
|
|
4510
|
+
inputZod: MarketingSchema,
|
|
4511
|
+
inputJsonSchema: { type: "object", required: ["action"], properties: props },
|
|
4512
|
+
allowedModels: [],
|
|
4513
|
+
defaultModel: "",
|
|
4514
|
+
run: marketingRun
|
|
4515
|
+
};
|
|
4516
|
+
}
|
|
4517
|
+
});
|
|
4518
|
+
async function pollToResult(jobId, ctx, successText) {
|
|
4519
|
+
const result = await pollJobUntilDoneOrHandoff(jobId, (id) => ctx.client.getJob(id), ctx.buildStatusUrl, {
|
|
4520
|
+
now: ctx.pollOverrides?.now,
|
|
4521
|
+
sleep: ctx.pollOverrides?.sleep
|
|
4522
|
+
});
|
|
4523
|
+
return pollResultToSkillResult(result, (urls) => urls.length === 0 ? `Job ${jobId} completed but returned no assets.` : successText(urls));
|
|
4524
|
+
}
|
|
4525
|
+
async function buildRun(input, ctx) {
|
|
4526
|
+
const doFetch = ctx.fetch ?? fetch;
|
|
4527
|
+
if (input.action === "traits") {
|
|
4528
|
+
const res2 = await doFetch(`${ctx.baseUrl}/api/v1/characters/build`, { headers: headers3(ctx, "pixeldojo_build_character") });
|
|
4529
|
+
const data2 = await res2.json().catch(() => ({}));
|
|
4530
|
+
if (!res2.ok || !data2.traits) return { kind: "text", text: `Could not list traits: ${data2.error ?? `HTTP ${res2.status}`}` };
|
|
4531
|
+
return {
|
|
4532
|
+
kind: "text",
|
|
4533
|
+
text: [
|
|
4534
|
+
`Build a character: pass picks as {"trait id": "option id"} (an array for traits you can pick several of), or randomize: true to fill the rest. ${data2.takes ?? 4} takes cost ${data2.credits ?? "?"} credits. Free to list.`,
|
|
4535
|
+
...data2.traits.map(
|
|
4536
|
+
(t) => `${t.id} (${t.label}${t.pick_up_to ? `, pick up to ${t.pick_up_to}` : ""}${t.people_only ? ", people only" : ""}): ${t.options.map((o) => o.id).join(", ")}`
|
|
4537
|
+
)
|
|
4538
|
+
].join("\n")
|
|
4539
|
+
};
|
|
4540
|
+
}
|
|
4541
|
+
const res = await doFetch(`${ctx.baseUrl}/api/v1/characters/build`, {
|
|
4542
|
+
method: "POST",
|
|
4543
|
+
headers: headers3(ctx, "pixeldojo_build_character"),
|
|
4544
|
+
body: JSON.stringify({
|
|
4545
|
+
...input.picks ? { picks: input.picks } : {},
|
|
4546
|
+
...input.randomize ? { randomize: true } : {},
|
|
4547
|
+
...input.details ? { details: input.details } : {},
|
|
4548
|
+
...input.takes ? { takes: input.takes } : {}
|
|
4549
|
+
})
|
|
4550
|
+
});
|
|
4551
|
+
const data = await res.json().catch(() => ({}));
|
|
4552
|
+
if (!res.ok || !data.jobId) {
|
|
4553
|
+
const err = typeof data.error === "string" ? data.error : data.error?.message;
|
|
4554
|
+
return { kind: "text", text: `Character not built (nothing charged): ${err ?? `HTTP ${res.status}`}` };
|
|
4555
|
+
}
|
|
4556
|
+
return pollToResult(
|
|
4557
|
+
data.jobId,
|
|
4558
|
+
ctx,
|
|
4559
|
+
(urls) => [
|
|
4560
|
+
`${urls.length} take${urls.length === 1 ? "" : "s"} of ${data.summary ?? "the character"} (${data.creditCost ?? "?"} credits):`,
|
|
4561
|
+
...urls.map((u, i) => `${i + 1}. ${u}`),
|
|
4562
|
+
`Show them to the user. To keep one, call pixeldojo_save_character with a name, image_urls: [that take] and description: "${data.summary ?? ""}" within the hour (outputs expire). It then works as @Name in any prompt and can perform with pixeldojo_perform.`
|
|
4563
|
+
].join("\n")
|
|
4564
|
+
);
|
|
4565
|
+
}
|
|
4566
|
+
async function performRun(input, ctx) {
|
|
4567
|
+
const doFetch = ctx.fetch ?? fetch;
|
|
4568
|
+
if (input.action === "list") {
|
|
4569
|
+
const res2 = await doFetch(`${ctx.baseUrl}/api/v1/characters/perform`, { headers: headers3(ctx, "pixeldojo_perform") });
|
|
4570
|
+
const data2 = await res2.json().catch(() => ({}));
|
|
4571
|
+
if (!res2.ok || !data2.presets) return { kind: "text", text: `Could not list presets: ${data2.error ?? `HTTP ${res2.status}`}` };
|
|
4572
|
+
return {
|
|
4573
|
+
kind: "text",
|
|
4574
|
+
text: [
|
|
4575
|
+
'Perform presets (a 5 s video of a saved character doing it). Run one with action "run", character and preset:',
|
|
4576
|
+
...data2.presets.map((p) => `- ${p.slug} (${p.group}, ${p.credits} credits): ${p.title}. ${p.tagline}`)
|
|
4577
|
+
].join("\n")
|
|
4578
|
+
};
|
|
4579
|
+
}
|
|
4580
|
+
if (!input.character || !input.preset) {
|
|
4581
|
+
return { kind: "text", text: `Pass character (a saved character's name or @Name) and preset (from action "list").` };
|
|
4582
|
+
}
|
|
4583
|
+
const res = await doFetch(`${ctx.baseUrl}/api/v1/characters/perform`, {
|
|
4584
|
+
method: "POST",
|
|
4585
|
+
headers: headers3(ctx, "pixeldojo_perform"),
|
|
4586
|
+
body: JSON.stringify({ character: input.character, preset: input.preset })
|
|
4587
|
+
});
|
|
4588
|
+
const data = await res.json().catch(() => ({}));
|
|
4589
|
+
if (!res.ok || !data.jobId) {
|
|
4590
|
+
const err = typeof data.error === "string" ? data.error : data.error?.message;
|
|
4591
|
+
return { kind: "text", text: `Not started (nothing charged): ${err ?? `HTTP ${res.status}`}` };
|
|
4592
|
+
}
|
|
4593
|
+
return pollToResult(data.jobId, ctx, (urls) => `${input.character} performed ${input.preset} (${data.creditCost ?? "?"} credits). Asset: ${urls[0]}`);
|
|
4594
|
+
}
|
|
4595
|
+
var headers3, BuildSchema, buildCharacterSkill, PerformSchema, performSkill;
|
|
4596
|
+
var init_character_builder = __esm({
|
|
4597
|
+
"src/skills/character-builder.ts"() {
|
|
4598
|
+
init_poll();
|
|
4599
|
+
init_types();
|
|
4600
|
+
headers3 = (ctx, skill) => ({
|
|
4601
|
+
Authorization: `Bearer ${ctx.apiKey}`,
|
|
4602
|
+
"Content-Type": "application/json",
|
|
4603
|
+
"x-pixeldojo-source": `mcp/${ctx.mcpVersion}`,
|
|
4604
|
+
"x-pixeldojo-skill": skill
|
|
4605
|
+
});
|
|
4606
|
+
BuildSchema = zod.z.object({
|
|
4607
|
+
action: zod.z.enum(["traits", "build"]),
|
|
4608
|
+
picks: zod.z.record(zod.z.union([zod.z.string(), zod.z.array(zod.z.string())])).optional(),
|
|
4609
|
+
randomize: zod.z.boolean().optional(),
|
|
4610
|
+
details: zod.z.string().max(200).optional(),
|
|
4611
|
+
takes: zod.z.number().int().min(1).max(4).optional()
|
|
4612
|
+
});
|
|
4613
|
+
buildCharacterSkill = {
|
|
4614
|
+
name: "pixeldojo_build_character",
|
|
4615
|
+
description: `Build an original AI character or influencer from traits instead of photos, like Character Studio's builder: type (realistic, bold or extreme caricature, anime, 3D, cat, dog, fox, frog, insect, robot...), gender, age (adults only), body, height, wild proportions (egg body, long limbs, tiny waist), head shape (gigachad, mega jaw), hair, face features, style and accessories. action "traits" (free) lists every trait and option id; action "build" renders up to 4 takes from picks (and/or randomize). Keep one with pixeldojo_save_character.`,
|
|
4616
|
+
inputZod: BuildSchema,
|
|
4617
|
+
inputJsonSchema: {
|
|
4618
|
+
type: "object",
|
|
4619
|
+
required: ["action"],
|
|
4620
|
+
properties: {
|
|
4621
|
+
action: { type: "string", enum: ["traits", "build"], description: "traits = list trait and option ids (free). build = render the takes (paid)." },
|
|
4622
|
+
picks: {
|
|
4623
|
+
type: "object",
|
|
4624
|
+
additionalProperties: { anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }] },
|
|
4625
|
+
description: 'build: {"trait id": "option id"}, e.g. {"type": "frog", "hair": "mullet", "hairColor": "blonde", "distinctive": ["gold-grill"], "style": "retro"}. Ids from action "traits"; labels work too.'
|
|
4626
|
+
},
|
|
4627
|
+
randomize: { type: "boolean", description: "build: fill every trait not in picks at random." },
|
|
4628
|
+
details: { type: "string", description: 'build: up to 200 characters of extra look, e.g. "freckles, gold hoops, a vintage denim jacket".' },
|
|
4629
|
+
takes: { type: "integer", minimum: 1, maximum: 4, description: "build: how many takes (default 4)." }
|
|
4630
|
+
}
|
|
4631
|
+
},
|
|
4632
|
+
allowedModels: [],
|
|
4633
|
+
defaultModel: "",
|
|
4634
|
+
run: buildRun
|
|
4635
|
+
};
|
|
4636
|
+
PerformSchema = zod.z.object({
|
|
4637
|
+
action: zod.z.enum(["list", "run"]),
|
|
4638
|
+
character: zod.z.string().max(100).optional(),
|
|
4639
|
+
preset: zod.z.string().max(80).optional()
|
|
4640
|
+
});
|
|
4641
|
+
performSkill = {
|
|
4642
|
+
name: "pixeldojo_perform",
|
|
4643
|
+
description: 'Make a saved Character Studio character perform: a dance, a trend or a star moment (AI Dance, Sassy Dance, Shoulder Shimmy, Hair Flip, Catwalk, Peace Wink, Red Carpet, Award Win...) as a share-ready 5 s video. action "list" (free) gives the presets and prices; action "run" takes character (name or @Name) and preset. Usually handed off; poll with pixeldojo_status.',
|
|
4644
|
+
inputZod: PerformSchema,
|
|
4645
|
+
inputJsonSchema: {
|
|
4646
|
+
type: "object",
|
|
4647
|
+
required: ["action"],
|
|
4648
|
+
properties: {
|
|
4649
|
+
action: { type: "string", enum: ["list", "run"], description: "list = presets with prices (free). run = make the video (paid)." },
|
|
4650
|
+
character: { type: "string", description: "run: the saved character's name or @Name (pixeldojo_library source=characters lists them)." },
|
|
4651
|
+
preset: { type: "string", description: 'run: a preset slug from action "list", e.g. "ai-dance", "shoulder-shimmy", "red-carpet".' }
|
|
4652
|
+
}
|
|
4653
|
+
},
|
|
4654
|
+
allowedModels: [],
|
|
4655
|
+
defaultModel: "",
|
|
4656
|
+
run: performRun
|
|
4657
|
+
};
|
|
4658
|
+
}
|
|
4659
|
+
});
|
|
3584
4660
|
function substitutePlaceholders(args, inputs, prevUrl) {
|
|
3585
4661
|
const missing = [];
|
|
3586
4662
|
const fill = (value) => {
|
|
@@ -3824,20 +4900,29 @@ __export(skills_exports, {
|
|
|
3824
4900
|
TOOL_ANNOTATIONS: () => TOOL_ANNOTATIONS,
|
|
3825
4901
|
adSkill: () => adSkill,
|
|
3826
4902
|
audioSkill: () => audioSkill,
|
|
4903
|
+
brandKitSkill: () => brandKitSkill,
|
|
4904
|
+
buildCharacterSkill: () => buildCharacterSkill,
|
|
3827
4905
|
campaignSkill: () => campaignSkill,
|
|
3828
4906
|
campaignStatusSkill: () => campaignStatusSkill,
|
|
3829
4907
|
captionSkill: () => captionSkill,
|
|
3830
4908
|
characterSkill: () => characterSkill,
|
|
4909
|
+
dramaSkill: () => dramaSkill,
|
|
3831
4910
|
editSkill: () => editSkill,
|
|
4911
|
+
effectRunSkill: () => effectRunSkill,
|
|
4912
|
+
effectsListSkill: () => effectsListSkill,
|
|
3832
4913
|
episodeSkill: () => episodeSkill,
|
|
3833
4914
|
filmSkill: () => filmSkill,
|
|
3834
4915
|
findHostedSkill: () => findHostedSkill,
|
|
3835
4916
|
findSkill: () => findSkill,
|
|
3836
4917
|
fromUrlSkill: () => fromUrlSkill,
|
|
3837
4918
|
generateSkill: () => generateSkill,
|
|
4919
|
+
kataSkill: () => kataSkill,
|
|
3838
4920
|
librarySkill: () => librarySkill,
|
|
3839
4921
|
listWorkflowsSkill: () => listWorkflowsSkill,
|
|
4922
|
+
marketingSkill: () => marketingSkill,
|
|
4923
|
+
musicVideoSkill: () => musicVideoSkill,
|
|
3840
4924
|
overlaySkill: () => overlaySkill,
|
|
4925
|
+
performSkill: () => performSkill,
|
|
3841
4926
|
runWorkflowSkill: () => runWorkflowSkill,
|
|
3842
4927
|
saveCharacterSkill: () => saveCharacterSkill,
|
|
3843
4928
|
saveSkill: () => saveSkill,
|
|
@@ -3882,6 +4967,9 @@ var init_skills = __esm({
|
|
|
3882
4967
|
init_film();
|
|
3883
4968
|
init_video();
|
|
3884
4969
|
init_episode();
|
|
4970
|
+
init_drama();
|
|
4971
|
+
init_music_video();
|
|
4972
|
+
init_kata();
|
|
3885
4973
|
init_ad();
|
|
3886
4974
|
init_campaign();
|
|
3887
4975
|
init_campaign_status();
|
|
@@ -3890,6 +4978,9 @@ var init_skills = __esm({
|
|
|
3890
4978
|
init_library();
|
|
3891
4979
|
init_save();
|
|
3892
4980
|
init_save_character();
|
|
4981
|
+
init_effects();
|
|
4982
|
+
init_marketing();
|
|
4983
|
+
init_character_builder();
|
|
3893
4984
|
init_workflows();
|
|
3894
4985
|
ALL_SKILLS = [
|
|
3895
4986
|
generateSkill,
|
|
@@ -3900,9 +4991,13 @@ var init_skills = __esm({
|
|
|
3900
4991
|
captionSkill,
|
|
3901
4992
|
overlaySkill,
|
|
3902
4993
|
audioSkill,
|
|
4994
|
+
effectsListSkill,
|
|
4995
|
+
effectRunSkill,
|
|
3903
4996
|
librarySkill,
|
|
3904
4997
|
saveSkill,
|
|
3905
4998
|
saveCharacterSkill,
|
|
4999
|
+
buildCharacterSkill,
|
|
5000
|
+
performSkill,
|
|
3906
5001
|
saveWorkflowSkill,
|
|
3907
5002
|
runWorkflowSkill,
|
|
3908
5003
|
listWorkflowsSkill,
|
|
@@ -3910,23 +5005,28 @@ var init_skills = __esm({
|
|
|
3910
5005
|
fromUrlSkill,
|
|
3911
5006
|
uploadSkill,
|
|
3912
5007
|
adSkill,
|
|
5008
|
+
brandKitSkill,
|
|
5009
|
+
marketingSkill,
|
|
3913
5010
|
campaignSkill,
|
|
3914
5011
|
campaignStatusSkill,
|
|
3915
5012
|
filmSkill,
|
|
3916
5013
|
videoSkill,
|
|
3917
|
-
episodeSkill
|
|
5014
|
+
episodeSkill,
|
|
5015
|
+
dramaSkill,
|
|
5016
|
+
musicVideoSkill,
|
|
5017
|
+
kataSkill
|
|
3918
5018
|
];
|
|
3919
5019
|
isLocalPathField = (key) => /_paths?$/.test(key);
|
|
3920
5020
|
HOSTED_SKILLS = ALL_SKILLS.filter(
|
|
3921
5021
|
(s) => s.name !== uploadSkill.name
|
|
3922
5022
|
).map((s) => {
|
|
3923
|
-
const
|
|
5023
|
+
const props2 = s.inputJsonSchema.properties ?? {};
|
|
3924
5024
|
return {
|
|
3925
5025
|
...s,
|
|
3926
5026
|
description: s.hostedDescription ?? s.description,
|
|
3927
5027
|
inputJsonSchema: {
|
|
3928
5028
|
...s.inputJsonSchema,
|
|
3929
|
-
properties: Object.fromEntries(Object.entries(
|
|
5029
|
+
properties: Object.fromEntries(Object.entries(props2).filter(([k]) => !isLocalPathField(k)))
|
|
3930
5030
|
}
|
|
3931
5031
|
};
|
|
3932
5032
|
});
|
|
@@ -3941,11 +5041,18 @@ var init_skills = __esm({
|
|
|
3941
5041
|
pixeldojo_caption: render("Caption video"),
|
|
3942
5042
|
pixeldojo_overlay: render("Add offer or end card"),
|
|
3943
5043
|
pixeldojo_audio: render("Generate music, speech or sound"),
|
|
5044
|
+
pixeldojo_effects: read("List one-click effects"),
|
|
5045
|
+
pixeldojo_effect: render("Run one-click effect"),
|
|
3944
5046
|
pixeldojo_video: render("Change, animate or extend video"),
|
|
3945
5047
|
pixeldojo_ad: render("Make video ad"),
|
|
5048
|
+
pixeldojo_brand_kit: render("Brand kit from website"),
|
|
5049
|
+
pixeldojo_marketing: render("Image ads, motion graphics and templates"),
|
|
3946
5050
|
pixeldojo_campaign: render("Make product launch campaign"),
|
|
3947
5051
|
pixeldojo_film: render("Make short film"),
|
|
3948
5052
|
pixeldojo_episode: render("Make faceless video"),
|
|
5053
|
+
pixeldojo_drama: render("Make micro-drama, short film or trailer"),
|
|
5054
|
+
pixeldojo_music_video: render("Make music video for a song"),
|
|
5055
|
+
pixeldojo_kata: render("Recreate an edit with your cast"),
|
|
3949
5056
|
pixeldojo_run_workflow: render("Run saved workflow"),
|
|
3950
5057
|
pixeldojo_upload: render("Upload local file"),
|
|
3951
5058
|
pixeldojo_save_workflow: { ...render("Save workflow"), destructiveHint: true },
|
|
@@ -3954,6 +5061,8 @@ var init_skills = __esm({
|
|
|
3954
5061
|
pixeldojo_library: read("Search media library"),
|
|
3955
5062
|
pixeldojo_save: render("Save to My Media"),
|
|
3956
5063
|
pixeldojo_save_character: render("Save character or element"),
|
|
5064
|
+
pixeldojo_build_character: render("Build AI character from traits"),
|
|
5065
|
+
pixeldojo_perform: render("Make character dance or do a trend"),
|
|
3957
5066
|
pixeldojo_list_workflows: read("List saved workflows"),
|
|
3958
5067
|
pixeldojo_from_url: { ...read("Read product page"), openWorldHint: true }
|
|
3959
5068
|
};
|
|
@@ -3963,10 +5072,10 @@ function makeAttributedClient(options) {
|
|
|
3963
5072
|
const { mcpVersion, skillName, baseFetch, ...sdkOptions } = options;
|
|
3964
5073
|
const real = baseFetch ?? globalThis.fetch.bind(globalThis);
|
|
3965
5074
|
const wrappedFetch = (input, init) => {
|
|
3966
|
-
const
|
|
3967
|
-
|
|
3968
|
-
if (skillName)
|
|
3969
|
-
return real(input, { ...init, headers });
|
|
5075
|
+
const headers4 = new Headers(init?.headers);
|
|
5076
|
+
headers4.set("x-pixeldojo-source", `mcp/${mcpVersion}`);
|
|
5077
|
+
if (skillName) headers4.set("x-pixeldojo-skill", skillName);
|
|
5078
|
+
return real(input, { ...init, headers: headers4 });
|
|
3970
5079
|
};
|
|
3971
5080
|
return new sdk.PixelDojoClient({
|
|
3972
5081
|
...sdkOptions,
|
|
@@ -4010,7 +5119,7 @@ function makeRedactedLogger(target = console) {
|
|
|
4010
5119
|
}
|
|
4011
5120
|
|
|
4012
5121
|
// src/server.ts
|
|
4013
|
-
var PACKAGE_VERSION = "0.
|
|
5122
|
+
var PACKAGE_VERSION = "0.30.0" ;
|
|
4014
5123
|
var SERVER_INSTRUCTIONS = "Pixel Dojo generates images, video, audio and short films from one catalog of AI models, billed in credits from the connected Pixel Dojo account. A job that runs past the 30-second wait returns a jobId to check later; check it rather than submitting again, since every new submission is charged. Generated asset URLs are deleted one hour after they are created: pixeldojo_save keeps one in the user's My Media permanently (after a safety check), and finished films stay in the account library. In any prompt, @Name uses a character, product, outfit, location, prop or style the user saved in Character Studio: its photos are attached automatically. pixeldojo_library source=characters lists them, and pixeldojo_save_character saves a new one from 3 to 5 photos.";
|
|
4015
5124
|
function buildServer(options) {
|
|
4016
5125
|
if (!options.apiKey) {
|