@pixeldojo/mcp 0.16.3 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -56,7 +56,7 @@ Restart your agent. You'll see 19 new tools.
56
56
  | `pixeldojo_ad` | Marketing Studio: a product video ad from a URL or product images. |
57
57
  | `pixeldojo_upscale` | Enhance any image (up to 16×) or video (up to 4K). |
58
58
  | `pixeldojo_audio` | Full audio scenes: dialogue + effects + music in one clip (Seed Audio 1.0). Reference clips are hosted URLs; local audio files are not accepted. |
59
- | `pixeldojo_library` | Search your media library: saved My Media + recent generations. |
59
+ | `pixeldojo_library` | Search your media library: saved Library items + recent generations. |
60
60
  | `pixeldojo_save_workflow` / `run_workflow` / `list_workflows` | Save a multi-step chain once, run it by name from any chat. |
61
61
  | `pixeldojo_upload` | Local file → 24h temp URL, for reference inputs. |
62
62
  | `pixeldojo_from_url` | Product page URL → structured product info. Free. |
package/dist/cli.cjs CHANGED
@@ -1455,6 +1455,39 @@ async function filmApi(ctx, method, path, body) {
1455
1455
  const data = await res.json().catch(() => ({}));
1456
1456
  return { status: res.status, data };
1457
1457
  }
1458
+ async function applyCast(ctx, filmId, cast) {
1459
+ let charged = false;
1460
+ const lines = await Promise.all(cast.map(async (c) => {
1461
+ const r = await filmApi(
1462
+ ctx,
1463
+ "POST",
1464
+ `/api/films/${filmId}/cast`,
1465
+ {
1466
+ role: c.name,
1467
+ ...c.character_id ? { characterId: c.character_id } : {},
1468
+ ...c.image_urls ? { imageUrls: c.image_urls } : {},
1469
+ ...c.prompt ? { prompt: c.prompt } : {}
1470
+ }
1471
+ );
1472
+ if (r.status === 200 && r.data.creditCost) charged = true;
1473
+ if (r.status !== 200) return `Could not cast ${c.name}: ${r.data.error ?? r.status}`;
1474
+ if (r.data.alreadyCast) return `${c.name} was already cast; nothing changed.`;
1475
+ return `Cast ${c.name}${r.data.creditCost ? ` (portrait, ${r.data.creditCost} credits)` : ""}${r.data.boundShots ? `, in ${r.data.boundShots} shot(s)` : ""}.`;
1476
+ }));
1477
+ return { lines, charged };
1478
+ }
1479
+ async function uncastRolesOf(ctx, filmId) {
1480
+ const r = await filmApi(ctx, "GET", `/api/films/${filmId}/cast`);
1481
+ return r.data.uncastRoles ?? [];
1482
+ }
1483
+ function castNeededText(filmId, roles) {
1484
+ const example = roles.map((r) => ({ name: r.name, prompt: r.suggestedPrompt || `describe ${r.name}` }));
1485
+ return [
1486
+ `Cast these characters before production (every named character needs a cast member so faces, ages and species hold in every shot):`,
1487
+ ...roles.map((r) => ` ${r.name}${r.suggestedPrompt ? `: ${r.suggestedPrompt}` : ""}`),
1488
+ `Call pixeldojo_film with {"film_id": "${filmId}", "action": "cast", "cast": ${JSON.stringify(example)}}, giving each one a prompt (a portrait is generated, charged), image_urls of your own photos, or a character_id from Character Studio.`
1489
+ ].join("\n");
1490
+ }
1458
1491
  function progressLine(f) {
1459
1492
  const all = shots(f);
1460
1493
  const frames = all.filter((sh) => ["frame-ready", "approved", "clip-generating", "clip-ready"].includes(sh.status) || sh.frameUrl).length;
@@ -1528,32 +1561,44 @@ async function plan(input, ctx) {
1528
1561
  });
1529
1562
  if (!created.data.film) return { kind: "text", text: `Could not create the film: ${created.data.error ?? created.status}` };
1530
1563
  const id = created.data.film.id;
1564
+ const castStarted = Date.now();
1565
+ const cast = input.cast?.length ? await applyCast(ctx, id, input.cast) : { lines: [], charged: false };
1566
+ const pollBudget = Math.max(0, PLANNER_MAX_POLLS - Math.ceil((Date.now() - castStarted) / PLANNER_POLL_MS));
1567
+ const reply = (text) => ({
1568
+ kind: "text",
1569
+ text: [
1570
+ ...cast.lines,
1571
+ cast.charged ? text.replace(/Nothing has been charged( yet)?\./g, "Only the cast portraits above have been charged.") : text
1572
+ ].join("\n")
1573
+ });
1531
1574
  let planned;
1532
1575
  if (music) {
1533
1576
  const brief = [input.idea?.trim() || input.script.trim(), input.lyrics?.trim() ? `Lyrics:
1534
1577
  ${input.lyrics.trim()}` : ""].filter(Boolean).join("\n\n");
1535
- planned = await runPlanner(ctx, id, `/api/films/${id}/plan`, { script: brief });
1578
+ planned = await runPlanner(ctx, id, `/api/films/${id}/plan`, { script: brief }, pollBudget);
1536
1579
  } else {
1537
1580
  let script = input.script?.trim() ?? "";
1538
1581
  if (!script) {
1539
- const wrote = await runPlanner(ctx, id, `/api/films/${id}/script`, { idea: input.idea.trim() });
1540
- if ("pending" in wrote) return { kind: "text", text: stillPlanningText(id, "script") };
1541
- if ("error" in wrote) return { kind: "text", text: `Script writing failed: ${wrote.error}` };
1582
+ const wrote = await runPlanner(ctx, id, `/api/films/${id}/script`, { idea: input.idea.trim() }, pollBudget);
1583
+ if ("pending" in wrote) return reply(stillPlanningText(id, "script"));
1584
+ if ("error" in wrote) return reply(`Script writing failed: ${wrote.error}`);
1542
1585
  script = wrote.film.script ?? "";
1543
- if (!script) return { kind: "text", text: "Script writing failed: the studio returned an empty script." };
1586
+ if (!script) return reply("Script writing failed: the studio returned an empty script.");
1544
1587
  }
1545
1588
  planned = await runPlanner(ctx, id, `/api/films/${id}/plan`, { script });
1546
1589
  }
1547
- if ("pending" in planned) return { kind: "text", text: stillPlanningText(id) };
1548
- if ("error" in planned) return { kind: "text", text: `Shot planning failed: ${planned.error}` };
1590
+ if ("pending" in planned) return reply(stillPlanningText(id));
1591
+ if ("error" in planned) return reply(`Shot planning failed: ${planned.error}`);
1549
1592
  const film = planned.film;
1550
1593
  if (needsSlice(film)) {
1551
1594
  const err = await sliceSong(ctx, film);
1552
- if (err) return { kind: "text", text: `${plannedText(film, ctx)}
1595
+ if (err) return reply(`${plannedText(film, ctx)}
1553
1596
 
1554
- ${err} Call action "status" to retry the cut.` };
1597
+ ${err} Call action "status" to retry the cut.`);
1555
1598
  }
1556
- return { kind: "text", text: plannedText(film, ctx) };
1599
+ const uncast = await uncastRolesOf(ctx, id);
1600
+ return reply([plannedText(film, ctx), uncast.length ? `
1601
+ ${castNeededText(id, uncast)}` : ""].filter(Boolean).join("\n"));
1557
1602
  }
1558
1603
  async function resumePlanning(film, ctx) {
1559
1604
  if (film.scenes.length > 0 && film.llmJob?.status !== "running") return null;
@@ -1580,6 +1625,8 @@ async function produce(filmId, ctx) {
1580
1625
  if (!data.film) return { kind: "text", text: `Film not found: ${data.error ?? filmId}` };
1581
1626
  const planning = await resumePlanning(data.film, ctx);
1582
1627
  if (planning) return planning;
1628
+ const uncast = await uncastRolesOf(ctx, filmId);
1629
+ if (uncast.length) return { kind: "text", text: `Nothing was charged. ${castNeededText(filmId, uncast)}` };
1583
1630
  const pending = shots(data.film).filter((sh) => sh.status === "idle" || sh.status === "failed" && !sh.frameUrl);
1584
1631
  let fired = 0;
1585
1632
  for (const sh of pending.slice(0, 6)) {
@@ -1653,19 +1700,27 @@ It is saved in the Library folder "Film: ${film.title}".`
1653
1700
  if (shots(film).length > 0 && shots(film).every((sh) => sh.status === "idle")) {
1654
1701
  return { kind: "text", text: plannedText(film, ctx) };
1655
1702
  }
1656
- const idleFrames = all.filter((sh) => sh.status === "idle");
1657
- for (const sh of idleFrames.slice(0, 6)) {
1658
- await filmApi(ctx, "POST", `/api/films/${filmId}/shots/${sh.id}/generate`, { kind: "frame" });
1659
- }
1660
- const framesDone = all.every((sh) => ["frame-ready", "clip-generating", "clip-ready"].includes(sh.status));
1661
- if (framesDone && all.some((sh) => sh.status === "frame-ready" && needsSave(sh.frameUrl))) {
1703
+ let started = 0;
1704
+ const resent = /* @__PURE__ */ new Set();
1705
+ const dispatch = async (sh, kind) => {
1706
+ const r = await filmApi(ctx, "POST", `/api/films/${filmId}/shots/${sh.id}/generate`, { kind });
1707
+ if (r.status !== 200) return;
1708
+ started += 1;
1709
+ if (sh.status === "failed") resent.add(sh.id);
1710
+ };
1711
+ const pendingFrames = all.filter((sh) => sh.status === "idle" || sh.status === "failed" && !sh.frameUrl && !isRefused(sh));
1712
+ if (pendingFrames.length > 0) {
1713
+ const uncast = await uncastRolesOf(ctx, filmId);
1714
+ if (uncast.length) return { kind: "text", text: castNeededText(filmId, uncast) };
1715
+ }
1716
+ for (const sh of pendingFrames.slice(0, 6)) await dispatch(sh, "frame");
1717
+ const framesDone = all.every(frameDone);
1718
+ if (all.some((sh) => frameDone(sh) && needsSave(sh.frameUrl))) {
1662
1719
  await filmApi(ctx, "POST", `/api/films/${filmId}/save`, { kind: "frame" });
1663
1720
  }
1664
1721
  const inFlight = count(film, (sh) => sh.status === "clip-generating");
1665
- const clipQueue = all.filter((sh) => (sh.status === "frame-ready" || sh.status === "failed" && Boolean(sh.frameUrl)) && framesDone);
1666
- for (const sh of clipQueue.slice(0, Math.max(0, 4 - inFlight))) {
1667
- await filmApi(ctx, "POST", `/api/films/${filmId}/shots/${sh.id}/generate`, { kind: "clip" });
1668
- }
1722
+ const clipQueue = framesDone ? all.filter((sh) => sh.status === "frame-ready" || sh.status === "failed" && frameDone(sh) && !isRefused(sh)) : [];
1723
+ for (const sh of clipQueue.slice(0, Math.max(0, 4 - inFlight))) await dispatch(sh, "clip");
1669
1724
  const clipsDone = all.length > 0 && all.every((sh) => sh.status === "clip-ready");
1670
1725
  if (clipsDone) {
1671
1726
  if (all.some((sh) => needsSave(sh.videoUrl))) {
@@ -1688,30 +1743,53 @@ It is saved in the Library folder "Film: ${film.title}".`
1688
1743
  return { kind: "text", text: `Score still composing. Poll again in ~20s with {"film_id": "${filmId}", "action": "status", "music_job_id": "${musicJobId}"}.` };
1689
1744
  }
1690
1745
  const body = soundtrack === "scene-synced" ? buildScorePlanBody(film) : buildFlatMusicBody(film, all);
1691
- const dispatch = await ctx.client.run("text-to-music", body);
1692
- return { kind: "text", text: `All ${all.length} clips ready \u2014 the ${soundtrack} score is composing (charged like any music generation). Poll again in ~20s with {"film_id": "${filmId}", "action": "status", "music_job_id": "${dispatch.jobId}"}.` };
1746
+ const dispatch2 = await ctx.client.run("text-to-music", body);
1747
+ return { kind: "text", text: `All ${all.length} clips ready \u2014 the ${soundtrack} score is composing (charged like any music generation). Poll again in ~20s with {"film_id": "${filmId}", "action": "status", "music_job_id": "${dispatch2.jobId}"}.` };
1693
1748
  }
1694
1749
  await filmApi(ctx, "POST", `/api/films/${filmId}/export`, {});
1695
1750
  return { kind: "text", text: `All clips ready \u2014 export started (${progressLine(film)}). Poll again in ~60s with action "status" for the finished film URL.` };
1696
1751
  }
1697
1752
  const failed = all.filter((sh) => sh.status === "failed");
1753
+ const refused = failed.filter(isRefused);
1754
+ const retrying = failed.filter((sh) => !isRefused(sh));
1755
+ const sentAgain = retrying.filter((sh) => resent.has(sh.id)).length;
1756
+ const paused = refused.length > 0 && started === 0 && !all.some((sh) => sh.status === "frame-generating" || sh.status === "clip-generating");
1757
+ const labels = new Map(film.scenes.flatMap((s, si) => s.shots.map((sh, i) => [sh.id, `${si + 1}.${i + 1}`])));
1758
+ const reason = (sh) => {
1759
+ const text = (sh.errorMessage ?? "no reason given").replace(/\s*Retry (?:the shot|it)\.\s*$/i, "");
1760
+ return text.length > 300 ? `${text.slice(0, 297)}...` : text;
1761
+ };
1762
+ const one = refused.length === 1;
1698
1763
  return {
1699
1764
  kind: "text",
1700
1765
  text: [
1701
- `In progress: ${progressLine(film)}.`,
1702
- failed.length > 0 ? `${failed.length} shot(s) failed and were re-queued (${failed[0]?.errorMessage?.slice(0, 100) ?? ""}).` : "",
1703
- `Poll again in ~30s with {"film_id": "${filmId}", "action": "status"}.`
1766
+ `${paused ? "Paused" : "In progress"}: ${progressLine(film)}.`,
1767
+ refused.length > 0 ? `${one ? "Shot" : "Shots"} ${refused.map((sh) => labels.get(sh.id)).join(", ")} need${one ? "s" : ""} a new frame or prompt. The provider refused ${one ? "it" : "them"} (${reason(refused[0])}) and usually refuses the same input again, so status does not resend ${one ? "it" : "them"}: every retry is a charged render. Change ${one ? "it" : "them"} and render again at ${ctx.baseUrl}/film-studio/${filmId}.` : "",
1768
+ retrying.length > 0 ? `${retrying.length} shot(s) failed (${reason(retrying[0])}): ${sentAgain === retrying.length ? "sent again" : sentAgain > 0 ? `${sentAgain} sent again, the rest on a later poll` : "sent again on a later poll"}.` : "",
1769
+ paused ? `Nothing else is rendering. Once the shot${one ? "" : "s"} render${one ? "s" : ""} again, call pixeldojo_film with {"film_id": "${filmId}", "action": "status"} to carry on.` : `Poll again in ~30s with {"film_id": "${filmId}", "action": "status"}.`
1704
1770
  ].filter(Boolean).join("\n")
1705
1771
  };
1706
1772
  }
1707
1773
  async function run8(input, ctx) {
1708
- const action = input.action === "plan" && input.film_id ? "status" : input.action;
1774
+ const action = input.action === "plan" && input.film_id ? input.cast?.length ? "cast" : "status" : input.action;
1709
1775
  if (action === "plan") return plan(input, ctx);
1710
- if (!input.film_id) return { kind: "text", text: "produce/status need the film_id returned by the plan step." };
1776
+ if (!input.film_id) return { kind: "text", text: "produce/status/cast need the film_id returned by the plan step." };
1777
+ if (action === "cast") {
1778
+ if (!input.cast?.length) return { kind: "text", text: "Pass cast: [{ name, prompt | image_urls | character_id }] to cast characters." };
1779
+ const { lines } = await applyCast(ctx, input.film_id, input.cast);
1780
+ const uncast = await uncastRolesOf(ctx, input.film_id);
1781
+ return {
1782
+ kind: "text",
1783
+ text: [
1784
+ ...lines,
1785
+ uncast.length ? castNeededText(input.film_id, uncast) : `Everyone is cast. Call pixeldojo_film with {"film_id": "${input.film_id}", "action": "produce"} to start production.`
1786
+ ].join("\n")
1787
+ };
1788
+ }
1711
1789
  if (action === "produce") return produce(input.film_id, ctx);
1712
1790
  return advance(input.film_id, input.soundtrack, input.music_job_id, ctx);
1713
1791
  }
1714
- 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;
1792
+ var STORY_LOOKS, MUSIC_VIDEO_LOOKS, LOOKS, MUSIC_CLIP_ENGINE_ID, InputSchema8, InputJsonSchema8, shots, count, frameDone, RETRY_CLEARS, REFUSED, isRefused, PLANNER_POLL_MS, PLANNER_MAX_POLLS, isMusicVideo, needsSlice, stillPlanningText, LOOK_MUSIC, ARC, filmSkill;
1715
1793
  var init_film = __esm({
1716
1794
  "src/skills/film.ts"() {
1717
1795
  init_upload_helpers();
@@ -1729,8 +1807,16 @@ var init_film = __esm({
1729
1807
  lyrics: zod.z.string().max(1e4).optional().describe("Music video mode: the song lyrics, so shots land on the right lines. Optional."),
1730
1808
  aspect: zod.z.enum(["16:9", "9:16"]).default("16:9"),
1731
1809
  length: zod.z.enum(["30s", "60s", "2-3min", "5min"]).default("60s").describe("Target length; drives the shot count (~6/12/24/60 shots)."),
1732
- film_id: zod.z.string().max(40).optional().describe("Existing film id (from a previous call) for produce/status."),
1733
- action: zod.z.enum(["plan", "produce", "status"]).default("plan").describe("'plan' creates + storyboards (free). 'produce' starts charged generation. 'status' advances production and reports progress; poll it every ~30s until it returns the finished film."),
1810
+ film_id: zod.z.string().max(40).optional().describe("Existing film id (from a previous call) for produce/status/cast."),
1811
+ action: zod.z.enum(["plan", "produce", "status", "cast"]).default("plan").describe("'plan' creates + storyboards (free). 'cast' casts characters on an existing film. 'produce' starts charged generation. 'status' advances production and reports progress; poll it every ~30s until it returns the finished film."),
1812
+ cast: zod.z.array(
1813
+ zod.z.object({
1814
+ name: zod.z.string().trim().min(1).max(60),
1815
+ image_urls: zod.z.array(zod.z.string().url().startsWith("https://").max(2048)).min(1).max(8).optional(),
1816
+ prompt: zod.z.string().trim().min(1).max(600).optional(),
1817
+ character_id: zod.z.string().max(40).optional()
1818
+ })
1819
+ ).max(8).optional().describe("Cast members, one per named character. Each gives image_urls (your own photos), a prompt (a portrait is generated, charged), or a character_id (a Character Studio character); with none of these the Character Studio character of that name is used."),
1734
1820
  soundtrack: zod.z.enum(["scene-synced", "flat", "none"]).default("scene-synced").describe("scene-synced composes one musical section per scene, pinned to the scene lengths (default). flat is a single cohesive bed. none skips music."),
1735
1821
  music_job_id: zod.z.string().max(80).optional().describe("The music_job_id from the previous status reply while the score is composing, so that score is attached. A status call without it, once all clips are ready and no score is attached, starts and charges for a new score.")
1736
1822
  });
@@ -1758,8 +1844,26 @@ var init_film = __esm({
1758
1844
  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".' },
1759
1845
  action: {
1760
1846
  type: "string",
1761
- enum: ["plan", "produce", "status"],
1762
- description: "plan (default, free): writes the script and plans shots, returns film_id and a cost estimate. produce (charged): starts rendering frames. status: advances production one step and reports progress; call it every ~30s until it returns the finished film URL."
1847
+ enum: ["plan", "produce", "status", "cast"],
1848
+ description: "plan (default, free): writes the script and plans shots, returns film_id, a cost estimate, and any characters still to cast. cast: casts characters on an existing film (pass film_id and cast). produce (charged): starts rendering frames; refused until every named character is cast. status: advances production one step and reports progress; call it every ~30s until it returns the finished film URL."
1849
+ },
1850
+ cast: {
1851
+ type: "array",
1852
+ maxItems: 8,
1853
+ description: 'Cast members, one per named character. Every named character must be cast before frames render, so faces, ages and species hold in every shot. Pass them on the plan call (the script then stars them by name) or later with action "cast" using the names the plan reports. Each entry: name, plus ONE of image_urls (your own photos of an adult, made into a character), prompt (a portrait is generated from it, charged like any image), or character_id (a Character Studio character). An entry with only a name uses your Character Studio character of that name.',
1854
+ items: {
1855
+ type: "object",
1856
+ properties: {
1857
+ name: { type: "string", description: 'The character name as the film uses it (e.g. "Harlan").' },
1858
+ image_urls: { type: "array", items: { type: "string" }, description: "Your own https photos of the character (1-8). PixelDojo uploads work; they are copied to permanent storage." },
1859
+ prompt: {
1860
+ type: "string",
1861
+ description: 'A description to generate the portrait from: species if not human, gender, adult age, look and outfit (e.g. "man in his late 50s, grey mustache, sunburnt, red windbreaker").'
1862
+ },
1863
+ character_id: { type: "string", description: "A Character Studio character id." }
1864
+ },
1865
+ required: ["name"]
1866
+ }
1763
1867
  },
1764
1868
  soundtrack: {
1765
1869
  type: "string",
@@ -1774,6 +1878,10 @@ var init_film = __esm({
1774
1878
  };
1775
1879
  shots = (f) => f.scenes.flatMap((s) => s.shots);
1776
1880
  count = (f, pred) => shots(f).filter(pred).length;
1881
+ frameDone = (sh) => Boolean(sh.frameUrl) && sh.status !== "idle" && sh.status !== "frame-generating";
1882
+ RETRY_CLEARS = /automatic audio check flagged|output audio\b[\s\S]*?sensitive information|temporarily unavailable/i;
1883
+ REFUSED = /copyright|infringement|content filter|safety filter|moderation|blocked|flagged|sensitive|real (?:person|human)|nsfw|content policy|policy violation|inappropriate/i;
1884
+ isRefused = (sh) => sh.status === "failed" && Boolean(sh.errorMessage) && !RETRY_CLEARS.test(sh.errorMessage) && REFUSED.test(sh.errorMessage);
1777
1885
  PLANNER_POLL_MS = 3e3;
1778
1886
  PLANNER_MAX_POLLS = 8;
1779
1887
  isMusicVideo = (f) => f.musicVideoMode === true;
@@ -1799,8 +1907,8 @@ var init_film = __esm({
1799
1907
  };
1800
1908
  filmSkill = {
1801
1909
  name: "pixeldojo_film",
1802
- 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).",
1803
- 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).",
1910
+ 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. Every named character must be cast (cast: your own images, a portrait prompt, or a Character Studio character) before frames render. Phased: plan is free; cast; produce starts charged generation; status advances production (poll every ~30s).",
1911
+ 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. Every named character must be cast (cast: your own images, a portrait prompt, or a Character Studio character) before frames render. Phased: plan is free; cast; produce starts charged generation; status advances production (poll every ~30s).",
1804
1912
  inputZod: InputSchema8,
1805
1913
  inputJsonSchema: InputJsonSchema8,
1806
1914
  allowedModels: [],
@@ -2234,7 +2342,7 @@ async function produce2(id, ctx) {
2234
2342
  if (saved.status !== 200 || !url) {
2235
2343
  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")}` };
2236
2344
  }
2237
- return { kind: "text", text: `"${ep.title}" is finished and saved to My Media: ${url}` };
2345
+ return { kind: "text", text: `"${ep.title}" is finished and saved to your Library: ${url}` };
2238
2346
  }
2239
2347
  const started = await episodeApi(ctx, "POST", `/${id}/export`, {});
2240
2348
  if (started.status !== 202) {
@@ -2305,7 +2413,7 @@ var init_episode = __esm({
2305
2413
  stillPlanning = (id) => `The script is still being written (episode_id: ${id}). Nothing has been charged. ${next(id, "status", "~30s")}`;
2306
2414
  episodeSkill = {
2307
2415
  name: "pixeldojo_episode",
2308
- 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.",
2416
+ 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 the Library. 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.",
2309
2417
  inputZod: InputSchema10,
2310
2418
  inputJsonSchema: InputJsonSchema10,
2311
2419
  allowedModels: [],
@@ -2855,7 +2963,7 @@ var init_library = __esm({
2855
2963
  };
2856
2964
  librarySkill = {
2857
2965
  name: "pixeldojo_library",
2858
- 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.",
2966
+ description: "Search your Library: saved media (permanent) and recent API/MCP generations (deleted 1 hour after they finish), newest first. Returns asset URLs to reuse as reference inputs.",
2859
2967
  inputZod: InputSchema16,
2860
2968
  inputJsonSchema: InputJsonSchema16,
2861
2969
  allowedModels: [],
@@ -3277,7 +3385,7 @@ function makeRedactedLogger(target = console) {
3277
3385
  }
3278
3386
 
3279
3387
  // src/server.ts
3280
- var PACKAGE_VERSION = "0.16.3" ;
3388
+ var PACKAGE_VERSION = "0.17.0" ;
3281
3389
  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.";
3282
3390
  function buildServer(options) {
3283
3391
  if (!options.apiKey) {