@pixeldojo/mcp 0.17.0 → 0.19.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 +2 -2
- package/dist/cli.cjs +334 -55
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +334 -55
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +334 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +334 -55
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.cjs
CHANGED
|
@@ -595,7 +595,7 @@ var init_generate = __esm({
|
|
|
595
595
|
DRAFT_DESCRIPTION = "Seedance 2.5 draft mode: render a cheap 480p draft (runs on seedance-2-5) so the user can try takes before paying for full resolution. The reply gives the draft's job id and duration; pass them to upgrade_draft to render that exact take at 1080p.";
|
|
596
596
|
UPGRADE_DESCRIPTION = "Job id of a finished Seedance 2.5 draft to render at 1080p, the same take with full detail. Also pass `duration` equal to the draft's (the draft reply states it). The draft's prompt and media are reused, so prompt and references are ignored. Billed as a 1080p run.";
|
|
597
597
|
InputSchema = zod.z.object({
|
|
598
|
-
prompt: zod.z.string().min(1).max(8e3).describe(
|
|
598
|
+
prompt: zod.z.string().min(1).max(8e3).describe('Prompt describing what to generate. Write @Name to use a character or element the user saved in Character Studio ("@Mara on a beach"): its photos are attached automatically on models that take reference images. pixeldojo_library source=characters lists them.'),
|
|
599
599
|
model: zod.z.string().optional().describe(
|
|
600
600
|
`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.`
|
|
601
601
|
),
|
|
@@ -635,7 +635,7 @@ var init_generate = __esm({
|
|
|
635
635
|
InputJsonSchema = {
|
|
636
636
|
type: "object",
|
|
637
637
|
properties: {
|
|
638
|
-
prompt: { type: "string", description:
|
|
638
|
+
prompt: { type: "string", description: 'Prompt describing what to generate. Write @Name to use a character or element the user saved in Character Studio ("@Mara on a beach"): its photos are attached automatically on models that take reference images. pixeldojo_library source=characters lists them.' },
|
|
639
639
|
model: {
|
|
640
640
|
type: "string",
|
|
641
641
|
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.`
|
|
@@ -849,7 +849,7 @@ var init_edit = __esm({
|
|
|
849
849
|
InputJsonSchema2 = {
|
|
850
850
|
type: "object",
|
|
851
851
|
properties: {
|
|
852
|
-
prompt: { type: "string", description:
|
|
852
|
+
prompt: { type: "string", description: 'Edit instruction. Write @Name to use a character or element the user saved in Character Studio ("@Mara on a beach"): its photos are attached automatically on models that take reference images. pixeldojo_library source=characters lists them.' },
|
|
853
853
|
image_url: {
|
|
854
854
|
type: "string",
|
|
855
855
|
format: "uri",
|
|
@@ -1455,6 +1455,13 @@ 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
|
+
function keepGoing(filmId, afterSec, extra = {}) {
|
|
1459
|
+
const set = Object.fromEntries(Object.entries(extra).filter(([, v]) => v !== void 0));
|
|
1460
|
+
return { input: { film_id: filmId, action: "status", ...set }, afterSec };
|
|
1461
|
+
}
|
|
1462
|
+
function shotLabels(f) {
|
|
1463
|
+
return new Map(f.scenes.flatMap((s, si) => s.shots.map((sh, i) => [sh.id, `${si + 1}.${i + 1}`])));
|
|
1464
|
+
}
|
|
1458
1465
|
async function applyCast(ctx, filmId, cast) {
|
|
1459
1466
|
let charged = false;
|
|
1460
1467
|
const lines = await Promise.all(cast.map(async (c) => {
|
|
@@ -1466,13 +1473,23 @@ async function applyCast(ctx, filmId, cast) {
|
|
|
1466
1473
|
role: c.name,
|
|
1467
1474
|
...c.character_id ? { characterId: c.character_id } : {},
|
|
1468
1475
|
...c.image_urls ? { imageUrls: c.image_urls } : {},
|
|
1469
|
-
...c.prompt ? { prompt: c.prompt } : {}
|
|
1476
|
+
...c.prompt ? { prompt: c.prompt } : {},
|
|
1477
|
+
...c.replace ? { replace: true } : {},
|
|
1478
|
+
...c.remove ? { remove: true } : {}
|
|
1470
1479
|
}
|
|
1471
1480
|
);
|
|
1472
|
-
if (r.
|
|
1473
|
-
if (r.status !== 200)
|
|
1474
|
-
|
|
1475
|
-
|
|
1481
|
+
if (r.data.creditCost) charged = true;
|
|
1482
|
+
if (r.status !== 200) {
|
|
1483
|
+
const where2 = r.data.code === "cast-rendered" ? ` Storyboard: ${ctx.baseUrl}/film-studio/${filmId}` : "";
|
|
1484
|
+
return `Could not ${c.remove ? "remove" : c.replace ? "replace" : "cast"} ${c.name}: ${r.data.error ?? r.status}${where2}`;
|
|
1485
|
+
}
|
|
1486
|
+
if (r.data.removed) return `Removed ${c.name} from the film and every shot.`;
|
|
1487
|
+
if (r.data.alreadyCast) {
|
|
1488
|
+
return c.replace ? `${c.name} is already played by that character; nothing changed.` : `${c.name} is already cast; nothing changed. To swap who plays ${c.name}, send the entry again with replace: true.`;
|
|
1489
|
+
}
|
|
1490
|
+
const cost = r.data.creditCost ? ` (portrait, ${r.data.creditCost} credits)` : "";
|
|
1491
|
+
const where = r.data.boundShots ? `, in ${r.data.boundShots} shot(s)` : "";
|
|
1492
|
+
return r.data.replaced ? `Replaced ${r.data.replaced.name} with ${r.data.character?.name ?? "the new cast member"}${r.data.character?.id ? ` (character_id ${r.data.character.id})` : ""} as ${c.name}${cost}${where}.` : `Cast ${c.name}${cost}${where}.`;
|
|
1476
1493
|
}));
|
|
1477
1494
|
return { lines, charged };
|
|
1478
1495
|
}
|
|
@@ -1564,12 +1581,13 @@ async function plan(input, ctx) {
|
|
|
1564
1581
|
const castStarted = Date.now();
|
|
1565
1582
|
const cast = input.cast?.length ? await applyCast(ctx, id, input.cast) : { lines: [], charged: false };
|
|
1566
1583
|
const pollBudget = Math.max(0, PLANNER_MAX_POLLS - Math.ceil((Date.now() - castStarted) / PLANNER_POLL_MS));
|
|
1567
|
-
const reply = (text) => ({
|
|
1584
|
+
const reply = (text, followUp) => ({
|
|
1568
1585
|
kind: "text",
|
|
1569
1586
|
text: [
|
|
1570
1587
|
...cast.lines,
|
|
1571
1588
|
cast.charged ? text.replace(/Nothing has been charged( yet)?\./g, "Only the cast portraits above have been charged.") : text
|
|
1572
|
-
].join("\n")
|
|
1589
|
+
].join("\n"),
|
|
1590
|
+
...followUp ? { followUp } : {}
|
|
1573
1591
|
});
|
|
1574
1592
|
let planned;
|
|
1575
1593
|
if (music) {
|
|
@@ -1580,14 +1598,14 @@ ${input.lyrics.trim()}` : ""].filter(Boolean).join("\n\n");
|
|
|
1580
1598
|
let script = input.script?.trim() ?? "";
|
|
1581
1599
|
if (!script) {
|
|
1582
1600
|
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"));
|
|
1601
|
+
if ("pending" in wrote) return reply(stillPlanningText(id, "script"), keepGoing(id, 30, { soundtrack: input.soundtrack }));
|
|
1584
1602
|
if ("error" in wrote) return reply(`Script writing failed: ${wrote.error}`);
|
|
1585
1603
|
script = wrote.film.script ?? "";
|
|
1586
1604
|
if (!script) return reply("Script writing failed: the studio returned an empty script.");
|
|
1587
1605
|
}
|
|
1588
1606
|
planned = await runPlanner(ctx, id, `/api/films/${id}/plan`, { script });
|
|
1589
1607
|
}
|
|
1590
|
-
if ("pending" in planned) return reply(stillPlanningText(id));
|
|
1608
|
+
if ("pending" in planned) return reply(stillPlanningText(id), keepGoing(id, 30, { soundtrack: input.soundtrack }));
|
|
1591
1609
|
if ("error" in planned) return reply(`Shot planning failed: ${planned.error}`);
|
|
1592
1610
|
const film = planned.film;
|
|
1593
1611
|
if (needsSlice(film)) {
|
|
@@ -1600,17 +1618,17 @@ ${err} Call action "status" to retry the cut.`);
|
|
|
1600
1618
|
return reply([plannedText(film, ctx), uncast.length ? `
|
|
1601
1619
|
${castNeededText(id, uncast)}` : ""].filter(Boolean).join("\n"));
|
|
1602
1620
|
}
|
|
1603
|
-
async function resumePlanning(film, ctx) {
|
|
1621
|
+
async function resumePlanning(film, ctx, soundtrack) {
|
|
1604
1622
|
if (film.scenes.length > 0 && film.llmJob?.status !== "running") return null;
|
|
1605
1623
|
if (film.llmJob?.status === "running") {
|
|
1606
|
-
return { kind: "text", text: stillPlanningText(film.id, film.llmJob.kind === "script" ? "script" : "plan") };
|
|
1624
|
+
return { kind: "text", text: stillPlanningText(film.id, film.llmJob.kind === "script" ? "script" : "plan"), followUp: keepGoing(film.id, 30, { soundtrack }) };
|
|
1607
1625
|
}
|
|
1608
1626
|
const prior = film.llmJob?.status === "error" ? `The last planning run failed (${film.llmJob.error ?? "unknown"}). ` : "";
|
|
1609
1627
|
if (!film.script?.trim()) {
|
|
1610
1628
|
return { kind: "text", text: `${prior}This film has nothing to plan from. Start a new one with action "plan".` };
|
|
1611
1629
|
}
|
|
1612
1630
|
const planned = await runPlanner(ctx, film.id, `/api/films/${film.id}/plan`, { script: film.script }, 0);
|
|
1613
|
-
if ("pending" in planned) return { kind: "text", text: `${prior}${stillPlanningText(film.id)}
|
|
1631
|
+
if ("pending" in planned) return { kind: "text", text: `${prior}${stillPlanningText(film.id)}`, followUp: keepGoing(film.id, 30, { soundtrack }) };
|
|
1614
1632
|
if ("error" in planned) return { kind: "text", text: `Shot planning failed: ${planned.error} Call action "status" to retry.` };
|
|
1615
1633
|
if (needsSlice(planned.film)) {
|
|
1616
1634
|
const err = await sliceSong(ctx, planned.film);
|
|
@@ -1620,22 +1638,30 @@ ${err} Call action "status" to retry the cut.` };
|
|
|
1620
1638
|
}
|
|
1621
1639
|
return { kind: "text", text: plannedText(planned.film, ctx) };
|
|
1622
1640
|
}
|
|
1623
|
-
async function produce(filmId, ctx) {
|
|
1641
|
+
async function produce(filmId, soundtrack, ctx) {
|
|
1624
1642
|
const { data } = await filmApi(ctx, "GET", `/api/films/${filmId}`);
|
|
1625
1643
|
if (!data.film) return { kind: "text", text: `Film not found: ${data.error ?? filmId}` };
|
|
1626
|
-
const planning = await resumePlanning(data.film, ctx);
|
|
1644
|
+
const planning = await resumePlanning(data.film, ctx, soundtrack);
|
|
1627
1645
|
if (planning) return planning;
|
|
1628
1646
|
const uncast = await uncastRolesOf(ctx, filmId);
|
|
1629
1647
|
if (uncast.length) return { kind: "text", text: `Nothing was charged. ${castNeededText(filmId, uncast)}` };
|
|
1630
1648
|
const pending = shots(data.film).filter((sh) => sh.status === "idle" || sh.status === "failed" && !sh.frameUrl);
|
|
1631
1649
|
let fired = 0;
|
|
1650
|
+
let refused;
|
|
1632
1651
|
for (const sh of pending.slice(0, 6)) {
|
|
1633
1652
|
const r = await filmApi(ctx, "POST", `/api/films/${filmId}/shots/${sh.id}/generate`, { kind: "frame" });
|
|
1634
1653
|
if (r.status === 200) fired += 1;
|
|
1654
|
+
else if (!benignDispatchRefusal(r.status, r.data.error)) refused ??= r.data.error ?? `HTTP ${r.status}`;
|
|
1655
|
+
}
|
|
1656
|
+
if (fired === 0 && refused) {
|
|
1657
|
+
return { kind: "text", text: `Production could not start on "${data.film.title}": ${refused}` };
|
|
1635
1658
|
}
|
|
1636
1659
|
return {
|
|
1637
1660
|
kind: "text",
|
|
1638
|
-
text: `Production started on "${data.film.title}": ${fired} frame(s) dispatched (each is a normal frame charge; the server's Frame check reviews every one)
|
|
1661
|
+
text: `Production started on "${data.film.title}": ${fired} frame(s) dispatched (each is a normal frame charge; the server's Frame check reviews every one).${refused ? ` Some could not start: ${refused}` : ""} Poll pixeldojo_film with {"film_id": "${filmId}", "action": "status"} every ~30 seconds \u2014 each poll advances the pipeline.`,
|
|
1662
|
+
// The caller's soundtrack choice rides along: the follow-up status calls
|
|
1663
|
+
// are the ones that reach the score.
|
|
1664
|
+
followUp: keepGoing(filmId, 30, { soundtrack })
|
|
1639
1665
|
};
|
|
1640
1666
|
}
|
|
1641
1667
|
function buildScorePlanBody(film) {
|
|
@@ -1681,30 +1707,49 @@ async function advance(filmId, soundtrack, musicJobId, ctx) {
|
|
|
1681
1707
|
if (film.status === "exported" && film.exportUrl) {
|
|
1682
1708
|
return {
|
|
1683
1709
|
kind: "text",
|
|
1684
|
-
text:
|
|
1710
|
+
text: [
|
|
1711
|
+
`Film finished: "${film.title}"
|
|
1685
1712
|
Watch/download: ${film.exportUrl}
|
|
1686
1713
|
Workspace: ${ctx.baseUrl}/film-studio/${film.id}
|
|
1687
|
-
It is saved in the Library folder "Film: ${film.title}"
|
|
1714
|
+
It is saved in the Library folder "Film: ${film.title}".`,
|
|
1715
|
+
shotIndex(film)
|
|
1716
|
+
].filter(Boolean).join("\n")
|
|
1688
1717
|
};
|
|
1689
1718
|
}
|
|
1690
|
-
|
|
1691
|
-
|
|
1719
|
+
const startExport = async () => {
|
|
1720
|
+
const r = await filmApi(ctx, "POST", `/api/films/${filmId}/export`, {});
|
|
1721
|
+
return r.status === 200 || r.status === 409 ? null : r.data.error ?? `HTTP ${r.status}`;
|
|
1722
|
+
};
|
|
1723
|
+
if (film.exportError && all.length > 0 && all.every((sh) => sh.status === "clip-ready")) {
|
|
1724
|
+
const again = await startExport();
|
|
1725
|
+
return {
|
|
1726
|
+
kind: "text",
|
|
1727
|
+
text: again ? `Export failed: ${film.exportError} Starting it again failed too: ${again}` : `The last export failed (${film.exportError}). Started it again; exports are free. Call pixeldojo_film with {"film_id": "${filmId}", "action": "status"} in about a minute for the finished film.`
|
|
1728
|
+
};
|
|
1692
1729
|
}
|
|
1693
|
-
const planning = await resumePlanning(film, ctx);
|
|
1730
|
+
const planning = await resumePlanning(film, ctx, soundtrack);
|
|
1694
1731
|
if (planning) return planning;
|
|
1695
1732
|
if (needsSlice(film)) {
|
|
1696
1733
|
const err = await sliceSong(ctx, film);
|
|
1697
1734
|
if (err) return { kind: "text", text: `${err} Poll again with action "status" to retry.` };
|
|
1698
|
-
return {
|
|
1735
|
+
return {
|
|
1736
|
+
kind: "text",
|
|
1737
|
+
text: `Song cut into one slice per shot. Poll again with {"film_id": "${filmId}", "action": "status"} to keep production moving.`,
|
|
1738
|
+
followUp: keepGoing(filmId, 5, { soundtrack })
|
|
1739
|
+
};
|
|
1699
1740
|
}
|
|
1700
1741
|
if (shots(film).length > 0 && shots(film).every((sh) => sh.status === "idle")) {
|
|
1701
1742
|
return { kind: "text", text: plannedText(film, ctx) };
|
|
1702
1743
|
}
|
|
1703
1744
|
let started = 0;
|
|
1704
1745
|
const resent = /* @__PURE__ */ new Set();
|
|
1746
|
+
const refusedStarts = [];
|
|
1705
1747
|
const dispatch = async (sh, kind) => {
|
|
1706
1748
|
const r = await filmApi(ctx, "POST", `/api/films/${filmId}/shots/${sh.id}/generate`, { kind });
|
|
1707
|
-
if (r.status !== 200)
|
|
1749
|
+
if (r.status !== 200) {
|
|
1750
|
+
if (!benignDispatchRefusal(r.status, r.data.error)) refusedStarts.push(r.data.error ?? `HTTP ${r.status}`);
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1708
1753
|
started += 1;
|
|
1709
1754
|
if (sh.status === "failed") resent.add(sh.id);
|
|
1710
1755
|
};
|
|
@@ -1734,27 +1779,47 @@ It is saved in the Library folder "Film: ${film.title}".`
|
|
|
1734
1779
|
const outUrl = out?.audio ?? out?.video ?? out?.images?.[0];
|
|
1735
1780
|
if (job.status === "completed" && typeof outUrl === "string") {
|
|
1736
1781
|
await filmApi(ctx, "PATCH", `/api/films/${filmId}`, { audioStems: [{ kind: "music", url: outUrl }] });
|
|
1737
|
-
|
|
1738
|
-
return { kind: "text", text: `Score attached
|
|
1782
|
+
const exportError2 = await startExport();
|
|
1783
|
+
if (exportError2) return { kind: "text", text: `Score attached, but the export could not start: ${exportError2}` };
|
|
1784
|
+
return {
|
|
1785
|
+
kind: "text",
|
|
1786
|
+
text: `Score attached \u2014 export started. Poll again in ~60s with {"film_id": "${filmId}", "action": "status"} for the finished film URL.`,
|
|
1787
|
+
followUp: keepGoing(filmId, 60, { soundtrack })
|
|
1788
|
+
};
|
|
1739
1789
|
}
|
|
1740
1790
|
if (job.status === "failed") {
|
|
1741
1791
|
return { kind: "text", text: `The score failed (${job.error ?? "unknown"}) and was refunded. Poll again to retry, or pass soundtrack "none" to export without music.` };
|
|
1742
1792
|
}
|
|
1743
|
-
return {
|
|
1793
|
+
return {
|
|
1794
|
+
kind: "text",
|
|
1795
|
+
text: `Score still composing. Poll again in ~20s with {"film_id": "${filmId}", "action": "status", "music_job_id": "${musicJobId}"}.`,
|
|
1796
|
+
followUp: keepGoing(filmId, 20, { soundtrack, music_job_id: musicJobId })
|
|
1797
|
+
};
|
|
1744
1798
|
}
|
|
1745
1799
|
const body = soundtrack === "scene-synced" ? buildScorePlanBody(film) : buildFlatMusicBody(film, all);
|
|
1746
1800
|
const dispatch2 = await ctx.client.run("text-to-music", body);
|
|
1747
|
-
return {
|
|
1801
|
+
return {
|
|
1802
|
+
kind: "text",
|
|
1803
|
+
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}"}.`,
|
|
1804
|
+
followUp: keepGoing(filmId, 20, { soundtrack, music_job_id: dispatch2.jobId })
|
|
1805
|
+
};
|
|
1748
1806
|
}
|
|
1749
|
-
|
|
1750
|
-
return { kind: "text", text: `All clips ready
|
|
1807
|
+
const exportError = await startExport();
|
|
1808
|
+
if (exportError) return { kind: "text", text: `All clips ready, but the export could not start: ${exportError}` };
|
|
1809
|
+
return {
|
|
1810
|
+
kind: "text",
|
|
1811
|
+
text: `All clips ready \u2014 export started (${progressLine(film)}). Poll again in ~60s with action "status" for the finished film URL.`,
|
|
1812
|
+
followUp: keepGoing(filmId, 60, { soundtrack })
|
|
1813
|
+
};
|
|
1751
1814
|
}
|
|
1752
1815
|
const failed = all.filter((sh) => sh.status === "failed");
|
|
1753
1816
|
const refused = failed.filter(isRefused);
|
|
1754
1817
|
const retrying = failed.filter((sh) => !isRefused(sh));
|
|
1755
1818
|
const sentAgain = retrying.filter((sh) => resent.has(sh.id)).length;
|
|
1756
|
-
const
|
|
1757
|
-
const
|
|
1819
|
+
const rendering = all.some((sh) => sh.status === "frame-generating" || sh.status === "clip-generating");
|
|
1820
|
+
const paused = refused.length > 0 && started === 0 && !rendering;
|
|
1821
|
+
const moving = started > 0 || rendering;
|
|
1822
|
+
const labels = shotLabels(film);
|
|
1758
1823
|
const reason = (sh) => {
|
|
1759
1824
|
const text = (sh.errorMessage ?? "no reason given").replace(/\s*Retry (?:the shot|it)\.\s*$/i, "");
|
|
1760
1825
|
return text.length > 300 ? `${text.slice(0, 297)}...` : text;
|
|
@@ -1766,16 +1831,77 @@ It is saved in the Library folder "Film: ${film.title}".`
|
|
|
1766
1831
|
`${paused ? "Paused" : "In progress"}: ${progressLine(film)}.`,
|
|
1767
1832
|
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
1833
|
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
|
-
|
|
1770
|
-
|
|
1834
|
+
refusedStarts.length > 0 ? `${refusedStarts.length} render(s) could not start: ${refusedStarts[0]}` : "",
|
|
1835
|
+
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.` : moving ? `Poll again in ~30s with {"film_id": "${filmId}", "action": "status"}.` : `Nothing is rendering. Fix what is above, then call pixeldojo_film with {"film_id": "${filmId}", "action": "status"} to carry on.`
|
|
1836
|
+
].filter(Boolean).join("\n"),
|
|
1837
|
+
...moving && !paused ? { followUp: keepGoing(filmId, 30, { soundtrack }) } : {}
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
function shotIndex(f) {
|
|
1841
|
+
const all = shots(f);
|
|
1842
|
+
if (all.length === 0 || all.length > 24) return "";
|
|
1843
|
+
const labels = shotLabels(f);
|
|
1844
|
+
let at = 0;
|
|
1845
|
+
const clock = (sec) => `${Math.floor(sec / 60)}:${String(Math.round(sec % 60)).padStart(2, "0")}`;
|
|
1846
|
+
const lines = all.map((sh) => {
|
|
1847
|
+
const from = at;
|
|
1848
|
+
at += sh.durationSec || 0;
|
|
1849
|
+
const what = sh.prompt.replace(/\s+/g, " ").trim();
|
|
1850
|
+
return ` ${labels.get(sh.id)} (about ${clock(from)}-${clock(at)}): ${what.length > 70 ? `${what.slice(0, 67)}...` : what}`;
|
|
1851
|
+
});
|
|
1852
|
+
return ['Shots (redo any with action "redo" and its label):', ...lines].join("\n");
|
|
1853
|
+
}
|
|
1854
|
+
async function redoShots(input, ctx) {
|
|
1855
|
+
const filmId = input.film_id;
|
|
1856
|
+
if (!input.shots?.length) {
|
|
1857
|
+
return { kind: "text", text: 'Pass shots: the labels status reports, like ["2.3"] for scene 2, shot 3.' };
|
|
1858
|
+
}
|
|
1859
|
+
const { data } = await filmApi(ctx, "GET", `/api/films/${filmId}`);
|
|
1860
|
+
const film = data.film;
|
|
1861
|
+
if (!film) return { kind: "text", text: `Film not found: ${data.error ?? filmId}` };
|
|
1862
|
+
const labels = shotLabels(film);
|
|
1863
|
+
const byLabel = new Map(shots(film).map((sh) => [labels.get(sh.id), sh]));
|
|
1864
|
+
const byId = new Map(shots(film).map((sh) => [sh.id, sh]));
|
|
1865
|
+
const kind = input.redo;
|
|
1866
|
+
const lines = [];
|
|
1867
|
+
let started = 0;
|
|
1868
|
+
let credits = 0;
|
|
1869
|
+
for (const ref of Array.from(new Set(input.shots))) {
|
|
1870
|
+
const sh = byLabel.get(ref) ?? byId.get(ref);
|
|
1871
|
+
if (!sh) {
|
|
1872
|
+
lines.push(`No shot ${ref} in this film (shots run 1.1 to ${Array.from(byLabel.keys()).pop()}).`);
|
|
1873
|
+
continue;
|
|
1874
|
+
}
|
|
1875
|
+
const label = labels.get(sh.id);
|
|
1876
|
+
if (kind === "clip" && !sh.frameUrl) {
|
|
1877
|
+
lines.push(`Shot ${label} has no frame yet, so there is no clip to redo. Redo it with redo "frame".`);
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
const r = await filmApi(ctx, "POST", `/api/films/${filmId}/shots/${sh.id}/generate`, { kind });
|
|
1881
|
+
if (r.status === 200) {
|
|
1882
|
+
started += 1;
|
|
1883
|
+
credits += r.data.creditCost ?? 0;
|
|
1884
|
+
lines.push(`Shot ${label}: new ${kind} rendering${r.data.creditCost ? ` (${r.data.creditCost} credits)` : ""}.`);
|
|
1885
|
+
} else {
|
|
1886
|
+
lines.push(`Shot ${label}: could not start: ${r.data.error ?? `HTTP ${r.status}`}`);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
if (started === 0) return { kind: "text", text: [...lines, "Nothing was started."].join("\n") };
|
|
1890
|
+
return {
|
|
1891
|
+
kind: "text",
|
|
1892
|
+
text: [
|
|
1893
|
+
...lines,
|
|
1894
|
+
`${credits ? `${credits} credits charged for the new take${started > 1 ? "s" : ""}. ` : ""}${kind === "frame" ? "Each new frame gets the Frame check, then a new clip. " : ""}The new take replaces the old one; keep calling pixeldojo_film with {"film_id": "${filmId}", "action": "status"}, which saves it and cuts a new export with it.`
|
|
1895
|
+
].join("\n"),
|
|
1896
|
+
followUp: keepGoing(filmId, 30, { soundtrack: input.soundtrack })
|
|
1771
1897
|
};
|
|
1772
1898
|
}
|
|
1773
1899
|
async function run8(input, ctx) {
|
|
1774
1900
|
const action = input.action === "plan" && input.film_id ? input.cast?.length ? "cast" : "status" : input.action;
|
|
1775
1901
|
if (action === "plan") return plan(input, ctx);
|
|
1776
|
-
if (!input.film_id) return { kind: "text", text: "produce/status/cast need the film_id returned by the plan step." };
|
|
1902
|
+
if (!input.film_id) return { kind: "text", text: "produce/status/cast/redo need the film_id returned by the plan step." };
|
|
1777
1903
|
if (action === "cast") {
|
|
1778
|
-
if (!input.cast?.length) return { kind: "text", text: "Pass cast: [{ name, prompt | image_urls | character_id }] to cast characters." };
|
|
1904
|
+
if (!input.cast?.length) return { kind: "text", text: "Pass cast: [{ name, prompt | image_urls | character_id }] to cast characters (add replace: true to swap someone already cast, or remove: true to take them off)." };
|
|
1779
1905
|
const { lines } = await applyCast(ctx, input.film_id, input.cast);
|
|
1780
1906
|
const uncast = await uncastRolesOf(ctx, input.film_id);
|
|
1781
1907
|
return {
|
|
@@ -1786,10 +1912,11 @@ async function run8(input, ctx) {
|
|
|
1786
1912
|
].join("\n")
|
|
1787
1913
|
};
|
|
1788
1914
|
}
|
|
1789
|
-
if (action === "produce") return produce(input.film_id, ctx);
|
|
1915
|
+
if (action === "produce") return produce(input.film_id, input.soundtrack, ctx);
|
|
1916
|
+
if (action === "redo") return redoShots(input, ctx);
|
|
1790
1917
|
return advance(input.film_id, input.soundtrack, input.music_job_id, ctx);
|
|
1791
1918
|
}
|
|
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;
|
|
1919
|
+
var STORY_LOOKS, MUSIC_VIDEO_LOOKS, LOOKS, MUSIC_CLIP_ENGINE_ID, InputSchema8, InputJsonSchema8, shots, benignDispatchRefusal, count, frameDone, RETRY_CLEARS, REFUSED, isRefused, PLANNER_POLL_MS, PLANNER_MAX_POLLS, isMusicVideo, needsSlice, stillPlanningText, LOOK_MUSIC, ARC, filmSkill;
|
|
1793
1920
|
var init_film = __esm({
|
|
1794
1921
|
"src/skills/film.ts"() {
|
|
1795
1922
|
init_upload_helpers();
|
|
@@ -1807,16 +1934,20 @@ var init_film = __esm({
|
|
|
1807
1934
|
lyrics: zod.z.string().max(1e4).optional().describe("Music video mode: the song lyrics, so shots land on the right lines. Optional."),
|
|
1808
1935
|
aspect: zod.z.enum(["16:9", "9:16"]).default("16:9"),
|
|
1809
1936
|
length: zod.z.enum(["30s", "60s", "2-3min", "5min"]).default("60s").describe("Target length; drives the shot count (~6/12/24/60 shots)."),
|
|
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."),
|
|
1937
|
+
film_id: zod.z.string().max(40).optional().describe("Existing film id (from a previous call) for produce/status/cast/redo."),
|
|
1938
|
+
action: zod.z.enum(["plan", "produce", "status", "cast", "redo"]).default("plan").describe("'plan' creates + storyboards (free). 'cast' casts, replaces or removes characters on an existing film. 'produce' starts charged generation. 'status' advances production and reports progress; poll it every ~30s until it returns the finished film. 'redo' renders a new take of the listed shots (charged), then status re-saves and re-exports."),
|
|
1939
|
+
shots: zod.z.array(zod.z.string().trim().min(1).max(40)).min(1).max(12).optional().describe('Redo: the shots to render again, by the labels status reports ("2.3" is scene 2, shot 3) or shot id.'),
|
|
1940
|
+
redo: zod.z.enum(["clip", "frame"]).default("clip").describe("Redo: 'clip' (default) renders a new take of the clip from the same frame; 'frame' paints a new storyboard frame, then a new clip from it."),
|
|
1812
1941
|
cast: zod.z.array(
|
|
1813
1942
|
zod.z.object({
|
|
1814
1943
|
name: zod.z.string().trim().min(1).max(60),
|
|
1815
1944
|
image_urls: zod.z.array(zod.z.string().url().startsWith("https://").max(2048)).min(1).max(8).optional(),
|
|
1816
1945
|
prompt: zod.z.string().trim().min(1).max(600).optional(),
|
|
1817
|
-
character_id: zod.z.string().max(40).optional()
|
|
1946
|
+
character_id: zod.z.string().max(40).optional(),
|
|
1947
|
+
replace: zod.z.boolean().optional(),
|
|
1948
|
+
remove: zod.z.boolean().optional()
|
|
1818
1949
|
})
|
|
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."),
|
|
1950
|
+
).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. replace: true swaps out whoever already plays that name; remove: true takes them off the film."),
|
|
1820
1951
|
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."),
|
|
1821
1952
|
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.")
|
|
1822
1953
|
});
|
|
@@ -1841,16 +1972,27 @@ var init_film = __esm({
|
|
|
1841
1972
|
lyrics: { type: "string", description: "Plan step, music video mode (optional): the song lyrics, so shots land on the right lines." },
|
|
1842
1973
|
aspect: { type: "string", enum: ["16:9", "9:16"], description: "Plan step: frame aspect ratio. Default 16:9." },
|
|
1843
1974
|
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." },
|
|
1844
|
-
film_id: { type: "string", description: 'The film_id returned by the plan step. Required for produce and
|
|
1975
|
+
film_id: { type: "string", description: 'The film_id returned by the plan step. Required for produce, status and redo; with it, action "plan" is treated as "status".' },
|
|
1845
1976
|
action: {
|
|
1846
1977
|
type: "string",
|
|
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."
|
|
1978
|
+
enum: ["plan", "produce", "status", "cast", "redo"],
|
|
1979
|
+
description: "plan (default, free): writes the script and plans shots, returns film_id, a cost estimate, and any characters still to cast. cast: casts, replaces or removes 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. redo (charged): renders a new take of the shots listed in `shots`, on a film in production or already finished; then call status, which saves the new take and cuts a new export with it."
|
|
1980
|
+
},
|
|
1981
|
+
shots: {
|
|
1982
|
+
type: "array",
|
|
1983
|
+
items: { type: "string" },
|
|
1984
|
+
maxItems: 12,
|
|
1985
|
+
description: 'Redo only: the shots to render again, by the labels status and the finished film report ("2.3" is scene 2, shot 3), or by shot id.'
|
|
1986
|
+
},
|
|
1987
|
+
redo: {
|
|
1988
|
+
type: "string",
|
|
1989
|
+
enum: ["clip", "frame"],
|
|
1990
|
+
description: "Redo only: 'clip' (default) renders a new take of the clip from the same storyboard frame, the fix for a glitch, bad motion or lip sync. 'frame' paints a new frame first (and then a new clip from it), the fix for a wrong look, face or composition. Each is charged like any render of that shot. To change what a shot shows, edit it on the storyboard."
|
|
1849
1991
|
},
|
|
1850
1992
|
cast: {
|
|
1851
1993
|
type: "array",
|
|
1852
1994
|
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.',
|
|
1995
|
+
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. A name that is already cast is left alone unless the entry sets replace: true, which hands that part (every shot, outfit and voice) to the new cast member. remove: true with only the name takes that cast member off the film; the part then needs a new cast member before frames render. replace and remove work until the shots of that cast member are rendered; after that, recast on the storyboard, where the frames that show them can be redone.',
|
|
1854
1996
|
items: {
|
|
1855
1997
|
type: "object",
|
|
1856
1998
|
properties: {
|
|
@@ -1860,7 +2002,15 @@ var init_film = __esm({
|
|
|
1860
2002
|
type: "string",
|
|
1861
2003
|
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
2004
|
},
|
|
1863
|
-
character_id: { type: "string", description: "A Character Studio character id." }
|
|
2005
|
+
character_id: { type: "string", description: "A Character Studio character id." },
|
|
2006
|
+
replace: {
|
|
2007
|
+
type: "boolean",
|
|
2008
|
+
description: "Swap out whoever already plays this name for the image_urls, prompt or character_id given here, before their shots are rendered. Without it, an already-cast name is left unchanged."
|
|
2009
|
+
},
|
|
2010
|
+
remove: {
|
|
2011
|
+
type: "boolean",
|
|
2012
|
+
description: "Take whoever plays this name off the film and every shot (free), before their shots are rendered. Give only the name."
|
|
2013
|
+
}
|
|
1864
2014
|
},
|
|
1865
2015
|
required: ["name"]
|
|
1866
2016
|
}
|
|
@@ -1877,6 +2027,7 @@ var init_film = __esm({
|
|
|
1877
2027
|
}
|
|
1878
2028
|
};
|
|
1879
2029
|
shots = (f) => f.scenes.flatMap((s) => s.shots);
|
|
2030
|
+
benignDispatchRefusal = (status2, error) => status2 === 429 || status2 === 409 && /already generating/i.test(error ?? "");
|
|
1880
2031
|
count = (f, pred) => shots(f).filter(pred).length;
|
|
1881
2032
|
frameDone = (sh) => Boolean(sh.frameUrl) && sh.status !== "idle" && sh.status !== "frame-generating";
|
|
1882
2033
|
RETRY_CLEARS = /automatic audio check flagged|output audio\b[\s\S]*?sensitive information|temporarily unavailable/i;
|
|
@@ -1907,8 +2058,8 @@ var init_film = __esm({
|
|
|
1907
2058
|
};
|
|
1908
2059
|
filmSkill = {
|
|
1909
2060
|
name: "pixeldojo_film",
|
|
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).",
|
|
2061
|
+
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; cast entries can also replace or remove someone. Phased: plan is free; cast; produce starts charged generation; status advances production (poll every ~30s); redo renders a new take of chosen shots and status re-exports.",
|
|
2062
|
+
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; cast entries can also replace or remove someone. Phased: plan is free; cast; produce starts charged generation; status advances production (poll every ~30s); redo renders a new take of chosen shots and status re-exports.",
|
|
1912
2063
|
inputZod: InputSchema8,
|
|
1913
2064
|
inputJsonSchema: InputJsonSchema8,
|
|
1914
2065
|
allowedModels: [],
|
|
@@ -2122,7 +2273,7 @@ var init_video = __esm({
|
|
|
2122
2273
|
},
|
|
2123
2274
|
prompt: {
|
|
2124
2275
|
type: "string",
|
|
2125
|
-
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)."
|
|
2276
|
+
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). @Name uses a character or element the user saved in Character Studio (photos attached automatically)."
|
|
2126
2277
|
},
|
|
2127
2278
|
video_url: {
|
|
2128
2279
|
type: "string",
|
|
@@ -2725,7 +2876,7 @@ async function run13(input, ctx) {
|
|
|
2725
2876
|
);
|
|
2726
2877
|
return {
|
|
2727
2878
|
kind: "handoff",
|
|
2728
|
-
text: `Campaign ${result.campaignId}: ` + (stillRunning.length > 0 ? `${stillRunning.length} of ${result.subJobs?.length ?? 0} sub-jobs still ${stillRunning[0].status}. Wait a few seconds, then poll again.` : `aggregate state pending. Poll again shortly.`),
|
|
2879
|
+
text: `Campaign ${result.campaignId}: ` + (result.progress ? `${result.progress}. Wait a few seconds, then poll again.` : stillRunning.length > 0 ? `${stillRunning.length} of ${result.subJobs?.length ?? 0} sub-jobs still ${stillRunning[0].status}. Wait a few seconds, then poll again.` : `aggregate state pending. Poll again shortly.`),
|
|
2729
2880
|
jobId: result.campaignId,
|
|
2730
2881
|
statusUrl: result.statusUrl ?? ctx.buildStatusUrl(result.campaignId)
|
|
2731
2882
|
};
|
|
@@ -2905,6 +3056,26 @@ var init_audio = __esm({
|
|
|
2905
3056
|
}
|
|
2906
3057
|
});
|
|
2907
3058
|
async function run16(input, ctx) {
|
|
3059
|
+
if (input.source === "characters") {
|
|
3060
|
+
const { characters } = await ctx.client.listCharacters({ q: input.q, limit: input.limit ?? 25 });
|
|
3061
|
+
if (characters.length === 0) {
|
|
3062
|
+
return {
|
|
3063
|
+
kind: "text",
|
|
3064
|
+
text: input.q ? `No saved character or element matches "${input.q}".` : "No saved characters or elements yet. Save one from photos with pixeldojo_save_character."
|
|
3065
|
+
};
|
|
3066
|
+
}
|
|
3067
|
+
const lines2 = characters.map(
|
|
3068
|
+
(c) => `- ${c.mention} (${c.kind})${c.description ? `: ${c.description.slice(0, 100)}` : ""}
|
|
3069
|
+
images: ${c.image_urls.slice(0, 4).join(" ")}`
|
|
3070
|
+
);
|
|
3071
|
+
return {
|
|
3072
|
+
kind: "text",
|
|
3073
|
+
text: `${characters.length} saved character(s)/element(s):
|
|
3074
|
+
${lines2.join("\n")}
|
|
3075
|
+
|
|
3076
|
+
Write the @Name in a pixeldojo_generate / pixeldojo_video / pixeldojo_edit prompt and their photos are attached automatically (models that take reference images).`
|
|
3077
|
+
};
|
|
3078
|
+
}
|
|
2908
3079
|
const res = await ctx.client.listLibrary({
|
|
2909
3080
|
q: input.q,
|
|
2910
3081
|
model: input.model,
|
|
@@ -2939,6 +3110,7 @@ var InputSchema16, InputJsonSchema16, librarySkill;
|
|
|
2939
3110
|
var init_library = __esm({
|
|
2940
3111
|
"src/skills/library.ts"() {
|
|
2941
3112
|
InputSchema16 = zod.z.object({
|
|
3113
|
+
source: zod.z.enum(["media", "characters"]).optional().describe("media (default): saved and recent results. characters: saved Character Studio characters and elements, usable as @Name in prompts."),
|
|
2942
3114
|
q: zod.z.string().optional().describe("Substring match against the prompt that produced the asset."),
|
|
2943
3115
|
model: zod.z.string().optional().describe("Restrict to a single model (apiId)."),
|
|
2944
3116
|
modality: zod.z.enum(["image", "video", "audio"]).optional().describe("Restrict to one output type."),
|
|
@@ -2947,6 +3119,11 @@ var init_library = __esm({
|
|
|
2947
3119
|
InputJsonSchema16 = {
|
|
2948
3120
|
type: "object",
|
|
2949
3121
|
properties: {
|
|
3122
|
+
source: {
|
|
3123
|
+
type: "string",
|
|
3124
|
+
enum: ["media", "characters"],
|
|
3125
|
+
description: "media (default): saved media and recent generations. characters: the saved Character Studio characters, products, outfits, locations, props and styles. Write @Name in any prompt to use one."
|
|
3126
|
+
},
|
|
2950
3127
|
q: {
|
|
2951
3128
|
type: "string",
|
|
2952
3129
|
description: "Substring match against the prompt that produced the asset. Omit to list the newest items."
|
|
@@ -2963,7 +3140,7 @@ var init_library = __esm({
|
|
|
2963
3140
|
};
|
|
2964
3141
|
librarySkill = {
|
|
2965
3142
|
name: "pixeldojo_library",
|
|
2966
|
-
description: "Search your Library: saved media (permanent) and recent API/MCP generations (deleted 1 hour after they finish),
|
|
3143
|
+
description: "Search your Library, newest first. source=media (default): saved media (permanent) and recent API/MCP generations (deleted 1 hour after they finish), as URLs to reuse as references. source=characters: saved Character Studio characters and elements, each usable as @Name in any prompt.",
|
|
2967
3144
|
inputZod: InputSchema16,
|
|
2968
3145
|
inputJsonSchema: InputJsonSchema16,
|
|
2969
3146
|
allowedModels: [],
|
|
@@ -2972,6 +3149,100 @@ var init_library = __esm({
|
|
|
2972
3149
|
};
|
|
2973
3150
|
}
|
|
2974
3151
|
});
|
|
3152
|
+
async function run17(input, ctx) {
|
|
3153
|
+
const res = await ctx.client.saveMedia({
|
|
3154
|
+
url: input.url,
|
|
3155
|
+
prompt: input.prompt,
|
|
3156
|
+
tool_name: input.tool_name,
|
|
3157
|
+
modality: input.modality,
|
|
3158
|
+
job_id: input.job_id
|
|
3159
|
+
});
|
|
3160
|
+
return {
|
|
3161
|
+
kind: "text",
|
|
3162
|
+
text: `Saved to My Media (permanent): ${res.url}
|
|
3163
|
+
It will appear at https://pixeldojo.ai/my-images and in pixeldojo_library searches.`
|
|
3164
|
+
};
|
|
3165
|
+
}
|
|
3166
|
+
var InputSchema17, InputJsonSchema17, saveSkill;
|
|
3167
|
+
var init_save = __esm({
|
|
3168
|
+
"src/skills/save.ts"() {
|
|
3169
|
+
InputSchema17 = zod.z.object({
|
|
3170
|
+
url: zod.z.string().url().describe("The asset URL to save (image or video)."),
|
|
3171
|
+
prompt: zod.z.string().optional().describe("The prompt that produced it \u2014 makes it findable in the library later."),
|
|
3172
|
+
tool_name: zod.z.string().optional().describe("Tool/model slug that produced it."),
|
|
3173
|
+
modality: zod.z.enum(["image", "video", "audio"]).optional(),
|
|
3174
|
+
job_id: zod.z.string().optional().describe("The jobId that produced this asset, when known.")
|
|
3175
|
+
});
|
|
3176
|
+
InputJsonSchema17 = {
|
|
3177
|
+
type: "object",
|
|
3178
|
+
properties: {
|
|
3179
|
+
url: { type: "string", format: "uri", description: "The result URL to keep (image or video from a PixelDojo job)." },
|
|
3180
|
+
prompt: { type: "string", description: "The prompt that produced it, so it is findable in the library later." },
|
|
3181
|
+
tool_name: { type: "string", description: "Model apiId that produced it, when known." },
|
|
3182
|
+
modality: { type: "string", enum: ["image", "video", "audio"], description: "Output type, when the URL does not make it obvious." },
|
|
3183
|
+
job_id: { type: "string", description: "The jobId that produced it, when known." }
|
|
3184
|
+
},
|
|
3185
|
+
required: ["url"]
|
|
3186
|
+
};
|
|
3187
|
+
saveSkill = {
|
|
3188
|
+
name: "pixeldojo_save",
|
|
3189
|
+
description: `Save a generated image or video into the user's My Media library permanently ("save this", "keep it"). Results otherwise expire 1 hour after they finish. Pass the result URL; it is safety-checked before it is kept.`,
|
|
3190
|
+
inputZod: InputSchema17,
|
|
3191
|
+
inputJsonSchema: InputJsonSchema17,
|
|
3192
|
+
allowedModels: [],
|
|
3193
|
+
defaultModel: "",
|
|
3194
|
+
run: run17
|
|
3195
|
+
};
|
|
3196
|
+
}
|
|
3197
|
+
});
|
|
3198
|
+
async function run18(input, ctx) {
|
|
3199
|
+
const res = await ctx.client.saveCharacter({
|
|
3200
|
+
name: input.name,
|
|
3201
|
+
kind: input.kind ?? "character",
|
|
3202
|
+
image_urls: input.image_urls,
|
|
3203
|
+
description: input.description
|
|
3204
|
+
});
|
|
3205
|
+
return {
|
|
3206
|
+
kind: "text",
|
|
3207
|
+
text: `Saved ${res.kind} "${res.name}" to Character Studio (${res.image_urls.length} image${res.image_urls.length === 1 ? "" : "s"}). Use it in any prompt as ${res.mention}, for example "${res.mention} walking on a beach at sunset". It also appears at https://pixeldojo.ai/characters.`
|
|
3208
|
+
};
|
|
3209
|
+
}
|
|
3210
|
+
var KINDS, InputSchema18, InputJsonSchema18, saveCharacterSkill;
|
|
3211
|
+
var init_save_character = __esm({
|
|
3212
|
+
"src/skills/save-character.ts"() {
|
|
3213
|
+
KINDS = ["character", "product", "outfit", "location", "object", "style"];
|
|
3214
|
+
InputSchema18 = zod.z.object({
|
|
3215
|
+
name: zod.z.string().min(1).max(60).describe("What to call it. Used as @Name in prompts."),
|
|
3216
|
+
kind: zod.z.enum(KINDS).optional().describe("Default character."),
|
|
3217
|
+
image_urls: zod.z.array(zod.z.string().url()).max(8).optional().describe("3 to 5 photos from different angles works best for a character."),
|
|
3218
|
+
description: zod.z.string().max(1500).optional().describe("Appearance notes, or the scene text for an element.")
|
|
3219
|
+
});
|
|
3220
|
+
InputJsonSchema18 = {
|
|
3221
|
+
type: "object",
|
|
3222
|
+
properties: {
|
|
3223
|
+
name: { type: "string", description: "What to call it. Used as @Name in prompts afterwards." },
|
|
3224
|
+
kind: { type: "string", enum: KINDS, description: "character (default), product, outfit, location, object (a prop) or style." },
|
|
3225
|
+
image_urls: {
|
|
3226
|
+
type: "array",
|
|
3227
|
+
items: { type: "string", format: "uri" },
|
|
3228
|
+
maxItems: 8,
|
|
3229
|
+
description: "Photos of it. For a person, 3 to 5 clear photos from different angles. Required for a character."
|
|
3230
|
+
},
|
|
3231
|
+
description: { type: "string", description: "Optional appearance notes, or the scene text for an element without photos." }
|
|
3232
|
+
},
|
|
3233
|
+
required: ["name"]
|
|
3234
|
+
};
|
|
3235
|
+
saveCharacterSkill = {
|
|
3236
|
+
name: "pixeldojo_save_character",
|
|
3237
|
+
description: "Save a character (a person, from 3-5 photos) or an element (product, outfit, location, prop, style) to the user's Character Studio library. Afterwards write @Name in any prompt to use it. List saved ones with pixeldojo_library source=characters.",
|
|
3238
|
+
inputZod: InputSchema18,
|
|
3239
|
+
inputJsonSchema: InputJsonSchema18,
|
|
3240
|
+
allowedModels: [],
|
|
3241
|
+
defaultModel: "",
|
|
3242
|
+
run: run18
|
|
3243
|
+
};
|
|
3244
|
+
}
|
|
3245
|
+
});
|
|
2975
3246
|
function substitutePlaceholders(args, inputs, prevUrl) {
|
|
2976
3247
|
const missing = [];
|
|
2977
3248
|
const fill = (value) => {
|
|
@@ -3228,6 +3499,8 @@ __export(skills_exports, {
|
|
|
3228
3499
|
librarySkill: () => librarySkill,
|
|
3229
3500
|
listWorkflowsSkill: () => listWorkflowsSkill,
|
|
3230
3501
|
runWorkflowSkill: () => runWorkflowSkill,
|
|
3502
|
+
saveCharacterSkill: () => saveCharacterSkill,
|
|
3503
|
+
saveSkill: () => saveSkill,
|
|
3231
3504
|
saveWorkflowSkill: () => saveWorkflowSkill,
|
|
3232
3505
|
statusSkill: () => statusSkill,
|
|
3233
3506
|
storyboardSkill: () => storyboardSkill,
|
|
@@ -3273,6 +3546,8 @@ var init_skills = __esm({
|
|
|
3273
3546
|
init_upload();
|
|
3274
3547
|
init_audio();
|
|
3275
3548
|
init_library();
|
|
3549
|
+
init_save();
|
|
3550
|
+
init_save_character();
|
|
3276
3551
|
init_workflows();
|
|
3277
3552
|
ALL_SKILLS = [
|
|
3278
3553
|
generateSkill,
|
|
@@ -3282,6 +3557,8 @@ var init_skills = __esm({
|
|
|
3282
3557
|
upscaleSkill,
|
|
3283
3558
|
audioSkill,
|
|
3284
3559
|
librarySkill,
|
|
3560
|
+
saveSkill,
|
|
3561
|
+
saveCharacterSkill,
|
|
3285
3562
|
saveWorkflowSkill,
|
|
3286
3563
|
runWorkflowSkill,
|
|
3287
3564
|
listWorkflowsSkill,
|
|
@@ -3329,6 +3606,8 @@ var init_skills = __esm({
|
|
|
3329
3606
|
pixeldojo_status: read("Check job status"),
|
|
3330
3607
|
pixeldojo_campaign_status: read("Check campaign status"),
|
|
3331
3608
|
pixeldojo_library: read("Search media library"),
|
|
3609
|
+
pixeldojo_save: render("Save to My Media"),
|
|
3610
|
+
pixeldojo_save_character: render("Save character or element"),
|
|
3332
3611
|
pixeldojo_list_workflows: read("List saved workflows"),
|
|
3333
3612
|
pixeldojo_from_url: { ...read("Read product page"), openWorldHint: true }
|
|
3334
3613
|
};
|
|
@@ -3385,8 +3664,8 @@ function makeRedactedLogger(target = console) {
|
|
|
3385
3664
|
}
|
|
3386
3665
|
|
|
3387
3666
|
// src/server.ts
|
|
3388
|
-
var PACKAGE_VERSION = "0.
|
|
3389
|
-
var SERVER_INSTRUCTIONS = "
|
|
3667
|
+
var PACKAGE_VERSION = "0.19.0" ;
|
|
3668
|
+
var SERVER_INSTRUCTIONS = "Pixel Dojo generates images, video, audio and short films from one catalog of AI models, billed in credits from the connected Pixel Dojo account. A job that runs past the 30-second wait returns a jobId to check later; check it rather than submitting again, since every new submission is charged. Generated asset URLs are deleted one hour after they are created: pixeldojo_save keeps one in the user's My Media permanently (after a safety check), and finished films stay in the account library. In any prompt, @Name uses a character, product, outfit, location, prop or style the user saved in Character Studio: its photos are attached automatically. pixeldojo_library source=characters lists them, and pixeldojo_save_character saves a new one from 3 to 5 photos.";
|
|
3390
3669
|
function buildServer(options) {
|
|
3391
3670
|
if (!options.apiKey) {
|
|
3392
3671
|
throw new Error(
|