@pixeldojo/mcp 0.14.0 → 0.15.1
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 +6 -2
- package/dist/cli.cjs +728 -88
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +728 -88
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +728 -88
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +728 -88
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -140,12 +140,13 @@ async function uploadLocalFile(path, ctx) {
|
|
|
140
140
|
);
|
|
141
141
|
}
|
|
142
142
|
const isImage = mime.startsWith("image/");
|
|
143
|
-
const
|
|
143
|
+
const isAudio = mime.startsWith("audio/");
|
|
144
|
+
const limit = isImage ? MAX_IMAGE_BYTES : isAudio ? MAX_AUDIO_BYTES : MAX_VIDEO_BYTES;
|
|
144
145
|
if (size > limit) {
|
|
145
146
|
const gotMB = (size / 1024 / 1024).toFixed(1);
|
|
146
147
|
const limitMB = (limit / 1024 / 1024).toFixed(0);
|
|
147
148
|
throw new SkillUploadError(
|
|
148
|
-
`"${basename(path)}" is ${gotMB}MB; limit for ${isImage ? "images" : "videos"} is ${limitMB}MB. Downscale or re-encode and try again.`
|
|
149
|
+
`"${basename(path)}" is ${gotMB}MB; limit for ${isImage ? "images" : isAudio ? "audio" : "videos"} is ${limitMB}MB. Downscale or re-encode and try again.`
|
|
149
150
|
);
|
|
150
151
|
}
|
|
151
152
|
const buffer = await readFile(path);
|
|
@@ -157,11 +158,12 @@ async function uploadLocalFile(path, ctx) {
|
|
|
157
158
|
async function uploadLocalFiles(paths, ctx) {
|
|
158
159
|
return Promise.all(paths.map((p) => uploadLocalFile(p, ctx)));
|
|
159
160
|
}
|
|
160
|
-
var MAX_IMAGE_BYTES, MAX_VIDEO_BYTES, EXT_TO_MIME, SkillUploadError, HOSTED_LOCAL_PATH_MESSAGE;
|
|
161
|
+
var MAX_IMAGE_BYTES, MAX_VIDEO_BYTES, MAX_AUDIO_BYTES, EXT_TO_MIME, SkillUploadError, HOSTED_LOCAL_PATH_MESSAGE;
|
|
161
162
|
var init_upload_helpers = __esm({
|
|
162
163
|
"src/skills/upload-helpers.ts"() {
|
|
163
164
|
MAX_IMAGE_BYTES = 50 * 1024 * 1024;
|
|
164
165
|
MAX_VIDEO_BYTES = 200 * 1024 * 1024;
|
|
166
|
+
MAX_AUDIO_BYTES = 50 * 1024 * 1024;
|
|
165
167
|
EXT_TO_MIME = {
|
|
166
168
|
".jpg": "image/jpeg",
|
|
167
169
|
".jpeg": "image/jpeg",
|
|
@@ -170,7 +172,14 @@ var init_upload_helpers = __esm({
|
|
|
170
172
|
".gif": "image/gif",
|
|
171
173
|
".mp4": "video/mp4",
|
|
172
174
|
".webm": "video/webm",
|
|
173
|
-
".mov": "video/quicktime"
|
|
175
|
+
".mov": "video/quicktime",
|
|
176
|
+
// Audio: the server checks the bytes match (a Film Studio song, song_path).
|
|
177
|
+
".mp3": "audio/mpeg",
|
|
178
|
+
".wav": "audio/wav",
|
|
179
|
+
".m4a": "audio/mp4",
|
|
180
|
+
".aac": "audio/aac",
|
|
181
|
+
".ogg": "audio/ogg",
|
|
182
|
+
".flac": "audio/flac"
|
|
174
183
|
};
|
|
175
184
|
SkillUploadError = class extends Error {
|
|
176
185
|
constructor(userMessage) {
|
|
@@ -540,7 +549,7 @@ var init_generate = __esm({
|
|
|
540
549
|
InputSchema = z.object({
|
|
541
550
|
prompt: z.string().min(1).max(8e3).describe("Prompt describing what to generate."),
|
|
542
551
|
model: z.string().optional().describe(
|
|
543
|
-
`Model apiId. Recommended: "seedream-5"
|
|
552
|
+
`Model apiId. Recommended (the models the PixelDojo studios use): "seedream-5" for images (the default), "seedance-2-5" for video (prompt only or a starting image), "wan-3-0-video" for video the others refuse. Use another model only when the user names one. Full catalog: https://pixeldojo.ai/api/v1/models; one model's fields: https://pixeldojo.ai/llm/{apiId}.txt. Omit for seedream-5, which makes still images only; name a video model to get a video.`
|
|
544
553
|
),
|
|
545
554
|
preset: z.string().optional().describe(
|
|
546
555
|
"Preset slug that seeds the running model's settings; explicit fields and params override it. Each preset belongs to one model (browse them by model at https://pixeldojo.ai/presets), so set `model` to that preset's model: a slug the running model has no preset for is rejected with a 400 before any charge."
|
|
@@ -579,7 +588,7 @@ var init_generate = __esm({
|
|
|
579
588
|
prompt: { type: "string", description: "Prompt describing what to generate." },
|
|
580
589
|
model: {
|
|
581
590
|
type: "string",
|
|
582
|
-
description: `Model apiId. Recommended: "seedream-5"
|
|
591
|
+
description: `Model apiId. Recommended (the models the PixelDojo studios use): "seedream-5" for images (the default), "seedance-2-5" for video (prompt only or a starting image), "wan-3-0-video" for video the others refuse. Use another model only when the user names one. Full catalog: https://pixeldojo.ai/api/v1/models; one model's fields: https://pixeldojo.ai/llm/{apiId}.txt. Omit for seedream-5, which makes still images only; name a video model to get a video.`
|
|
583
592
|
},
|
|
584
593
|
preset: {
|
|
585
594
|
type: "string",
|
|
@@ -627,8 +636,8 @@ var init_generate = __esm({
|
|
|
627
636
|
};
|
|
628
637
|
generateSkill = {
|
|
629
638
|
name: "pixeldojo_generate",
|
|
630
|
-
description: "Generate an image or video from a prompt with any model in the PixelDojo catalog. With no `model`, a prompt runs on seedream-5, which makes still images only. For a video, name seedance-2-5
|
|
631
|
-
hostedDescription: "Generate an image or video from a prompt with any model in the PixelDojo catalog. With no `model`, a prompt runs on seedream-5, which makes still images only. For a video, name seedance-2-5
|
|
639
|
+
description: "Generate an image or video from a prompt with any model in the PixelDojo catalog. With no `model`, a prompt runs on seedream-5, which makes still images only. For a video, name seedance-2-5, which works from a prompt alone or a starting image. If a provider refuses the content, retry once on wan-3-0-video (video) or seedream-5 (images), the least-restricted models. For a multi-shot film or a music video synced to a song use pixeldojo_film; to change, move or extend an existing clip use pixeldojo_video. These are the models the PixelDojo studios use; name another only when the user asks for it. References: `image_url`/`image_urls` for stills, `video_url` for a source clip, `image_path`/`image_paths` for absolute local files (uploaded to 24h temp storage first). A clip or an audio track URL with no `model` runs on seedance-2-reference; a named model that cannot read a clip is swapped for it and the result says so, while a named model with no audio input drops the track. Model-specific settings go in `params` alongside an explicit `model`. Returns asset URLs, or a jobId for pixeldojo_status when the job outlasts the 30s wait.",
|
|
640
|
+
hostedDescription: "Generate an image or video from a prompt with any model in the PixelDojo catalog. With no `model`, a prompt runs on seedream-5, which makes still images only. For a video, name seedance-2-5, which works from a prompt alone or a starting image. If a provider refuses the content, retry once on wan-3-0-video (video) or seedream-5 (images), the least-restricted models. For a multi-shot film or a music video synced to a song use pixeldojo_film; to change, move or extend an existing clip use pixeldojo_video. These are the models the PixelDojo studios use; name another only when the user asks for it. References: `image_url`/`image_urls` for stills and `video_url` for a source clip, all public http(s) URLs. A clip or an audio track URL with no `model` runs on seedance-2-reference; a named model that cannot read a clip is swapped for it and the result says so, while a named model with no audio input drops the track. Model-specific settings go in `params` alongside an explicit `model`. Returns asset URLs, or a jobId for pixeldojo_status when the job outlasts the 30s wait.",
|
|
632
641
|
inputZod: InputSchema,
|
|
633
642
|
inputJsonSchema: InputJsonSchema,
|
|
634
643
|
allowedModels: null,
|
|
@@ -912,6 +921,7 @@ var init_character = __esm({
|
|
|
912
921
|
"seedream-4",
|
|
913
922
|
"google-nano-banana",
|
|
914
923
|
"wan-2.7-image-edit",
|
|
924
|
+
"qwen-3-pro",
|
|
915
925
|
"qwen-image-2.0-pro"
|
|
916
926
|
];
|
|
917
927
|
DEFAULT_MODEL3 = "seedream-5";
|
|
@@ -922,6 +932,7 @@ var init_character = __esm({
|
|
|
922
932
|
"seedream-4": "image_urls",
|
|
923
933
|
"google-nano-banana": "image_input",
|
|
924
934
|
"wan-2.7-image-edit": "image_urls",
|
|
935
|
+
"qwen-3-pro": "image",
|
|
925
936
|
"qwen-image-2.0-pro": "reference_images"
|
|
926
937
|
};
|
|
927
938
|
InputSchema3 = z.object({
|
|
@@ -1397,43 +1408,124 @@ function progressLine(f) {
|
|
|
1397
1408
|
const rendering = all.filter((sh) => sh.status === "clip-generating").length;
|
|
1398
1409
|
return `frames ${frames}/${all.length}, clips ${clips}/${all.length}${rendering > 0 ? ` (${rendering} rendering)` : ""}`;
|
|
1399
1410
|
}
|
|
1411
|
+
async function runPlanner(ctx, filmId, path, body, maxPolls = PLANNER_MAX_POLLS) {
|
|
1412
|
+
const submit = await filmApi(ctx, "POST", path, body);
|
|
1413
|
+
if (submit.data.film) return { film: submit.data.film };
|
|
1414
|
+
const jobId = submit.data.jobId;
|
|
1415
|
+
if (!jobId) return { error: submit.data.error ?? `HTTP ${submit.status}` };
|
|
1416
|
+
const sleep = ctx.pollOverrides?.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
1417
|
+
for (let i = 0; i < maxPolls; i += 1) {
|
|
1418
|
+
await sleep(PLANNER_POLL_MS);
|
|
1419
|
+
const { data } = await filmApi(ctx, "GET", `/api/films/${filmId}`);
|
|
1420
|
+
const job = data.film?.llmJob;
|
|
1421
|
+
if (!data.film || job?.id !== jobId) continue;
|
|
1422
|
+
if (job.status === "error") return { error: job.error ?? "the run failed" };
|
|
1423
|
+
if (job.status === "done") return { film: data.film };
|
|
1424
|
+
}
|
|
1425
|
+
return { pending: true };
|
|
1426
|
+
}
|
|
1427
|
+
async function sliceSong(ctx, film) {
|
|
1428
|
+
const r = await filmApi(ctx, "POST", `/api/films/${film.id}/music/slice`, {
|
|
1429
|
+
songDurationSec: film.musicDurationSec
|
|
1430
|
+
});
|
|
1431
|
+
return r.status === 200 ? null : `Could not cut the song into shots: ${r.data.error ?? r.status}`;
|
|
1432
|
+
}
|
|
1433
|
+
function plannedText(film, ctx) {
|
|
1434
|
+
const total = shots(film).length;
|
|
1435
|
+
const music = isMusicVideo(film);
|
|
1436
|
+
const sceneLines = film.scenes.map((s, i) => ` Scene ${i + 1} "${s.title}"${s.location ? ` [${s.location}]` : ""}: ${s.shots.length} shots`);
|
|
1437
|
+
return [
|
|
1438
|
+
`Planned ${music ? "music video " : ""}"${film.title}" (film_id: ${film.id}): ${film.scenes.length} scenes, ${total} shots, ${film.look}, ${film.aspect}.`,
|
|
1439
|
+
...sceneLines,
|
|
1440
|
+
"",
|
|
1441
|
+
music ? `Nothing has been charged yet. The song is cut into one slice per shot and each clip lip-syncs to its slice on the studio music video engine. Producing renders every shot, charged as each renders. No score is composed: the export plays your song.` : `Nothing has been charged yet. Producing renders every shot (roughly 35-40 credits per 5-second shot at default engines, charged as each renders) plus a few credits for the soundtrack.`,
|
|
1442
|
+
`Review or edit at ${ctx.baseUrl}/film-studio/${film.id}, or call pixeldojo_film with {"film_id": "${film.id}", "action": "produce"} to start production.`
|
|
1443
|
+
].join("\n");
|
|
1444
|
+
}
|
|
1400
1445
|
async function plan(input, ctx) {
|
|
1446
|
+
const music = Boolean(input.song_url || input.song_path);
|
|
1401
1447
|
if (!input.idea?.trim() && !input.script?.trim()) {
|
|
1402
|
-
return {
|
|
1448
|
+
return {
|
|
1449
|
+
kind: "text",
|
|
1450
|
+
text: music ? "Provide an `idea` describing the visuals of the music video (plus optional `lyrics`)." : "Provide an `idea` (we write the script, free) or a full `script` to plan a film."
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
if (input.song_path && !input.song_url) {
|
|
1454
|
+
try {
|
|
1455
|
+
input = { ...input, song_url: await uploadLocalFile(input.song_path, ctx) };
|
|
1456
|
+
} catch (err) {
|
|
1457
|
+
if (err instanceof SkillUploadError) return { kind: "text", text: `Could not upload the song: ${err.userMessage}` };
|
|
1458
|
+
throw err;
|
|
1459
|
+
}
|
|
1403
1460
|
}
|
|
1404
1461
|
const created = await filmApi(ctx, "POST", "/api/films", {
|
|
1405
1462
|
aspect: input.aspect,
|
|
1406
|
-
look: input.look,
|
|
1463
|
+
look: input.look ?? (music ? MUSIC_VIDEO_LOOKS[0] : "live-action"),
|
|
1407
1464
|
lengthTarget: input.length,
|
|
1408
|
-
assetIds: []
|
|
1465
|
+
assetIds: [],
|
|
1466
|
+
...music ? {
|
|
1467
|
+
// No script step names a music video, so it would stay "Untitled film".
|
|
1468
|
+
...input.idea?.trim() ? { title: input.idea.trim().slice(0, 80) } : {},
|
|
1469
|
+
musicVideoMode: true,
|
|
1470
|
+
musicUrl: input.song_url,
|
|
1471
|
+
...input.song_duration_sec ? { musicDurationSec: input.song_duration_sec } : {},
|
|
1472
|
+
clipEngine: MUSIC_CLIP_ENGINE_ID
|
|
1473
|
+
} : {}
|
|
1409
1474
|
});
|
|
1410
1475
|
if (!created.data.film) return { kind: "text", text: `Could not create the film: ${created.data.error ?? created.status}` };
|
|
1411
1476
|
const id = created.data.film.id;
|
|
1412
|
-
let
|
|
1413
|
-
if (
|
|
1414
|
-
const
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1477
|
+
let planned;
|
|
1478
|
+
if (music) {
|
|
1479
|
+
const brief = [input.idea?.trim() || input.script.trim(), input.lyrics?.trim() ? `Lyrics:
|
|
1480
|
+
${input.lyrics.trim()}` : ""].filter(Boolean).join("\n\n");
|
|
1481
|
+
planned = await runPlanner(ctx, id, `/api/films/${id}/plan`, { script: brief });
|
|
1482
|
+
} else {
|
|
1483
|
+
let script = input.script?.trim() ?? "";
|
|
1484
|
+
if (!script) {
|
|
1485
|
+
const wrote = await runPlanner(ctx, id, `/api/films/${id}/script`, { idea: input.idea.trim() });
|
|
1486
|
+
if ("pending" in wrote) return { kind: "text", text: stillPlanningText(id, "script") };
|
|
1487
|
+
if ("error" in wrote) return { kind: "text", text: `Script writing failed: ${wrote.error}` };
|
|
1488
|
+
script = wrote.film.script ?? "";
|
|
1489
|
+
if (!script) return { kind: "text", text: "Script writing failed: the studio returned an empty script." };
|
|
1490
|
+
}
|
|
1491
|
+
planned = await runPlanner(ctx, id, `/api/films/${id}/plan`, { script });
|
|
1492
|
+
}
|
|
1493
|
+
if ("pending" in planned) return { kind: "text", text: stillPlanningText(id) };
|
|
1494
|
+
if ("error" in planned) return { kind: "text", text: `Shot planning failed: ${planned.error}` };
|
|
1495
|
+
const film = planned.film;
|
|
1496
|
+
if (needsSlice(film)) {
|
|
1497
|
+
const err = await sliceSong(ctx, film);
|
|
1498
|
+
if (err) return { kind: "text", text: `${plannedText(film, ctx)}
|
|
1499
|
+
|
|
1500
|
+
${err} Call action "status" to retry the cut.` };
|
|
1501
|
+
}
|
|
1502
|
+
return { kind: "text", text: plannedText(film, ctx) };
|
|
1503
|
+
}
|
|
1504
|
+
async function resumePlanning(film, ctx) {
|
|
1505
|
+
if (film.scenes.length > 0 && film.llmJob?.status !== "running") return null;
|
|
1506
|
+
if (film.llmJob?.status === "running") {
|
|
1507
|
+
return { kind: "text", text: stillPlanningText(film.id, film.llmJob.kind === "script" ? "script" : "plan") };
|
|
1508
|
+
}
|
|
1509
|
+
const prior = film.llmJob?.status === "error" ? `The last planning run failed (${film.llmJob.error ?? "unknown"}). ` : "";
|
|
1510
|
+
if (!film.script?.trim()) {
|
|
1511
|
+
return { kind: "text", text: `${prior}This film has nothing to plan from. Start a new one with action "plan".` };
|
|
1512
|
+
}
|
|
1513
|
+
const planned = await runPlanner(ctx, film.id, `/api/films/${film.id}/plan`, { script: film.script }, 0);
|
|
1514
|
+
if ("pending" in planned) return { kind: "text", text: `${prior}${stillPlanningText(film.id)}` };
|
|
1515
|
+
if ("error" in planned) return { kind: "text", text: `Shot planning failed: ${planned.error} Call action "status" to retry.` };
|
|
1516
|
+
if (needsSlice(planned.film)) {
|
|
1517
|
+
const err = await sliceSong(ctx, planned.film);
|
|
1518
|
+
if (err) return { kind: "text", text: `${plannedText(planned.film, ctx)}
|
|
1519
|
+
|
|
1520
|
+
${err} Call action "status" to retry the cut.` };
|
|
1521
|
+
}
|
|
1522
|
+
return { kind: "text", text: plannedText(planned.film, ctx) };
|
|
1433
1523
|
}
|
|
1434
1524
|
async function produce(filmId, ctx) {
|
|
1435
1525
|
const { data } = await filmApi(ctx, "GET", `/api/films/${filmId}`);
|
|
1436
1526
|
if (!data.film) return { kind: "text", text: `Film not found: ${data.error ?? filmId}` };
|
|
1527
|
+
const planning = await resumePlanning(data.film, ctx);
|
|
1528
|
+
if (planning) return planning;
|
|
1437
1529
|
const pending = shots(data.film).filter((sh) => sh.status === "idle" || sh.status === "failed" && !sh.frameUrl);
|
|
1438
1530
|
let fired = 0;
|
|
1439
1531
|
for (const sh of pending.slice(0, 6)) {
|
|
@@ -1497,6 +1589,16 @@ It is saved in the Library folder "Film: ${film.title}".`
|
|
|
1497
1589
|
if (film.exportError) {
|
|
1498
1590
|
return { kind: "text", text: `Export failed: ${film.exportError} Poll again with action "status" to retry.` };
|
|
1499
1591
|
}
|
|
1592
|
+
const planning = await resumePlanning(film, ctx);
|
|
1593
|
+
if (planning) return planning;
|
|
1594
|
+
if (needsSlice(film)) {
|
|
1595
|
+
const err = await sliceSong(ctx, film);
|
|
1596
|
+
if (err) return { kind: "text", text: `${err} Poll again with action "status" to retry.` };
|
|
1597
|
+
return { kind: "text", text: `Song cut into one slice per shot. Poll again with {"film_id": "${filmId}", "action": "status"} to keep production moving.` };
|
|
1598
|
+
}
|
|
1599
|
+
if (shots(film).length > 0 && shots(film).every((sh) => sh.status === "idle")) {
|
|
1600
|
+
return { kind: "text", text: plannedText(film, ctx) };
|
|
1601
|
+
}
|
|
1500
1602
|
const idleFrames = all.filter((sh) => sh.status === "idle");
|
|
1501
1603
|
for (const sh of idleFrames.slice(0, 6)) {
|
|
1502
1604
|
await filmApi(ctx, "POST", `/api/films/${filmId}/shots/${sh.id}/generate`, { kind: "frame" });
|
|
@@ -1516,7 +1618,7 @@ It is saved in the Library folder "Film: ${film.title}".`
|
|
|
1516
1618
|
await filmApi(ctx, "POST", `/api/films/${filmId}/save`, { kind: "clip" });
|
|
1517
1619
|
}
|
|
1518
1620
|
const hasMusic = film.audioStems.some((st) => st.kind === "music");
|
|
1519
|
-
if (soundtrack !== "none" && !hasMusic) {
|
|
1621
|
+
if (soundtrack !== "none" && !hasMusic && !isMusicVideo(film)) {
|
|
1520
1622
|
if (musicJobId) {
|
|
1521
1623
|
const job = await ctx.client.getJob(musicJobId);
|
|
1522
1624
|
const out = job.output;
|
|
@@ -1555,14 +1657,22 @@ async function run8(input, ctx) {
|
|
|
1555
1657
|
if (action === "produce") return produce(input.film_id, ctx);
|
|
1556
1658
|
return advance(input.film_id, input.soundtrack, input.music_job_id, ctx);
|
|
1557
1659
|
}
|
|
1558
|
-
var LOOKS, InputSchema8, InputJsonSchema8, shots, count, LOOK_MUSIC, ARC, filmSkill;
|
|
1660
|
+
var STORY_LOOKS, MUSIC_VIDEO_LOOKS, LOOKS, MUSIC_CLIP_ENGINE_ID, InputSchema8, InputJsonSchema8, shots, count, PLANNER_POLL_MS, PLANNER_MAX_POLLS, isMusicVideo, needsSlice, stillPlanningText, LOOK_MUSIC, ARC, filmSkill;
|
|
1559
1661
|
var init_film = __esm({
|
|
1560
1662
|
"src/skills/film.ts"() {
|
|
1561
|
-
|
|
1663
|
+
init_upload_helpers();
|
|
1664
|
+
STORY_LOOKS = ["live-action", "anime", "3d-animation", "noir", "horror", "sci-fi", "documentary", "western", "romance", "romantic-comedy", "comedy"];
|
|
1665
|
+
MUSIC_VIDEO_LOOKS = ["rap-video", "neon-club", "stage-performance", "retro-camcorder", "y2k-pop", "moody-rnb", "rock-grit", "dream-pop", "k-pop"];
|
|
1666
|
+
LOOKS = [...STORY_LOOKS, ...MUSIC_VIDEO_LOOKS];
|
|
1667
|
+
MUSIC_CLIP_ENGINE_ID = "minimax-h3-reference";
|
|
1562
1668
|
InputSchema8 = z.object({
|
|
1563
1669
|
idea: z.string().max(2e3).optional().describe("One-to-three sentence film idea. The studio writes the screenplay (free). Provide idea OR script for planning."),
|
|
1564
1670
|
script: z.string().max(2e4).optional().describe("A full screenplay to plan instead of an idea (sluglines, action lines, dialogue)."),
|
|
1565
|
-
look: z.enum(LOOKS).
|
|
1671
|
+
look: z.enum(LOOKS).optional().describe("Visual style carried through frames, clips, and soundtrack. Default live-action, or rap-video for a music video."),
|
|
1672
|
+
song_url: z.string().url().startsWith("https://").max(2e3).optional().describe("Music video mode: URL of the song, which must be your own PixelDojo upload (from pixeldojo_upload), not a third-party link. The film is cut to the song and every clip lip-syncs to its slice of it. idea describes the visuals."),
|
|
1673
|
+
song_path: z.string().max(4096).optional().describe("Music video mode, local server only: absolute path to the song file (MP3, WAV, M4A, AAC, OGG, FLAC up to 50MB). Uploaded for you; use instead of song_url."),
|
|
1674
|
+
song_duration_sec: z.number().positive().max(7200).optional().describe("Music video mode (optional): the song length in seconds. Omit it and the studio measures the song."),
|
|
1675
|
+
lyrics: z.string().max(1e4).optional().describe("Music video mode: the song lyrics, so shots land on the right lines. Optional."),
|
|
1566
1676
|
aspect: z.enum(["16:9", "9:16"]).default("16:9"),
|
|
1567
1677
|
length: z.enum(["30s", "60s", "2-3min", "5min"]).default("60s").describe("Target length; drives the shot count (~6/12/24/60 shots)."),
|
|
1568
1678
|
film_id: z.string().max(40).optional().describe("Existing film id (from a previous call) for produce/status."),
|
|
@@ -1575,9 +1685,22 @@ var init_film = __esm({
|
|
|
1575
1685
|
properties: {
|
|
1576
1686
|
idea: { type: "string", description: "Plan step: one-to-three sentence film idea. The studio writes the screenplay (free). Provide idea or script." },
|
|
1577
1687
|
script: { type: "string", description: "Plan step: a full screenplay (sluglines, action lines, dialogue) to plan instead of an idea." },
|
|
1578
|
-
look: { type: "string", enum: [...LOOKS], description:
|
|
1688
|
+
look: { type: "string", enum: [...LOOKS], description: `Plan step: visual style carried through frames, clips and soundtrack. Story looks: ${STORY_LOOKS.join(", ")} (default live-action). Music video looks: ${MUSIC_VIDEO_LOOKS.join(", ")} (default rap-video).` },
|
|
1689
|
+
song_url: {
|
|
1690
|
+
type: "string",
|
|
1691
|
+
description: "Plan step, music video mode: URL of the song. It must be your own PixelDojo upload (from pixeldojo_upload or /api/v1/upload); a third-party link is refused. Makes a music video cut to this song, every clip lip-synced to its slice of it, rendered on the studio music video engine. idea (or script) describes the visuals. No score is composed: the song is the soundtrack."
|
|
1692
|
+
},
|
|
1693
|
+
song_path: {
|
|
1694
|
+
type: "string",
|
|
1695
|
+
description: "Plan step, music video mode: absolute local path to the song (MP3, WAV, M4A, AAC, OGG, FLAC up to 50MB). Uploaded for you; use instead of song_url."
|
|
1696
|
+
},
|
|
1697
|
+
song_duration_sec: {
|
|
1698
|
+
type: "number",
|
|
1699
|
+
description: "Plan step, music video mode (optional): the song length in seconds. Omit it and the studio measures the song."
|
|
1700
|
+
},
|
|
1701
|
+
lyrics: { type: "string", description: "Plan step, music video mode (optional): the song lyrics, so shots land on the right lines." },
|
|
1579
1702
|
aspect: { type: "string", enum: ["16:9", "9:16"], description: "Plan step: frame aspect ratio. Default 16:9." },
|
|
1580
|
-
length: { type: "string", enum: ["30s", "60s", "2-3min", "5min"], description: "Plan step: target length, which sets the shot count (~6/12/24/60 shots). Default 60s." },
|
|
1703
|
+
length: { type: "string", enum: ["30s", "60s", "2-3min", "5min"], description: "Plan step: target length, which sets the shot count (~6/12/24/60 shots). Default 60s. Ignored for a music video, whose length is the song." },
|
|
1581
1704
|
film_id: { type: "string", description: 'The film_id returned by the plan step. Required for produce and status; with it, action "plan" is treated as "status".' },
|
|
1582
1705
|
action: {
|
|
1583
1706
|
type: "string",
|
|
@@ -1587,7 +1710,7 @@ var init_film = __esm({
|
|
|
1587
1710
|
soundtrack: {
|
|
1588
1711
|
type: "string",
|
|
1589
1712
|
enum: ["scene-synced", "flat", "none"],
|
|
1590
|
-
description: "Read on status calls once all clips are ready. scene-synced (default): one musical section per scene. flat: one cohesive bed. none: export without music."
|
|
1713
|
+
description: "Read on status calls once all clips are ready. scene-synced (default): one musical section per scene. flat: one cohesive bed. none: export without music. Ignored for a music video, which always exports with its song."
|
|
1591
1714
|
},
|
|
1592
1715
|
music_job_id: {
|
|
1593
1716
|
type: "string",
|
|
@@ -1597,6 +1720,11 @@ var init_film = __esm({
|
|
|
1597
1720
|
};
|
|
1598
1721
|
shots = (f) => f.scenes.flatMap((s) => s.shots);
|
|
1599
1722
|
count = (f, pred) => shots(f).filter(pred).length;
|
|
1723
|
+
PLANNER_POLL_MS = 3e3;
|
|
1724
|
+
PLANNER_MAX_POLLS = 8;
|
|
1725
|
+
isMusicVideo = (f) => f.musicVideoMode === true;
|
|
1726
|
+
needsSlice = (f) => isMusicVideo(f) && shots(f).length > 0 && !shots(f).some((sh) => sh.musicChunkUrl);
|
|
1727
|
+
stillPlanningText = (filmId, stage = "plan") => `${stage === "script" ? "Still writing the script" : "Still planning the shots"} for film_id ${filmId}. Nothing has been charged. Call pixeldojo_film with {"film_id": "${filmId}", "action": "status"} in ~30s; it returns the plan once it is ready.`;
|
|
1600
1728
|
LOOK_MUSIC = {
|
|
1601
1729
|
"live-action": "modern cinematic underscore, warm strings and soft felt piano, subtle rhythmic pulse, slow emotional build",
|
|
1602
1730
|
anime: "sweeping anime film score, lush strings and piano, taiko drum accents, soaring heartfelt theme",
|
|
@@ -1617,7 +1745,8 @@ var init_film = __esm({
|
|
|
1617
1745
|
};
|
|
1618
1746
|
filmSkill = {
|
|
1619
1747
|
name: "pixeldojo_film",
|
|
1620
|
-
description: "Make a finished short film from one idea: screenplay, storyboard frames (AI-reviewed), video clips with spoken dialogue, scene-synced soundtrack, stitched export.
|
|
1748
|
+
description: "Make a finished short film from one idea: screenplay, storyboard frames (AI-reviewed), video clips with spoken dialogue, scene-synced soundtrack, stitched export. Or a music video: pass song_path (a local file) or song_url (your own PixelDojo upload from pixeldojo_upload, never a third-party link) with optional lyrics; the film is cut to that song, each clip lip-synced to its slice on the studio music video engine, and no score is composed: the export plays the song. Phased: plan is free; produce starts charged generation; status advances production (poll every ~30s).",
|
|
1749
|
+
hostedDescription: "Make a finished short film from one idea: screenplay, storyboard frames (AI-reviewed), video clips with spoken dialogue, scene-synced soundtrack, stitched export. Or a music video: pass song_url (your own PixelDojo upload made with POST /api/v1/upload, never a third-party link) with optional lyrics; the film is cut to that song, each clip lip-synced to its slice on the studio music video engine, and no score is composed: the export plays the song. Phased: plan is free; produce starts charged generation; status advances production (poll every ~30s).",
|
|
1621
1750
|
inputZod: InputSchema8,
|
|
1622
1751
|
inputJsonSchema: InputJsonSchema8,
|
|
1623
1752
|
allowedModels: [],
|
|
@@ -1626,7 +1755,512 @@ var init_film = __esm({
|
|
|
1626
1755
|
};
|
|
1627
1756
|
}
|
|
1628
1757
|
});
|
|
1758
|
+
function buildVideoPayload(verb, apiId, i) {
|
|
1759
|
+
const prompt = i.prompt ?? "";
|
|
1760
|
+
const vd = i.videoDuration;
|
|
1761
|
+
if (NEEDS_VIDEO_DURATION.has(apiId) && vd === void 0) {
|
|
1762
|
+
return {
|
|
1763
|
+
error: `${apiId} needs video_duration: the source clip's length in seconds (it sets the credit cost).`
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
if (verb === "move" && !i.images[0]) {
|
|
1767
|
+
return { error: `The move verb needs image_url: the character image to put into the video.` };
|
|
1768
|
+
}
|
|
1769
|
+
const droppedImages = i.images.length > 0 && (verb === "extend" || NO_IMAGE_ENGINES.has(apiId)) ? `${apiId} takes no reference images, so I left them off.` : void 0;
|
|
1770
|
+
if (verb === "change") {
|
|
1771
|
+
switch (apiId) {
|
|
1772
|
+
case "seedance-2-5-video-edit":
|
|
1773
|
+
return {
|
|
1774
|
+
body: {
|
|
1775
|
+
mode: "edit",
|
|
1776
|
+
prompt,
|
|
1777
|
+
video_url: i.videoUrl,
|
|
1778
|
+
reference_images: i.images,
|
|
1779
|
+
...vd !== void 0 ? { source_duration_seconds: vd } : {}
|
|
1780
|
+
}
|
|
1781
|
+
};
|
|
1782
|
+
case "flux-video-edit":
|
|
1783
|
+
return { body: { prompt, video: i.videoUrl, video_duration: Math.ceil(vd) }, note: droppedImages };
|
|
1784
|
+
case "xai-video-edit":
|
|
1785
|
+
return { body: { prompt, video_url: i.videoUrl, video_duration: vd }, note: droppedImages };
|
|
1786
|
+
case "happyhorse-1.0-video-edit":
|
|
1787
|
+
return { body: { prompt, video_url: i.videoUrl, video_duration: vd, reference_urls: i.images } };
|
|
1788
|
+
default:
|
|
1789
|
+
return { body: { prompt, video_url: i.videoUrl, video_duration: vd, image_urls: i.images } };
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
if (verb === "move") {
|
|
1793
|
+
if (apiId === "wan-video-character-swap") {
|
|
1794
|
+
return { body: { prompt: "", image_url: i.images[0], video_url: i.videoUrl, video_duration: vd } };
|
|
1795
|
+
}
|
|
1796
|
+
return {
|
|
1797
|
+
body: {
|
|
1798
|
+
prompt,
|
|
1799
|
+
video: i.videoUrl,
|
|
1800
|
+
video_duration: vd,
|
|
1801
|
+
character_image: i.images[0],
|
|
1802
|
+
// Not a background toggle: 'video' is the 30s duration cap.
|
|
1803
|
+
character_orientation: "video"
|
|
1804
|
+
}
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
switch (apiId) {
|
|
1808
|
+
case "seedance-2-5-video-edit":
|
|
1809
|
+
return {
|
|
1810
|
+
body: {
|
|
1811
|
+
mode: "extend",
|
|
1812
|
+
prompt,
|
|
1813
|
+
video_url: i.videoUrl,
|
|
1814
|
+
direction: i.direction ?? "forward",
|
|
1815
|
+
duration: clamp(i.duration ?? 5, 4, 30),
|
|
1816
|
+
...vd !== void 0 ? { source_duration_seconds: vd } : {}
|
|
1817
|
+
},
|
|
1818
|
+
note: droppedImages
|
|
1819
|
+
};
|
|
1820
|
+
case "wan-2.7-i2v": {
|
|
1821
|
+
const chosen = clamp(i.duration ?? 5, 2, 15);
|
|
1822
|
+
return {
|
|
1823
|
+
body: {
|
|
1824
|
+
mode: "video-extend",
|
|
1825
|
+
prompt,
|
|
1826
|
+
video_url: i.videoUrl,
|
|
1827
|
+
// A continuation shorter than its own source is rejected outright.
|
|
1828
|
+
duration: vd ? Math.min(15, Math.max(chosen, Math.ceil(vd) + 1)) : chosen,
|
|
1829
|
+
...vd ? { source_video_duration: vd } : {}
|
|
1830
|
+
},
|
|
1831
|
+
note: droppedImages
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1834
|
+
default:
|
|
1835
|
+
return {
|
|
1836
|
+
body: { prompt, video_url: i.videoUrl, extension_duration: clamp(i.duration ?? 6, 2, 10) },
|
|
1837
|
+
note: droppedImages
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1629
1841
|
async function run9(input, ctx) {
|
|
1842
|
+
const paramsGate = paramsWithoutModel(input.params, input.model);
|
|
1843
|
+
if (paramsGate) return { kind: "text", text: paramsGate };
|
|
1844
|
+
const verb = input.verb;
|
|
1845
|
+
const engines = VIDEO_STUDIO_ENGINES[verb];
|
|
1846
|
+
const notes = [];
|
|
1847
|
+
let apiId = input.model ?? engines[0];
|
|
1848
|
+
if (!engines.includes(apiId)) {
|
|
1849
|
+
notes.push(`${apiId} is not a Video Studio ${verb} engine, so I used ${engines[0]} instead.`);
|
|
1850
|
+
apiId = engines[0];
|
|
1851
|
+
}
|
|
1852
|
+
if (verb === "extend" && input.direction === "backward" && !BACKWARD_ENGINES.has(apiId)) {
|
|
1853
|
+
notes.push(`${apiId} only extends forward, so I used ${engines[0]} to extend backward.`);
|
|
1854
|
+
apiId = engines[0];
|
|
1855
|
+
}
|
|
1856
|
+
let videoUrl = input.video_url;
|
|
1857
|
+
const images = [...input.image_url ? [input.image_url] : [], ...input.image_urls ?? []];
|
|
1858
|
+
try {
|
|
1859
|
+
if (!videoUrl && input.video_path) videoUrl = await uploadLocalFile(input.video_path, ctx);
|
|
1860
|
+
if (!input.image_url && input.image_path) images.unshift(await uploadLocalFile(input.image_path, ctx));
|
|
1861
|
+
} catch (err) {
|
|
1862
|
+
if (err instanceof SkillUploadError) {
|
|
1863
|
+
return { kind: "text", text: `pixeldojo_video (upload): ${err.userMessage}` };
|
|
1864
|
+
}
|
|
1865
|
+
throw err;
|
|
1866
|
+
}
|
|
1867
|
+
if (!videoUrl) {
|
|
1868
|
+
return {
|
|
1869
|
+
kind: "text",
|
|
1870
|
+
text: `pixeldojo_video ${verb} needs video_url: the ${verb === "move" ? "driving motion clip" : "source clip"}.`
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
if (verb !== "move" && !input.prompt?.trim()) {
|
|
1874
|
+
return { kind: "text", text: `pixeldojo_video ${verb} needs a prompt.` };
|
|
1875
|
+
}
|
|
1876
|
+
const built = buildVideoPayload(verb, apiId, {
|
|
1877
|
+
prompt: input.prompt,
|
|
1878
|
+
videoUrl,
|
|
1879
|
+
images: images.filter((u, i, all) => all.indexOf(u) === i),
|
|
1880
|
+
videoDuration: input.video_duration,
|
|
1881
|
+
duration: input.duration,
|
|
1882
|
+
direction: input.direction
|
|
1883
|
+
});
|
|
1884
|
+
if ("error" in built) return { kind: "text", text: `pixeldojo_video: ${built.error}` };
|
|
1885
|
+
if (built.note) notes.push(built.note);
|
|
1886
|
+
const body = built.body;
|
|
1887
|
+
const paramsNote = applyParams(body, input.params, input.model, apiId);
|
|
1888
|
+
if (paramsNote) notes.push(paramsNote);
|
|
1889
|
+
const submitted = await ctx.client.run(apiId, body);
|
|
1890
|
+
const result = await pollJobUntilDoneOrHandoff(
|
|
1891
|
+
submitted.jobId,
|
|
1892
|
+
(id) => ctx.client.getJob(id),
|
|
1893
|
+
ctx.buildStatusUrl,
|
|
1894
|
+
{ now: ctx.pollOverrides?.now, sleep: ctx.pollOverrides?.sleep }
|
|
1895
|
+
);
|
|
1896
|
+
return prefixNote(
|
|
1897
|
+
pollResultToSkillResult(
|
|
1898
|
+
result,
|
|
1899
|
+
(urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Video ready (${verb} on ${apiId}). Asset: ${urls.join(", ")}`
|
|
1900
|
+
),
|
|
1901
|
+
notes.join(" ")
|
|
1902
|
+
);
|
|
1903
|
+
}
|
|
1904
|
+
var VIDEO_STUDIO_ENGINES, VERBS, BACKWARD_ENGINES, clamp, NEEDS_VIDEO_DURATION, NO_IMAGE_ENGINES, InputSchema9, engineList, InputJsonSchema9, BASE_DESCRIPTION, videoSkill;
|
|
1905
|
+
var init_video = __esm({
|
|
1906
|
+
"src/skills/video.ts"() {
|
|
1907
|
+
init_poll();
|
|
1908
|
+
init_types();
|
|
1909
|
+
init_upload_helpers();
|
|
1910
|
+
init_params();
|
|
1911
|
+
VIDEO_STUDIO_ENGINES = {
|
|
1912
|
+
change: [
|
|
1913
|
+
"seedance-2-5-video-edit",
|
|
1914
|
+
"flux-video-edit",
|
|
1915
|
+
"kling-video-edit",
|
|
1916
|
+
"wan-video-edit",
|
|
1917
|
+
"seedance-video-edit",
|
|
1918
|
+
"happyhorse-1.0-video-edit",
|
|
1919
|
+
"xai-video-edit"
|
|
1920
|
+
],
|
|
1921
|
+
move: ["kling-motion-control", "wan-video-character-swap"],
|
|
1922
|
+
extend: ["seedance-2-5-video-edit", "wan-2.7-i2v", "grok-video-extend"]
|
|
1923
|
+
};
|
|
1924
|
+
VERBS = ["change", "move", "extend"];
|
|
1925
|
+
BACKWARD_ENGINES = /* @__PURE__ */ new Set(["seedance-2-5-video-edit"]);
|
|
1926
|
+
clamp = (n, min, max) => Math.min(max, Math.max(min, Math.round(n)));
|
|
1927
|
+
NEEDS_VIDEO_DURATION = /* @__PURE__ */ new Set([
|
|
1928
|
+
"flux-video-edit",
|
|
1929
|
+
"kling-video-edit",
|
|
1930
|
+
"wan-video-edit",
|
|
1931
|
+
"seedance-video-edit",
|
|
1932
|
+
"happyhorse-1.0-video-edit",
|
|
1933
|
+
"xai-video-edit",
|
|
1934
|
+
"kling-motion-control",
|
|
1935
|
+
"wan-video-character-swap"
|
|
1936
|
+
]);
|
|
1937
|
+
NO_IMAGE_ENGINES = /* @__PURE__ */ new Set(["flux-video-edit", "xai-video-edit"]);
|
|
1938
|
+
InputSchema9 = z.object({
|
|
1939
|
+
verb: z.enum(VERBS),
|
|
1940
|
+
prompt: z.string().max(8e3).optional(),
|
|
1941
|
+
video_url: z.string().url().optional(),
|
|
1942
|
+
video_path: z.string().min(1).optional(),
|
|
1943
|
+
image_url: z.string().url().optional(),
|
|
1944
|
+
image_urls: z.array(z.string().url()).optional(),
|
|
1945
|
+
image_path: z.string().min(1).optional(),
|
|
1946
|
+
model: z.string().optional(),
|
|
1947
|
+
video_duration: z.number().positive().optional(),
|
|
1948
|
+
duration: z.number().int().positive().optional(),
|
|
1949
|
+
direction: z.enum(["forward", "backward"]).optional(),
|
|
1950
|
+
params: ParamsInput
|
|
1951
|
+
});
|
|
1952
|
+
engineList = (verb) => `${verb}: ${VIDEO_STUDIO_ENGINES[verb].join(", ")} (default ${VIDEO_STUDIO_ENGINES[verb][0]})`;
|
|
1953
|
+
InputJsonSchema9 = {
|
|
1954
|
+
type: "object",
|
|
1955
|
+
properties: {
|
|
1956
|
+
verb: {
|
|
1957
|
+
type: "string",
|
|
1958
|
+
enum: VERBS,
|
|
1959
|
+
description: "change = restyle or edit an existing clip; move = put a character image into a driving video (motion transfer / character swap); extend = continue a clip forward (or backward on the default engine)."
|
|
1960
|
+
},
|
|
1961
|
+
prompt: {
|
|
1962
|
+
type: "string",
|
|
1963
|
+
description: "What to change, how the character should move, or what happens next. Required for change and extend. Optional for move (wan-video-character-swap ignores it)."
|
|
1964
|
+
},
|
|
1965
|
+
video_url: {
|
|
1966
|
+
type: "string",
|
|
1967
|
+
format: "uri",
|
|
1968
|
+
description: "The source clip (change, extend) or the driving motion clip (move). Required."
|
|
1969
|
+
},
|
|
1970
|
+
video_path: {
|
|
1971
|
+
type: "string",
|
|
1972
|
+
description: "Absolute local path to the source clip (MP4, WebM, MOV). Uploaded to 24h temp storage and used as video_url."
|
|
1973
|
+
},
|
|
1974
|
+
image_url: {
|
|
1975
|
+
type: "string",
|
|
1976
|
+
format: "uri",
|
|
1977
|
+
description: "The character image (move, required) or a reference image (change, optional; some engines take none)."
|
|
1978
|
+
},
|
|
1979
|
+
image_urls: {
|
|
1980
|
+
type: "array",
|
|
1981
|
+
items: { type: "string", format: "uri" },
|
|
1982
|
+
description: "More reference images for change engines that take a set. Move uses the first image only."
|
|
1983
|
+
},
|
|
1984
|
+
image_path: {
|
|
1985
|
+
type: "string",
|
|
1986
|
+
description: "Absolute local path to the character or reference image. Uploaded to 24h temp storage and used as image_url."
|
|
1987
|
+
},
|
|
1988
|
+
model: {
|
|
1989
|
+
type: "string",
|
|
1990
|
+
description: `Video Studio engine for this verb. Omit for the default. ${VERBS.map((v) => engineList(v)).join("; ")}. A model not on the verb's list is replaced by the default and the result says so.`
|
|
1991
|
+
},
|
|
1992
|
+
video_duration: {
|
|
1993
|
+
type: "number",
|
|
1994
|
+
description: "The source clip's length in seconds. Required by every engine except the change/extend default and grok-video-extend, since it sets their credit cost."
|
|
1995
|
+
},
|
|
1996
|
+
duration: {
|
|
1997
|
+
type: "integer",
|
|
1998
|
+
description: "Extend only: output length in seconds (default engine 4-30, wan-2.7-i2v 2-15 total), or seconds added on grok-video-extend (2-10). Ignored by change and move, which keep the source length."
|
|
1999
|
+
},
|
|
2000
|
+
direction: {
|
|
2001
|
+
type: "string",
|
|
2002
|
+
enum: ["forward", "backward"],
|
|
2003
|
+
description: "Extend only. backward adds footage before the clip; only the default engine supports it."
|
|
2004
|
+
},
|
|
2005
|
+
params: PARAMS_JSON_SCHEMA
|
|
2006
|
+
},
|
|
2007
|
+
required: ["verb"]
|
|
2008
|
+
};
|
|
2009
|
+
BASE_DESCRIPTION = "Video Studio verbs for an existing clip, on the same engines the studio uses. verb=change restyles or edits a clip (prompt + video_url, optional reference images). verb=move puts a character image into a driving video's motion (video_url + image_url + video_duration). verb=extend continues a clip forward, or backward on the default engine (prompt + video_url, optional duration and direction). Omit `model` for the studio default; engine-specific settings go in `params` with an explicit `model`. To make a new video, use pixeldojo_generate. Returns asset URLs, or a jobId for pixeldojo_status when the job outlasts the 30s wait.";
|
|
2010
|
+
videoSkill = {
|
|
2011
|
+
name: "pixeldojo_video",
|
|
2012
|
+
description: `${BASE_DESCRIPTION} Local files: video_path / image_path (uploaded to 24h temp storage first).`,
|
|
2013
|
+
hostedDescription: `${BASE_DESCRIPTION} Media must be public http(s) URLs.`,
|
|
2014
|
+
inputZod: InputSchema9,
|
|
2015
|
+
inputJsonSchema: InputJsonSchema9,
|
|
2016
|
+
allowedModels: Array.from(new Set(VERBS.flatMap((v) => VIDEO_STUDIO_ENGINES[v]))),
|
|
2017
|
+
defaultModel: VIDEO_STUDIO_ENGINES.change[0],
|
|
2018
|
+
run: run9
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
});
|
|
2022
|
+
async function episodeApi(ctx, method, path, body) {
|
|
2023
|
+
const res = await fetch(`${ctx.baseUrl}/api/faceless-studio/episodes${path}`, {
|
|
2024
|
+
method,
|
|
2025
|
+
headers: {
|
|
2026
|
+
Authorization: `Bearer ${ctx.apiKey}`,
|
|
2027
|
+
"Content-Type": "application/json",
|
|
2028
|
+
"x-pixeldojo-source": `mcp/${ctx.mcpVersion}`,
|
|
2029
|
+
"x-pixeldojo-skill": "episode"
|
|
2030
|
+
},
|
|
2031
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
2032
|
+
});
|
|
2033
|
+
const data = await res.json().catch(() => ({}));
|
|
2034
|
+
return { status: res.status, data };
|
|
2035
|
+
}
|
|
2036
|
+
function quoteText(ep, price, ctx) {
|
|
2037
|
+
const engines = [...new Set(ep.scenes.map((s) => s.engine))].join(", ");
|
|
2038
|
+
const seconds = ep.scenes.reduce((n, s) => n + s.shotSeconds, 0);
|
|
2039
|
+
return [
|
|
2040
|
+
`Planned "${ep.title}" (episode_id: ${ep.id}): ${ep.scenes.length} scenes, about ${Math.round(seconds)}s of picture, engine ${engines}.`,
|
|
2041
|
+
...ep.scenes.slice(0, 12).map((s, i) => ` ${i + 1}. ${s.narration.slice(0, 110)}`),
|
|
2042
|
+
...ep.scenes.length > 12 ? [` ... and ${ep.scenes.length - 12} more.`] : [],
|
|
2043
|
+
"",
|
|
2044
|
+
price ? `Nothing has been charged yet. Producing it costs about ${price.total} credits at list price: scenes ${price.scenes}, narration ${price.narration}, music ${price.music}. Each part is charged as it renders; an active sale can make it lower.` : "Nothing has been charged yet.",
|
|
2045
|
+
`Review or edit at ${ctx.baseUrl}/faceless-studio?episode=${ep.id}, or call ${call(ep.id, "produce")} to start production.`
|
|
2046
|
+
].join("\n");
|
|
2047
|
+
}
|
|
2048
|
+
async function waitForPlan(ctx, id) {
|
|
2049
|
+
const sleep = ctx.pollOverrides?.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
2050
|
+
for (let i = 0; i < PLAN_MAX_POLLS; i += 1) {
|
|
2051
|
+
await sleep(PLAN_POLL_MS);
|
|
2052
|
+
const { data } = await episodeApi(ctx, "GET", `/${id}`);
|
|
2053
|
+
if (data.episode?.plan.job?.status !== "running") return data;
|
|
2054
|
+
}
|
|
2055
|
+
return null;
|
|
2056
|
+
}
|
|
2057
|
+
async function setEngine(ctx, ep, engine) {
|
|
2058
|
+
const { data } = await episodeApi(ctx, "PATCH", `/${ep.id}`, {
|
|
2059
|
+
scenes: ep.scenes.map((s) => ({ id: s.id, narration: s.narration, visualPrompt: s.visualPrompt, engine }))
|
|
2060
|
+
});
|
|
2061
|
+
if (!data.episode) return {};
|
|
2062
|
+
return (await episodeApi(ctx, "GET", `/${ep.id}`)).data;
|
|
2063
|
+
}
|
|
2064
|
+
async function create(input, ctx) {
|
|
2065
|
+
if (!input.topic?.trim()) {
|
|
2066
|
+
return { kind: "text", text: "Provide a `topic` (8 to 400 characters) to create an episode." };
|
|
2067
|
+
}
|
|
2068
|
+
const created = await episodeApi(ctx, "POST", "", {
|
|
2069
|
+
topic: input.topic.trim(),
|
|
2070
|
+
category: input.category,
|
|
2071
|
+
format: input.aspect,
|
|
2072
|
+
targetSeconds: input.length_seconds,
|
|
2073
|
+
style: input.style,
|
|
2074
|
+
voiceId: input.voice,
|
|
2075
|
+
...input.title?.trim() ? { title: input.title.trim() } : {}
|
|
2076
|
+
});
|
|
2077
|
+
const ep = created.data.episode;
|
|
2078
|
+
if (!ep) return { kind: "text", text: `Could not create the episode: ${created.data.error ?? created.status}` };
|
|
2079
|
+
if (!input.music) await episodeApi(ctx, "PATCH", `/${ep.id}`, { musicEnabled: false });
|
|
2080
|
+
const plan2 = await episodeApi(ctx, "POST", `/${ep.id}/plan`, { scope: "episode" });
|
|
2081
|
+
if (plan2.status !== 202) {
|
|
2082
|
+
return { kind: "text", text: `Episode created (episode_id: ${ep.id}) but planning did not start: ${plan2.data.error ?? plan2.status}` };
|
|
2083
|
+
}
|
|
2084
|
+
let planned = await waitForPlan(ctx, ep.id);
|
|
2085
|
+
if (!planned) return { kind: "text", text: stillPlanning(ep.id) };
|
|
2086
|
+
if (!planned.episode || planned.episode.plan.job?.status === "error" || planned.episode.scenes.length === 0) {
|
|
2087
|
+
return { kind: "text", text: `Planning failed: ${planned.episode?.plan.job?.error ?? planned.error ?? "no scenes came back"}` };
|
|
2088
|
+
}
|
|
2089
|
+
if (input.engine) {
|
|
2090
|
+
const switched = await setEngine(ctx, planned.episode, input.engine);
|
|
2091
|
+
if (switched.episode) planned = switched;
|
|
2092
|
+
}
|
|
2093
|
+
return { kind: "text", text: quoteText(planned.episode, planned.price, ctx) };
|
|
2094
|
+
}
|
|
2095
|
+
function progress(ep) {
|
|
2096
|
+
const done = ep.scenes.filter((s) => s.render.status === "done" && s.render.url && !s.render.stale).length;
|
|
2097
|
+
const running = ep.scenes.filter((s) => s.render.status === "running").length;
|
|
2098
|
+
const narr = ep.narration?.audioUrl ? "recorded" : ep.narration?.status === "running" ? "recording" : "not recorded";
|
|
2099
|
+
const music = !ep.musicEnabled ? "off" : ep.music?.url ? "ready" : ep.music?.status ?? "not started";
|
|
2100
|
+
const exp = ep.export?.status ?? "not started";
|
|
2101
|
+
return `narration ${narr}, scenes ${done}/${ep.scenes.length}${running ? ` (${running} rendering)` : ""}, music ${music}, export ${exp}`;
|
|
2102
|
+
}
|
|
2103
|
+
async function status(id, ctx) {
|
|
2104
|
+
const { data, status: code } = await episodeApi(ctx, "GET", `/${id}`);
|
|
2105
|
+
const ep = data.episode;
|
|
2106
|
+
if (!ep) return { kind: "text", text: `Episode not found: ${data.error ?? code}` };
|
|
2107
|
+
if (ep.plan.job?.status === "running") return { kind: "text", text: stillPlanning(id) };
|
|
2108
|
+
if (ep.scenes.length === 0) {
|
|
2109
|
+
return { kind: "text", text: `"${ep.title}" has no scene plan yet${ep.plan.job?.error ? ` (${ep.plan.job.error})` : ""}. Create a new episode with action "create".` };
|
|
2110
|
+
}
|
|
2111
|
+
if (ep.export?.status === "done" && ep.export.url && ep.savedAt && !ep.export.stale) {
|
|
2112
|
+
return { kind: "text", text: `"${ep.title}" is finished: ${ep.export.url}` };
|
|
2113
|
+
}
|
|
2114
|
+
const quote = data.price ? ` Plan quote: about ${data.price.total} credits at list price.` : "";
|
|
2115
|
+
return { kind: "text", text: `"${ep.title}": ${progress(ep)}.${quote} Call ${call(id, "produce")} to keep production moving.` };
|
|
2116
|
+
}
|
|
2117
|
+
async function generate(ctx, id, path, label, regenerate = false) {
|
|
2118
|
+
const r = await episodeApi(ctx, "POST", `/${id}${path}`, regenerate ? { regenerate: true } : {});
|
|
2119
|
+
if (r.status === 200) {
|
|
2120
|
+
return { ok: true, line: `${label} started (${r.data.creditCost ?? "?"} credits).` };
|
|
2121
|
+
}
|
|
2122
|
+
return { ok: false, line: `${label} did not start: ${r.data.error ?? r.status}` };
|
|
2123
|
+
}
|
|
2124
|
+
async function produce2(id, ctx) {
|
|
2125
|
+
const { data, status: code } = await episodeApi(ctx, "GET", `/${id}`);
|
|
2126
|
+
const ep = data.episode;
|
|
2127
|
+
if (!ep) return { kind: "text", text: `Episode not found: ${data.error ?? code}` };
|
|
2128
|
+
if (ep.plan.job?.status === "running") return { kind: "text", text: stillPlanning(id) };
|
|
2129
|
+
if (ep.scenes.length === 0) {
|
|
2130
|
+
return { kind: "text", text: `"${ep.title}" has no scene plan to produce. Create a new episode with action "create".` };
|
|
2131
|
+
}
|
|
2132
|
+
if (!ep.narration?.audioUrl) {
|
|
2133
|
+
if (ep.narration?.status === "running") {
|
|
2134
|
+
return { kind: "text", text: `Narration is recording. ${next(id, "produce", "~20s")}` };
|
|
2135
|
+
}
|
|
2136
|
+
const g = await generate(ctx, id, "/narration/generate", "Narration");
|
|
2137
|
+
const failed = ep.narration?.status === "failed" ? ` (the last take failed: ${ep.narration.error ?? "unknown"})` : "";
|
|
2138
|
+
return { kind: "text", text: `${g.line}${failed} ${next(id, "produce", "~20s")}` };
|
|
2139
|
+
}
|
|
2140
|
+
const running = ep.scenes.filter((s) => s.render.status === "running").length;
|
|
2141
|
+
const queue = ep.scenes.filter(needsRender);
|
|
2142
|
+
if (queue.length > 0 || running > 0) {
|
|
2143
|
+
const lines = [];
|
|
2144
|
+
let spent = 0;
|
|
2145
|
+
for (const s of queue.slice(0, Math.max(0, MAX_IN_FLIGHT - running))) {
|
|
2146
|
+
const r = await episodeApi(ctx, "POST", `/${id}/scenes/${s.id}/generate`, {});
|
|
2147
|
+
if (r.status === 200) spent += r.data.creditCost ?? 0;
|
|
2148
|
+
else {
|
|
2149
|
+
lines.push(`Scene ${ep.scenes.indexOf(s) + 1} did not start: ${r.data.error ?? r.status}`);
|
|
2150
|
+
if (r.status === 402 || r.status === 403) break;
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
const failed = ep.scenes.filter((s) => s.render.status === "failed");
|
|
2154
|
+
return {
|
|
2155
|
+
kind: "text",
|
|
2156
|
+
text: [
|
|
2157
|
+
`In progress: ${progress(ep)}.`,
|
|
2158
|
+
spent > 0 ? `Started scene renders for ${Math.round(spent * 10) / 10} credits.` : "",
|
|
2159
|
+
failed.length > 0 ? `${failed.length} scene(s) failed and were re-queued (${failed[0]?.render.error?.slice(0, 100) ?? ""}).` : "",
|
|
2160
|
+
...lines,
|
|
2161
|
+
next(id, "produce", "~30s")
|
|
2162
|
+
].filter(Boolean).join("\n")
|
|
2163
|
+
};
|
|
2164
|
+
}
|
|
2165
|
+
if (ep.musicEnabled && !ep.music?.url && ep.music?.status !== "failed") {
|
|
2166
|
+
if (ep.music?.status === "running") {
|
|
2167
|
+
return { kind: "text", text: `All scenes rendered; the music bed is composing. ${next(id, "produce", "~20s")}` };
|
|
2168
|
+
}
|
|
2169
|
+
const g = await generate(ctx, id, "/music/generate", "Music bed");
|
|
2170
|
+
return { kind: "text", text: `All scenes rendered. ${g.line} ${next(id, "produce", "~20s")}` };
|
|
2171
|
+
}
|
|
2172
|
+
const exp = ep.export;
|
|
2173
|
+
if (exp?.status === "running") {
|
|
2174
|
+
return { kind: "text", text: `Export is stitching the episode. ${next(id, "produce", "~60s")}` };
|
|
2175
|
+
}
|
|
2176
|
+
if (exp?.status === "done" && exp.url && !exp.stale) {
|
|
2177
|
+
if (ep.savedAt) return { kind: "text", text: `"${ep.title}" is finished: ${exp.url}` };
|
|
2178
|
+
const saved = await episodeApi(ctx, "POST", `/${id}/save`, {});
|
|
2179
|
+
const url = saved.data.episode?.export?.url;
|
|
2180
|
+
if (saved.status !== 200 || !url) {
|
|
2181
|
+
return { kind: "text", text: `The export is ready but could not be saved: ${saved.data.error ?? saved.status}. The draft (kept 24h): ${exp.url}. ${next(id, "produce", "~30s")}` };
|
|
2182
|
+
}
|
|
2183
|
+
return { kind: "text", text: `"${ep.title}" is finished and saved to My Media: ${url}` };
|
|
2184
|
+
}
|
|
2185
|
+
const started = await episodeApi(ctx, "POST", `/${id}/export`, {});
|
|
2186
|
+
if (started.status !== 202) {
|
|
2187
|
+
return { kind: "text", text: `Export did not start: ${started.data.error ?? started.status}. ${next(id, "produce", "~30s")}` };
|
|
2188
|
+
}
|
|
2189
|
+
const prior = exp?.status === "failed" ? ` (the last export failed: ${exp.error ?? "unknown"})` : "";
|
|
2190
|
+
const musicNote = ep.musicEnabled && ep.music?.status === "failed" ? " The music bed failed, so this cut has no music." : "";
|
|
2191
|
+
return { kind: "text", text: `Export started, free${prior}.${musicNote} ${next(id, "produce", "~60s")}` };
|
|
2192
|
+
}
|
|
2193
|
+
async function run10(input, ctx) {
|
|
2194
|
+
const action = input.action === "create" && input.episode_id ? "status" : input.action;
|
|
2195
|
+
if (action === "create") return create(input, ctx);
|
|
2196
|
+
if (!input.episode_id) return { kind: "text", text: "produce and status need the episode_id returned by the create step." };
|
|
2197
|
+
if (action === "produce") return produce2(input.episode_id, ctx);
|
|
2198
|
+
return status(input.episode_id, ctx);
|
|
2199
|
+
}
|
|
2200
|
+
var STYLES, CATEGORIES, ENGINES, MIN_SECONDS, MAX_SECONDS, MAX_IN_FLIGHT, InputSchema10, InputJsonSchema10, call, next, needsRender, PLAN_POLL_MS, PLAN_MAX_POLLS, stillPlanning, episodeSkill;
|
|
2201
|
+
var init_episode = __esm({
|
|
2202
|
+
"src/skills/episode.ts"() {
|
|
2203
|
+
STYLES = ["editorial-motion", "paper-diorama", "hand-drawn", "storybook", "poster-vector", "soft-3d"];
|
|
2204
|
+
CATEGORIES = ["education", "history", "kids", "storytelling", "custom"];
|
|
2205
|
+
ENGINES = ["omni-draft", "omni-final", "still-motion"];
|
|
2206
|
+
MIN_SECONDS = 60;
|
|
2207
|
+
MAX_SECONDS = 300;
|
|
2208
|
+
MAX_IN_FLIGHT = 4;
|
|
2209
|
+
InputSchema10 = z.object({
|
|
2210
|
+
action: z.enum(["create", "produce", "status"]).default("create"),
|
|
2211
|
+
topic: z.string().min(8).max(400).optional(),
|
|
2212
|
+
episode_id: z.string().max(40).optional(),
|
|
2213
|
+
category: z.enum(CATEGORIES).default("education"),
|
|
2214
|
+
style: z.enum(STYLES).default("editorial-motion"),
|
|
2215
|
+
aspect: z.enum(["16:9", "9:16"]).default("16:9"),
|
|
2216
|
+
length_seconds: z.number().int().min(MIN_SECONDS).max(MAX_SECONDS).default(120),
|
|
2217
|
+
voice: z.string().min(1).max(80).default("Wise_Woman"),
|
|
2218
|
+
engine: z.enum(ENGINES).optional(),
|
|
2219
|
+
music: z.boolean().default(true),
|
|
2220
|
+
title: z.string().max(120).optional()
|
|
2221
|
+
});
|
|
2222
|
+
InputJsonSchema10 = {
|
|
2223
|
+
type: "object",
|
|
2224
|
+
properties: {
|
|
2225
|
+
action: {
|
|
2226
|
+
type: "string",
|
|
2227
|
+
enum: ["create", "produce", "status"],
|
|
2228
|
+
description: "create (default, free): writes the script and scene plan, returns episode_id and the credit quote. produce (charged): advances production one step (narration, scene renders, music, export, save) and reports what that step cost; call it every ~30s until it returns the finished video URL. status (free): progress only."
|
|
2229
|
+
},
|
|
2230
|
+
topic: { type: "string", description: "Create step: what the episode is about, 8 to 400 characters." },
|
|
2231
|
+
episode_id: { type: "string", description: "The episode_id from the create step. Required for produce and status." },
|
|
2232
|
+
category: { type: "string", enum: [...CATEGORIES], description: "Create step: tone of the script. Default education." },
|
|
2233
|
+
style: { type: "string", enum: [...STYLES], description: "Create step: visual style of every scene. Default editorial-motion." },
|
|
2234
|
+
aspect: { type: "string", enum: ["16:9", "9:16"], description: "Create step: 16:9 landscape (default) or 9:16 vertical." },
|
|
2235
|
+
length_seconds: { type: "integer", minimum: MIN_SECONDS, maximum: MAX_SECONDS, description: "Create step: target length in seconds. Default 120." },
|
|
2236
|
+
voice: { type: "string", description: "Create step: narrator voice id from the text-to-speech tool (e.g. Wise_Woman, Deep_Voice_Man, Friendly_Person). Default Wise_Woman." },
|
|
2237
|
+
engine: {
|
|
2238
|
+
type: "string",
|
|
2239
|
+
enum: [...ENGINES],
|
|
2240
|
+
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."
|
|
2241
|
+
},
|
|
2242
|
+
music: { type: "boolean", description: "Create step: add a music bed under the narration. Default true." },
|
|
2243
|
+
title: { type: "string", description: "Create step (optional): episode title. Defaults to the topic." }
|
|
2244
|
+
}
|
|
2245
|
+
};
|
|
2246
|
+
call = (id, action) => `pixeldojo_episode with {"episode_id": "${id}", "action": "${action}"}`;
|
|
2247
|
+
next = (id, action, wait) => `Call ${call(id, action)} in ${wait}.`;
|
|
2248
|
+
needsRender = (s) => s.render.status !== "running" && (s.render.status === "idle" || s.render.status === "failed" || Boolean(s.render.stale) || !s.render.url);
|
|
2249
|
+
PLAN_POLL_MS = 3e3;
|
|
2250
|
+
PLAN_MAX_POLLS = 8;
|
|
2251
|
+
stillPlanning = (id) => `The script is still being written (episode_id: ${id}). Nothing has been charged. ${next(id, "status", "~30s")}`;
|
|
2252
|
+
episodeSkill = {
|
|
2253
|
+
name: "pixeldojo_episode",
|
|
2254
|
+
description: "Make a finished narrated faceless video (explainer, history, kids story) from one topic: script, scene visuals, AI narration, music bed, stitched export saved to My Media. Phased: create is free and returns a credit quote; produce is charged and advances one step per call (poll every ~30s until it returns the video URL); status is free.",
|
|
2255
|
+
inputZod: InputSchema10,
|
|
2256
|
+
inputJsonSchema: InputJsonSchema10,
|
|
2257
|
+
allowedModels: [],
|
|
2258
|
+
defaultModel: "",
|
|
2259
|
+
run: run10
|
|
2260
|
+
};
|
|
2261
|
+
}
|
|
2262
|
+
});
|
|
2263
|
+
async function run11(input, ctx) {
|
|
1630
2264
|
const productImages = [
|
|
1631
2265
|
...input.product_image_urls ?? [],
|
|
1632
2266
|
...await Promise.all((input.product_image_paths ?? []).map((p) => uploadLocalFile(p, ctx)))
|
|
@@ -1669,13 +2303,13 @@ async function run9(input, ctx) {
|
|
|
1669
2303
|
(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]}`
|
|
1670
2304
|
);
|
|
1671
2305
|
}
|
|
1672
|
-
var
|
|
2306
|
+
var InputSchema11, InputJsonSchema11, adSkill;
|
|
1673
2307
|
var init_ad = __esm({
|
|
1674
2308
|
"src/skills/ad.ts"() {
|
|
1675
2309
|
init_types();
|
|
1676
2310
|
init_poll();
|
|
1677
2311
|
init_upload_helpers();
|
|
1678
|
-
|
|
2312
|
+
InputSchema11 = z.object({
|
|
1679
2313
|
style: z.string().max(80).optional().describe(
|
|
1680
2314
|
"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."
|
|
1681
2315
|
),
|
|
@@ -1701,7 +2335,7 @@ var init_ad = __esm({
|
|
|
1701
2335
|
path: ["product_url"]
|
|
1702
2336
|
}
|
|
1703
2337
|
);
|
|
1704
|
-
|
|
2338
|
+
InputJsonSchema11 = {
|
|
1705
2339
|
type: "object",
|
|
1706
2340
|
properties: {
|
|
1707
2341
|
style: {
|
|
@@ -1788,16 +2422,16 @@ var init_ad = __esm({
|
|
|
1788
2422
|
name: "pixeldojo_ad",
|
|
1789
2423
|
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.",
|
|
1790
2424
|
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.",
|
|
1791
|
-
inputZod:
|
|
1792
|
-
inputJsonSchema:
|
|
2425
|
+
inputZod: InputSchema11,
|
|
2426
|
+
inputJsonSchema: InputJsonSchema11,
|
|
1793
2427
|
// Composition skill — the server routes to the ad engine internally.
|
|
1794
2428
|
allowedModels: [],
|
|
1795
2429
|
defaultModel: "",
|
|
1796
|
-
run:
|
|
2430
|
+
run: run11
|
|
1797
2431
|
};
|
|
1798
2432
|
}
|
|
1799
2433
|
});
|
|
1800
|
-
async function
|
|
2434
|
+
async function run12(input, ctx) {
|
|
1801
2435
|
const client = ctx.client;
|
|
1802
2436
|
const result = await client.runCampaign(input);
|
|
1803
2437
|
if (result.kind === "campaign") {
|
|
@@ -1824,10 +2458,10 @@ async function run10(input, ctx) {
|
|
|
1824
2458
|
statusUrl: result.statusUrl ?? ctx.buildStatusUrl(result.campaignId)
|
|
1825
2459
|
};
|
|
1826
2460
|
}
|
|
1827
|
-
var
|
|
2461
|
+
var InputSchema12, InputJsonSchema12, campaignSkill;
|
|
1828
2462
|
var init_campaign = __esm({
|
|
1829
2463
|
"src/skills/campaign.ts"() {
|
|
1830
|
-
|
|
2464
|
+
InputSchema12 = z.object({
|
|
1831
2465
|
productUrl: z.string().url().optional().describe("Product page URL. We extract name + description + images automatically."),
|
|
1832
2466
|
product: z.object({
|
|
1833
2467
|
name: z.string().min(1).max(200),
|
|
@@ -1843,7 +2477,7 @@ var init_campaign = __esm({
|
|
|
1843
2477
|
message: "productUrl or product is required",
|
|
1844
2478
|
path: ["productUrl"]
|
|
1845
2479
|
});
|
|
1846
|
-
|
|
2480
|
+
InputJsonSchema12 = {
|
|
1847
2481
|
type: "object",
|
|
1848
2482
|
properties: {
|
|
1849
2483
|
productUrl: {
|
|
@@ -1894,17 +2528,17 @@ var init_campaign = __esm({
|
|
|
1894
2528
|
campaignSkill = {
|
|
1895
2529
|
name: "pixeldojo_campaign",
|
|
1896
2530
|
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.",
|
|
1897
|
-
inputZod:
|
|
1898
|
-
inputJsonSchema:
|
|
2531
|
+
inputZod: InputSchema12,
|
|
2532
|
+
inputJsonSchema: InputJsonSchema12,
|
|
1899
2533
|
// Composition skill — internal sub-jobs route to per-tool models, no
|
|
1900
2534
|
// single allowed-models list applies here.
|
|
1901
2535
|
allowedModels: [],
|
|
1902
2536
|
defaultModel: "",
|
|
1903
|
-
run:
|
|
2537
|
+
run: run12
|
|
1904
2538
|
};
|
|
1905
2539
|
}
|
|
1906
2540
|
});
|
|
1907
|
-
async function
|
|
2541
|
+
async function run13(input, ctx) {
|
|
1908
2542
|
const client = ctx.client;
|
|
1909
2543
|
const result = await client.getCampaign(input.campaignId);
|
|
1910
2544
|
if (result.kind === "campaign") {
|
|
@@ -1934,13 +2568,13 @@ async function run11(input, ctx) {
|
|
|
1934
2568
|
statusUrl: result.statusUrl ?? ctx.buildStatusUrl(result.campaignId)
|
|
1935
2569
|
};
|
|
1936
2570
|
}
|
|
1937
|
-
var
|
|
2571
|
+
var InputSchema13, InputJsonSchema13, campaignStatusSkill;
|
|
1938
2572
|
var init_campaign_status = __esm({
|
|
1939
2573
|
"src/skills/campaign-status.ts"() {
|
|
1940
|
-
|
|
2574
|
+
InputSchema13 = z.object({
|
|
1941
2575
|
campaignId: z.string().min(1).max(200).describe("Campaign id returned from a prior pixeldojo_campaign handoff.")
|
|
1942
2576
|
});
|
|
1943
|
-
|
|
2577
|
+
InputJsonSchema13 = {
|
|
1944
2578
|
type: "object",
|
|
1945
2579
|
properties: {
|
|
1946
2580
|
campaignId: {
|
|
@@ -1953,15 +2587,15 @@ var init_campaign_status = __esm({
|
|
|
1953
2587
|
campaignStatusSkill = {
|
|
1954
2588
|
name: "pixeldojo_campaign_status",
|
|
1955
2589
|
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.",
|
|
1956
|
-
inputZod:
|
|
1957
|
-
inputJsonSchema:
|
|
2590
|
+
inputZod: InputSchema13,
|
|
2591
|
+
inputJsonSchema: InputJsonSchema13,
|
|
1958
2592
|
allowedModels: [],
|
|
1959
2593
|
defaultModel: "",
|
|
1960
|
-
run:
|
|
2594
|
+
run: run13
|
|
1961
2595
|
};
|
|
1962
2596
|
}
|
|
1963
2597
|
});
|
|
1964
|
-
async function
|
|
2598
|
+
async function run14(input, ctx) {
|
|
1965
2599
|
let url;
|
|
1966
2600
|
try {
|
|
1967
2601
|
url = await uploadLocalFile(input.path, ctx);
|
|
@@ -1974,40 +2608,40 @@ async function run12(input, ctx) {
|
|
|
1974
2608
|
return {
|
|
1975
2609
|
kind: "text",
|
|
1976
2610
|
text: `Uploaded ${basename(input.path)} \u2192 ${url}
|
|
1977
|
-
(temp storage; auto-deletes in ~24h. Pass this URL as image_url,
|
|
2611
|
+
(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.)`
|
|
1978
2612
|
};
|
|
1979
2613
|
}
|
|
1980
|
-
var
|
|
2614
|
+
var InputSchema14, InputJsonSchema14, uploadSkill;
|
|
1981
2615
|
var init_upload = __esm({
|
|
1982
2616
|
"src/skills/upload.ts"() {
|
|
1983
2617
|
init_upload_helpers();
|
|
1984
|
-
|
|
2618
|
+
InputSchema14 = z.object({
|
|
1985
2619
|
path: z.string().min(1).max(4096).describe(
|
|
1986
|
-
"Absolute local file path to upload. JPEG, PNG, WebP, GIF (\u226450MB)
|
|
2620
|
+
"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."
|
|
1987
2621
|
)
|
|
1988
2622
|
});
|
|
1989
|
-
|
|
2623
|
+
InputJsonSchema14 = {
|
|
1990
2624
|
type: "object",
|
|
1991
2625
|
properties: {
|
|
1992
2626
|
path: {
|
|
1993
2627
|
type: "string",
|
|
1994
|
-
description: "Absolute local file path. Allowed: JPEG, PNG, WebP, GIF (\u226450MB)
|
|
2628
|
+
description: "Absolute local file path. Allowed: 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; auto-expires after 24h."
|
|
1995
2629
|
}
|
|
1996
2630
|
},
|
|
1997
2631
|
required: ["path"]
|
|
1998
2632
|
};
|
|
1999
2633
|
uploadSkill = {
|
|
2000
2634
|
name: "pixeldojo_upload",
|
|
2001
|
-
description: "Upload a local image or
|
|
2002
|
-
inputZod:
|
|
2003
|
-
inputJsonSchema:
|
|
2635
|
+
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.",
|
|
2636
|
+
inputZod: InputSchema14,
|
|
2637
|
+
inputJsonSchema: InputJsonSchema14,
|
|
2004
2638
|
allowedModels: [],
|
|
2005
2639
|
defaultModel: "",
|
|
2006
|
-
run:
|
|
2640
|
+
run: run14
|
|
2007
2641
|
};
|
|
2008
2642
|
}
|
|
2009
2643
|
});
|
|
2010
|
-
async function
|
|
2644
|
+
async function run15(input, ctx) {
|
|
2011
2645
|
const requested = input.model ?? DEFAULT_MODEL6;
|
|
2012
2646
|
if (!ALLOWED4.includes(requested)) {
|
|
2013
2647
|
return {
|
|
@@ -2045,14 +2679,14 @@ async function run13(input, ctx) {
|
|
|
2045
2679
|
(urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Audio ready. Asset: ${urls[0]}`
|
|
2046
2680
|
);
|
|
2047
2681
|
}
|
|
2048
|
-
var ALLOWED4, DEFAULT_MODEL6,
|
|
2682
|
+
var ALLOWED4, DEFAULT_MODEL6, InputSchema15, InputJsonSchema15, audioSkill;
|
|
2049
2683
|
var init_audio = __esm({
|
|
2050
2684
|
"src/skills/audio.ts"() {
|
|
2051
2685
|
init_poll();
|
|
2052
2686
|
init_types();
|
|
2053
2687
|
ALLOWED4 = ["seed-audio", "text-to-speech"];
|
|
2054
2688
|
DEFAULT_MODEL6 = "seed-audio";
|
|
2055
|
-
|
|
2689
|
+
InputSchema15 = z.object({
|
|
2056
2690
|
prompt: z.string().min(1).describe(
|
|
2057
2691
|
"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."
|
|
2058
2692
|
),
|
|
@@ -2062,7 +2696,7 @@ var init_audio = __esm({
|
|
|
2062
2696
|
output_format: z.string().optional().describe("Output format. seed-audio: mp3 (default), wav, pcm, ogg_opus. text-to-speech: mp3 (default), wav, flac, pcm."),
|
|
2063
2697
|
speed: z.number().min(0.5).max(2).optional().describe("Speech speed multiplier, 0.5-2 (default 1). Both models.")
|
|
2064
2698
|
});
|
|
2065
|
-
|
|
2699
|
+
InputJsonSchema15 = {
|
|
2066
2700
|
type: "object",
|
|
2067
2701
|
properties: {
|
|
2068
2702
|
prompt: {
|
|
@@ -2100,15 +2734,15 @@ var init_audio = __esm({
|
|
|
2100
2734
|
audioSkill = {
|
|
2101
2735
|
name: "pixeldojo_audio",
|
|
2102
2736
|
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.",
|
|
2103
|
-
inputZod:
|
|
2104
|
-
inputJsonSchema:
|
|
2737
|
+
inputZod: InputSchema15,
|
|
2738
|
+
inputJsonSchema: InputJsonSchema15,
|
|
2105
2739
|
allowedModels: [...ALLOWED4],
|
|
2106
2740
|
defaultModel: DEFAULT_MODEL6,
|
|
2107
|
-
run:
|
|
2741
|
+
run: run15
|
|
2108
2742
|
};
|
|
2109
2743
|
}
|
|
2110
2744
|
});
|
|
2111
|
-
async function
|
|
2745
|
+
async function run16(input, ctx) {
|
|
2112
2746
|
const res = await ctx.client.listLibrary({
|
|
2113
2747
|
q: input.q,
|
|
2114
2748
|
model: input.model,
|
|
@@ -2139,16 +2773,16 @@ ${lines.join("\n")}
|
|
|
2139
2773
|
Pass any URL back into pixeldojo_generate, pixeldojo_edit, pixeldojo_character, or pixeldojo_upscale as a reference input.`
|
|
2140
2774
|
};
|
|
2141
2775
|
}
|
|
2142
|
-
var
|
|
2776
|
+
var InputSchema16, InputJsonSchema16, librarySkill;
|
|
2143
2777
|
var init_library = __esm({
|
|
2144
2778
|
"src/skills/library.ts"() {
|
|
2145
|
-
|
|
2779
|
+
InputSchema16 = z.object({
|
|
2146
2780
|
q: z.string().optional().describe("Substring match against the prompt that produced the asset."),
|
|
2147
2781
|
model: z.string().optional().describe("Restrict to a single model (apiId)."),
|
|
2148
2782
|
modality: z.enum(["image", "video", "audio"]).optional().describe("Restrict to one output type."),
|
|
2149
2783
|
limit: z.number().int().min(1).max(50).optional().describe("Max results (default 10).")
|
|
2150
2784
|
});
|
|
2151
|
-
|
|
2785
|
+
InputJsonSchema16 = {
|
|
2152
2786
|
type: "object",
|
|
2153
2787
|
properties: {
|
|
2154
2788
|
q: {
|
|
@@ -2168,11 +2802,11 @@ var init_library = __esm({
|
|
|
2168
2802
|
librarySkill = {
|
|
2169
2803
|
name: "pixeldojo_library",
|
|
2170
2804
|
description: "Search your media library: saved My Media items (permanent) and recent API/MCP generations (deleted 1 hour after they finish), newest first. Returns asset URLs to reuse as reference inputs.",
|
|
2171
|
-
inputZod:
|
|
2172
|
-
inputJsonSchema:
|
|
2805
|
+
inputZod: InputSchema16,
|
|
2806
|
+
inputJsonSchema: InputJsonSchema16,
|
|
2173
2807
|
allowedModels: [],
|
|
2174
2808
|
defaultModel: "",
|
|
2175
|
-
run:
|
|
2809
|
+
run: run16
|
|
2176
2810
|
};
|
|
2177
2811
|
}
|
|
2178
2812
|
});
|
|
@@ -2422,6 +3056,7 @@ __export(skills_exports, {
|
|
|
2422
3056
|
campaignStatusSkill: () => campaignStatusSkill,
|
|
2423
3057
|
characterSkill: () => characterSkill,
|
|
2424
3058
|
editSkill: () => editSkill,
|
|
3059
|
+
episodeSkill: () => episodeSkill,
|
|
2425
3060
|
filmSkill: () => filmSkill,
|
|
2426
3061
|
findHostedSkill: () => findHostedSkill,
|
|
2427
3062
|
findSkill: () => findSkill,
|
|
@@ -2434,7 +3069,8 @@ __export(skills_exports, {
|
|
|
2434
3069
|
statusSkill: () => statusSkill,
|
|
2435
3070
|
storyboardSkill: () => storyboardSkill,
|
|
2436
3071
|
uploadSkill: () => uploadSkill,
|
|
2437
|
-
upscaleSkill: () => upscaleSkill
|
|
3072
|
+
upscaleSkill: () => upscaleSkill,
|
|
3073
|
+
videoSkill: () => videoSkill
|
|
2438
3074
|
});
|
|
2439
3075
|
function findHostedSkill(name) {
|
|
2440
3076
|
const wanted = name.replace(/^pixeldojo:/, "pixeldojo_");
|
|
@@ -2455,6 +3091,8 @@ var init_skills = __esm({
|
|
|
2455
3091
|
init_status();
|
|
2456
3092
|
init_from_url();
|
|
2457
3093
|
init_film();
|
|
3094
|
+
init_video();
|
|
3095
|
+
init_episode();
|
|
2458
3096
|
init_ad();
|
|
2459
3097
|
init_campaign();
|
|
2460
3098
|
init_campaign_status();
|
|
@@ -2479,7 +3117,9 @@ var init_skills = __esm({
|
|
|
2479
3117
|
adSkill,
|
|
2480
3118
|
campaignSkill,
|
|
2481
3119
|
campaignStatusSkill,
|
|
2482
|
-
filmSkill
|
|
3120
|
+
filmSkill,
|
|
3121
|
+
videoSkill,
|
|
3122
|
+
episodeSkill
|
|
2483
3123
|
];
|
|
2484
3124
|
isLocalPathField = (key) => /_paths?$/.test(key);
|
|
2485
3125
|
HOSTED_SKILLS = ALL_SKILLS.filter(
|
|
@@ -2548,7 +3188,7 @@ function makeRedactedLogger(target = console) {
|
|
|
2548
3188
|
}
|
|
2549
3189
|
|
|
2550
3190
|
// src/server.ts
|
|
2551
|
-
var PACKAGE_VERSION = "0.
|
|
3191
|
+
var PACKAGE_VERSION = "0.15.1" ;
|
|
2552
3192
|
var SERVER_INSTRUCTIONS = "PixelDojo generates images, video, audio and short films across 100+ models, billed in credits from the connected PixelDojo 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, so download anything the user wants to keep. Finished films are the exception and stay in the account library.";
|
|
2553
3193
|
function buildServer(options) {
|
|
2554
3194
|
if (!options.apiKey) {
|