@pixeldojo/mcp 0.13.1 → 0.15.0

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