@pixeldojo/mcp 0.18.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 +304 -45
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +304 -45
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +304 -45
- 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 +304 -45
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -593,7 +593,7 @@ var init_generate = __esm({
|
|
|
593
593
|
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.";
|
|
594
594
|
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.";
|
|
595
595
|
InputSchema = z.object({
|
|
596
|
-
prompt: z.string().min(1).max(8e3).describe(
|
|
596
|
+
prompt: 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.'),
|
|
597
597
|
model: z.string().optional().describe(
|
|
598
598
|
`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.`
|
|
599
599
|
),
|
|
@@ -633,7 +633,7 @@ var init_generate = __esm({
|
|
|
633
633
|
InputJsonSchema = {
|
|
634
634
|
type: "object",
|
|
635
635
|
properties: {
|
|
636
|
-
prompt: { type: "string", description:
|
|
636
|
+
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.' },
|
|
637
637
|
model: {
|
|
638
638
|
type: "string",
|
|
639
639
|
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.`
|
|
@@ -847,7 +847,7 @@ var init_edit = __esm({
|
|
|
847
847
|
InputJsonSchema2 = {
|
|
848
848
|
type: "object",
|
|
849
849
|
properties: {
|
|
850
|
-
prompt: { type: "string", description:
|
|
850
|
+
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.' },
|
|
851
851
|
image_url: {
|
|
852
852
|
type: "string",
|
|
853
853
|
format: "uri",
|
|
@@ -1453,6 +1453,13 @@ async function filmApi(ctx, method, path, body) {
|
|
|
1453
1453
|
const data = await res.json().catch(() => ({}));
|
|
1454
1454
|
return { status: res.status, data };
|
|
1455
1455
|
}
|
|
1456
|
+
function keepGoing(filmId, afterSec, extra = {}) {
|
|
1457
|
+
const set = Object.fromEntries(Object.entries(extra).filter(([, v]) => v !== void 0));
|
|
1458
|
+
return { input: { film_id: filmId, action: "status", ...set }, afterSec };
|
|
1459
|
+
}
|
|
1460
|
+
function shotLabels(f) {
|
|
1461
|
+
return new Map(f.scenes.flatMap((s, si) => s.shots.map((sh, i) => [sh.id, `${si + 1}.${i + 1}`])));
|
|
1462
|
+
}
|
|
1456
1463
|
async function applyCast(ctx, filmId, cast) {
|
|
1457
1464
|
let charged = false;
|
|
1458
1465
|
const lines = await Promise.all(cast.map(async (c) => {
|
|
@@ -1572,12 +1579,13 @@ async function plan(input, ctx) {
|
|
|
1572
1579
|
const castStarted = Date.now();
|
|
1573
1580
|
const cast = input.cast?.length ? await applyCast(ctx, id, input.cast) : { lines: [], charged: false };
|
|
1574
1581
|
const pollBudget = Math.max(0, PLANNER_MAX_POLLS - Math.ceil((Date.now() - castStarted) / PLANNER_POLL_MS));
|
|
1575
|
-
const reply = (text) => ({
|
|
1582
|
+
const reply = (text, followUp) => ({
|
|
1576
1583
|
kind: "text",
|
|
1577
1584
|
text: [
|
|
1578
1585
|
...cast.lines,
|
|
1579
1586
|
cast.charged ? text.replace(/Nothing has been charged( yet)?\./g, "Only the cast portraits above have been charged.") : text
|
|
1580
|
-
].join("\n")
|
|
1587
|
+
].join("\n"),
|
|
1588
|
+
...followUp ? { followUp } : {}
|
|
1581
1589
|
});
|
|
1582
1590
|
let planned;
|
|
1583
1591
|
if (music) {
|
|
@@ -1588,14 +1596,14 @@ ${input.lyrics.trim()}` : ""].filter(Boolean).join("\n\n");
|
|
|
1588
1596
|
let script = input.script?.trim() ?? "";
|
|
1589
1597
|
if (!script) {
|
|
1590
1598
|
const wrote = await runPlanner(ctx, id, `/api/films/${id}/script`, { idea: input.idea.trim() }, pollBudget);
|
|
1591
|
-
if ("pending" in wrote) return reply(stillPlanningText(id, "script"));
|
|
1599
|
+
if ("pending" in wrote) return reply(stillPlanningText(id, "script"), keepGoing(id, 30, { soundtrack: input.soundtrack }));
|
|
1592
1600
|
if ("error" in wrote) return reply(`Script writing failed: ${wrote.error}`);
|
|
1593
1601
|
script = wrote.film.script ?? "";
|
|
1594
1602
|
if (!script) return reply("Script writing failed: the studio returned an empty script.");
|
|
1595
1603
|
}
|
|
1596
1604
|
planned = await runPlanner(ctx, id, `/api/films/${id}/plan`, { script });
|
|
1597
1605
|
}
|
|
1598
|
-
if ("pending" in planned) return reply(stillPlanningText(id));
|
|
1606
|
+
if ("pending" in planned) return reply(stillPlanningText(id), keepGoing(id, 30, { soundtrack: input.soundtrack }));
|
|
1599
1607
|
if ("error" in planned) return reply(`Shot planning failed: ${planned.error}`);
|
|
1600
1608
|
const film = planned.film;
|
|
1601
1609
|
if (needsSlice(film)) {
|
|
@@ -1608,17 +1616,17 @@ ${err} Call action "status" to retry the cut.`);
|
|
|
1608
1616
|
return reply([plannedText(film, ctx), uncast.length ? `
|
|
1609
1617
|
${castNeededText(id, uncast)}` : ""].filter(Boolean).join("\n"));
|
|
1610
1618
|
}
|
|
1611
|
-
async function resumePlanning(film, ctx) {
|
|
1619
|
+
async function resumePlanning(film, ctx, soundtrack) {
|
|
1612
1620
|
if (film.scenes.length > 0 && film.llmJob?.status !== "running") return null;
|
|
1613
1621
|
if (film.llmJob?.status === "running") {
|
|
1614
|
-
return { kind: "text", text: stillPlanningText(film.id, film.llmJob.kind === "script" ? "script" : "plan") };
|
|
1622
|
+
return { kind: "text", text: stillPlanningText(film.id, film.llmJob.kind === "script" ? "script" : "plan"), followUp: keepGoing(film.id, 30, { soundtrack }) };
|
|
1615
1623
|
}
|
|
1616
1624
|
const prior = film.llmJob?.status === "error" ? `The last planning run failed (${film.llmJob.error ?? "unknown"}). ` : "";
|
|
1617
1625
|
if (!film.script?.trim()) {
|
|
1618
1626
|
return { kind: "text", text: `${prior}This film has nothing to plan from. Start a new one with action "plan".` };
|
|
1619
1627
|
}
|
|
1620
1628
|
const planned = await runPlanner(ctx, film.id, `/api/films/${film.id}/plan`, { script: film.script }, 0);
|
|
1621
|
-
if ("pending" in planned) return { kind: "text", text: `${prior}${stillPlanningText(film.id)}
|
|
1629
|
+
if ("pending" in planned) return { kind: "text", text: `${prior}${stillPlanningText(film.id)}`, followUp: keepGoing(film.id, 30, { soundtrack }) };
|
|
1622
1630
|
if ("error" in planned) return { kind: "text", text: `Shot planning failed: ${planned.error} Call action "status" to retry.` };
|
|
1623
1631
|
if (needsSlice(planned.film)) {
|
|
1624
1632
|
const err = await sliceSong(ctx, planned.film);
|
|
@@ -1628,22 +1636,30 @@ ${err} Call action "status" to retry the cut.` };
|
|
|
1628
1636
|
}
|
|
1629
1637
|
return { kind: "text", text: plannedText(planned.film, ctx) };
|
|
1630
1638
|
}
|
|
1631
|
-
async function produce(filmId, ctx) {
|
|
1639
|
+
async function produce(filmId, soundtrack, ctx) {
|
|
1632
1640
|
const { data } = await filmApi(ctx, "GET", `/api/films/${filmId}`);
|
|
1633
1641
|
if (!data.film) return { kind: "text", text: `Film not found: ${data.error ?? filmId}` };
|
|
1634
|
-
const planning = await resumePlanning(data.film, ctx);
|
|
1642
|
+
const planning = await resumePlanning(data.film, ctx, soundtrack);
|
|
1635
1643
|
if (planning) return planning;
|
|
1636
1644
|
const uncast = await uncastRolesOf(ctx, filmId);
|
|
1637
1645
|
if (uncast.length) return { kind: "text", text: `Nothing was charged. ${castNeededText(filmId, uncast)}` };
|
|
1638
1646
|
const pending = shots(data.film).filter((sh) => sh.status === "idle" || sh.status === "failed" && !sh.frameUrl);
|
|
1639
1647
|
let fired = 0;
|
|
1648
|
+
let refused;
|
|
1640
1649
|
for (const sh of pending.slice(0, 6)) {
|
|
1641
1650
|
const r = await filmApi(ctx, "POST", `/api/films/${filmId}/shots/${sh.id}/generate`, { kind: "frame" });
|
|
1642
1651
|
if (r.status === 200) fired += 1;
|
|
1652
|
+
else if (!benignDispatchRefusal(r.status, r.data.error)) refused ??= r.data.error ?? `HTTP ${r.status}`;
|
|
1653
|
+
}
|
|
1654
|
+
if (fired === 0 && refused) {
|
|
1655
|
+
return { kind: "text", text: `Production could not start on "${data.film.title}": ${refused}` };
|
|
1643
1656
|
}
|
|
1644
1657
|
return {
|
|
1645
1658
|
kind: "text",
|
|
1646
|
-
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)
|
|
1659
|
+
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.`,
|
|
1660
|
+
// The caller's soundtrack choice rides along: the follow-up status calls
|
|
1661
|
+
// are the ones that reach the score.
|
|
1662
|
+
followUp: keepGoing(filmId, 30, { soundtrack })
|
|
1647
1663
|
};
|
|
1648
1664
|
}
|
|
1649
1665
|
function buildScorePlanBody(film) {
|
|
@@ -1689,30 +1705,49 @@ async function advance(filmId, soundtrack, musicJobId, ctx) {
|
|
|
1689
1705
|
if (film.status === "exported" && film.exportUrl) {
|
|
1690
1706
|
return {
|
|
1691
1707
|
kind: "text",
|
|
1692
|
-
text:
|
|
1708
|
+
text: [
|
|
1709
|
+
`Film finished: "${film.title}"
|
|
1693
1710
|
Watch/download: ${film.exportUrl}
|
|
1694
1711
|
Workspace: ${ctx.baseUrl}/film-studio/${film.id}
|
|
1695
|
-
It is saved in the Library folder "Film: ${film.title}"
|
|
1712
|
+
It is saved in the Library folder "Film: ${film.title}".`,
|
|
1713
|
+
shotIndex(film)
|
|
1714
|
+
].filter(Boolean).join("\n")
|
|
1696
1715
|
};
|
|
1697
1716
|
}
|
|
1698
|
-
|
|
1699
|
-
|
|
1717
|
+
const startExport = async () => {
|
|
1718
|
+
const r = await filmApi(ctx, "POST", `/api/films/${filmId}/export`, {});
|
|
1719
|
+
return r.status === 200 || r.status === 409 ? null : r.data.error ?? `HTTP ${r.status}`;
|
|
1720
|
+
};
|
|
1721
|
+
if (film.exportError && all.length > 0 && all.every((sh) => sh.status === "clip-ready")) {
|
|
1722
|
+
const again = await startExport();
|
|
1723
|
+
return {
|
|
1724
|
+
kind: "text",
|
|
1725
|
+
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.`
|
|
1726
|
+
};
|
|
1700
1727
|
}
|
|
1701
|
-
const planning = await resumePlanning(film, ctx);
|
|
1728
|
+
const planning = await resumePlanning(film, ctx, soundtrack);
|
|
1702
1729
|
if (planning) return planning;
|
|
1703
1730
|
if (needsSlice(film)) {
|
|
1704
1731
|
const err = await sliceSong(ctx, film);
|
|
1705
1732
|
if (err) return { kind: "text", text: `${err} Poll again with action "status" to retry.` };
|
|
1706
|
-
return {
|
|
1733
|
+
return {
|
|
1734
|
+
kind: "text",
|
|
1735
|
+
text: `Song cut into one slice per shot. Poll again with {"film_id": "${filmId}", "action": "status"} to keep production moving.`,
|
|
1736
|
+
followUp: keepGoing(filmId, 5, { soundtrack })
|
|
1737
|
+
};
|
|
1707
1738
|
}
|
|
1708
1739
|
if (shots(film).length > 0 && shots(film).every((sh) => sh.status === "idle")) {
|
|
1709
1740
|
return { kind: "text", text: plannedText(film, ctx) };
|
|
1710
1741
|
}
|
|
1711
1742
|
let started = 0;
|
|
1712
1743
|
const resent = /* @__PURE__ */ new Set();
|
|
1744
|
+
const refusedStarts = [];
|
|
1713
1745
|
const dispatch = async (sh, kind) => {
|
|
1714
1746
|
const r = await filmApi(ctx, "POST", `/api/films/${filmId}/shots/${sh.id}/generate`, { kind });
|
|
1715
|
-
if (r.status !== 200)
|
|
1747
|
+
if (r.status !== 200) {
|
|
1748
|
+
if (!benignDispatchRefusal(r.status, r.data.error)) refusedStarts.push(r.data.error ?? `HTTP ${r.status}`);
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1716
1751
|
started += 1;
|
|
1717
1752
|
if (sh.status === "failed") resent.add(sh.id);
|
|
1718
1753
|
};
|
|
@@ -1742,27 +1777,47 @@ It is saved in the Library folder "Film: ${film.title}".`
|
|
|
1742
1777
|
const outUrl = out?.audio ?? out?.video ?? out?.images?.[0];
|
|
1743
1778
|
if (job.status === "completed" && typeof outUrl === "string") {
|
|
1744
1779
|
await filmApi(ctx, "PATCH", `/api/films/${filmId}`, { audioStems: [{ kind: "music", url: outUrl }] });
|
|
1745
|
-
|
|
1746
|
-
return { kind: "text", text: `Score attached
|
|
1780
|
+
const exportError2 = await startExport();
|
|
1781
|
+
if (exportError2) return { kind: "text", text: `Score attached, but the export could not start: ${exportError2}` };
|
|
1782
|
+
return {
|
|
1783
|
+
kind: "text",
|
|
1784
|
+
text: `Score attached \u2014 export started. Poll again in ~60s with {"film_id": "${filmId}", "action": "status"} for the finished film URL.`,
|
|
1785
|
+
followUp: keepGoing(filmId, 60, { soundtrack })
|
|
1786
|
+
};
|
|
1747
1787
|
}
|
|
1748
1788
|
if (job.status === "failed") {
|
|
1749
1789
|
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.` };
|
|
1750
1790
|
}
|
|
1751
|
-
return {
|
|
1791
|
+
return {
|
|
1792
|
+
kind: "text",
|
|
1793
|
+
text: `Score still composing. Poll again in ~20s with {"film_id": "${filmId}", "action": "status", "music_job_id": "${musicJobId}"}.`,
|
|
1794
|
+
followUp: keepGoing(filmId, 20, { soundtrack, music_job_id: musicJobId })
|
|
1795
|
+
};
|
|
1752
1796
|
}
|
|
1753
1797
|
const body = soundtrack === "scene-synced" ? buildScorePlanBody(film) : buildFlatMusicBody(film, all);
|
|
1754
1798
|
const dispatch2 = await ctx.client.run("text-to-music", body);
|
|
1755
|
-
return {
|
|
1799
|
+
return {
|
|
1800
|
+
kind: "text",
|
|
1801
|
+
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}"}.`,
|
|
1802
|
+
followUp: keepGoing(filmId, 20, { soundtrack, music_job_id: dispatch2.jobId })
|
|
1803
|
+
};
|
|
1756
1804
|
}
|
|
1757
|
-
|
|
1758
|
-
return { kind: "text", text: `All clips ready
|
|
1805
|
+
const exportError = await startExport();
|
|
1806
|
+
if (exportError) return { kind: "text", text: `All clips ready, but the export could not start: ${exportError}` };
|
|
1807
|
+
return {
|
|
1808
|
+
kind: "text",
|
|
1809
|
+
text: `All clips ready \u2014 export started (${progressLine(film)}). Poll again in ~60s with action "status" for the finished film URL.`,
|
|
1810
|
+
followUp: keepGoing(filmId, 60, { soundtrack })
|
|
1811
|
+
};
|
|
1759
1812
|
}
|
|
1760
1813
|
const failed = all.filter((sh) => sh.status === "failed");
|
|
1761
1814
|
const refused = failed.filter(isRefused);
|
|
1762
1815
|
const retrying = failed.filter((sh) => !isRefused(sh));
|
|
1763
1816
|
const sentAgain = retrying.filter((sh) => resent.has(sh.id)).length;
|
|
1764
|
-
const
|
|
1765
|
-
const
|
|
1817
|
+
const rendering = all.some((sh) => sh.status === "frame-generating" || sh.status === "clip-generating");
|
|
1818
|
+
const paused = refused.length > 0 && started === 0 && !rendering;
|
|
1819
|
+
const moving = started > 0 || rendering;
|
|
1820
|
+
const labels = shotLabels(film);
|
|
1766
1821
|
const reason = (sh) => {
|
|
1767
1822
|
const text = (sh.errorMessage ?? "no reason given").replace(/\s*Retry (?:the shot|it)\.\s*$/i, "");
|
|
1768
1823
|
return text.length > 300 ? `${text.slice(0, 297)}...` : text;
|
|
@@ -1774,14 +1829,75 @@ It is saved in the Library folder "Film: ${film.title}".`
|
|
|
1774
1829
|
`${paused ? "Paused" : "In progress"}: ${progressLine(film)}.`,
|
|
1775
1830
|
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}.` : "",
|
|
1776
1831
|
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"}.` : "",
|
|
1777
|
-
|
|
1778
|
-
|
|
1832
|
+
refusedStarts.length > 0 ? `${refusedStarts.length} render(s) could not start: ${refusedStarts[0]}` : "",
|
|
1833
|
+
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.`
|
|
1834
|
+
].filter(Boolean).join("\n"),
|
|
1835
|
+
...moving && !paused ? { followUp: keepGoing(filmId, 30, { soundtrack }) } : {}
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
function shotIndex(f) {
|
|
1839
|
+
const all = shots(f);
|
|
1840
|
+
if (all.length === 0 || all.length > 24) return "";
|
|
1841
|
+
const labels = shotLabels(f);
|
|
1842
|
+
let at = 0;
|
|
1843
|
+
const clock = (sec) => `${Math.floor(sec / 60)}:${String(Math.round(sec % 60)).padStart(2, "0")}`;
|
|
1844
|
+
const lines = all.map((sh) => {
|
|
1845
|
+
const from = at;
|
|
1846
|
+
at += sh.durationSec || 0;
|
|
1847
|
+
const what = sh.prompt.replace(/\s+/g, " ").trim();
|
|
1848
|
+
return ` ${labels.get(sh.id)} (about ${clock(from)}-${clock(at)}): ${what.length > 70 ? `${what.slice(0, 67)}...` : what}`;
|
|
1849
|
+
});
|
|
1850
|
+
return ['Shots (redo any with action "redo" and its label):', ...lines].join("\n");
|
|
1851
|
+
}
|
|
1852
|
+
async function redoShots(input, ctx) {
|
|
1853
|
+
const filmId = input.film_id;
|
|
1854
|
+
if (!input.shots?.length) {
|
|
1855
|
+
return { kind: "text", text: 'Pass shots: the labels status reports, like ["2.3"] for scene 2, shot 3.' };
|
|
1856
|
+
}
|
|
1857
|
+
const { data } = await filmApi(ctx, "GET", `/api/films/${filmId}`);
|
|
1858
|
+
const film = data.film;
|
|
1859
|
+
if (!film) return { kind: "text", text: `Film not found: ${data.error ?? filmId}` };
|
|
1860
|
+
const labels = shotLabels(film);
|
|
1861
|
+
const byLabel = new Map(shots(film).map((sh) => [labels.get(sh.id), sh]));
|
|
1862
|
+
const byId = new Map(shots(film).map((sh) => [sh.id, sh]));
|
|
1863
|
+
const kind = input.redo;
|
|
1864
|
+
const lines = [];
|
|
1865
|
+
let started = 0;
|
|
1866
|
+
let credits = 0;
|
|
1867
|
+
for (const ref of Array.from(new Set(input.shots))) {
|
|
1868
|
+
const sh = byLabel.get(ref) ?? byId.get(ref);
|
|
1869
|
+
if (!sh) {
|
|
1870
|
+
lines.push(`No shot ${ref} in this film (shots run 1.1 to ${Array.from(byLabel.keys()).pop()}).`);
|
|
1871
|
+
continue;
|
|
1872
|
+
}
|
|
1873
|
+
const label = labels.get(sh.id);
|
|
1874
|
+
if (kind === "clip" && !sh.frameUrl) {
|
|
1875
|
+
lines.push(`Shot ${label} has no frame yet, so there is no clip to redo. Redo it with redo "frame".`);
|
|
1876
|
+
continue;
|
|
1877
|
+
}
|
|
1878
|
+
const r = await filmApi(ctx, "POST", `/api/films/${filmId}/shots/${sh.id}/generate`, { kind });
|
|
1879
|
+
if (r.status === 200) {
|
|
1880
|
+
started += 1;
|
|
1881
|
+
credits += r.data.creditCost ?? 0;
|
|
1882
|
+
lines.push(`Shot ${label}: new ${kind} rendering${r.data.creditCost ? ` (${r.data.creditCost} credits)` : ""}.`);
|
|
1883
|
+
} else {
|
|
1884
|
+
lines.push(`Shot ${label}: could not start: ${r.data.error ?? `HTTP ${r.status}`}`);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
if (started === 0) return { kind: "text", text: [...lines, "Nothing was started."].join("\n") };
|
|
1888
|
+
return {
|
|
1889
|
+
kind: "text",
|
|
1890
|
+
text: [
|
|
1891
|
+
...lines,
|
|
1892
|
+
`${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.`
|
|
1893
|
+
].join("\n"),
|
|
1894
|
+
followUp: keepGoing(filmId, 30, { soundtrack: input.soundtrack })
|
|
1779
1895
|
};
|
|
1780
1896
|
}
|
|
1781
1897
|
async function run8(input, ctx) {
|
|
1782
1898
|
const action = input.action === "plan" && input.film_id ? input.cast?.length ? "cast" : "status" : input.action;
|
|
1783
1899
|
if (action === "plan") return plan(input, ctx);
|
|
1784
|
-
if (!input.film_id) return { kind: "text", text: "produce/status/cast need the film_id returned by the plan step." };
|
|
1900
|
+
if (!input.film_id) return { kind: "text", text: "produce/status/cast/redo need the film_id returned by the plan step." };
|
|
1785
1901
|
if (action === "cast") {
|
|
1786
1902
|
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)." };
|
|
1787
1903
|
const { lines } = await applyCast(ctx, input.film_id, input.cast);
|
|
@@ -1794,10 +1910,11 @@ async function run8(input, ctx) {
|
|
|
1794
1910
|
].join("\n")
|
|
1795
1911
|
};
|
|
1796
1912
|
}
|
|
1797
|
-
if (action === "produce") return produce(input.film_id, ctx);
|
|
1913
|
+
if (action === "produce") return produce(input.film_id, input.soundtrack, ctx);
|
|
1914
|
+
if (action === "redo") return redoShots(input, ctx);
|
|
1798
1915
|
return advance(input.film_id, input.soundtrack, input.music_job_id, ctx);
|
|
1799
1916
|
}
|
|
1800
|
-
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;
|
|
1917
|
+
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;
|
|
1801
1918
|
var init_film = __esm({
|
|
1802
1919
|
"src/skills/film.ts"() {
|
|
1803
1920
|
init_upload_helpers();
|
|
@@ -1815,8 +1932,10 @@ var init_film = __esm({
|
|
|
1815
1932
|
lyrics: z.string().max(1e4).optional().describe("Music video mode: the song lyrics, so shots land on the right lines. Optional."),
|
|
1816
1933
|
aspect: z.enum(["16:9", "9:16"]).default("16:9"),
|
|
1817
1934
|
length: z.enum(["30s", "60s", "2-3min", "5min"]).default("60s").describe("Target length; drives the shot count (~6/12/24/60 shots)."),
|
|
1818
|
-
film_id: z.string().max(40).optional().describe("Existing film id (from a previous call) for produce/status/cast."),
|
|
1819
|
-
action: z.enum(["plan", "produce", "status", "cast"]).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."),
|
|
1935
|
+
film_id: z.string().max(40).optional().describe("Existing film id (from a previous call) for produce/status/cast/redo."),
|
|
1936
|
+
action: 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."),
|
|
1937
|
+
shots: z.array(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.'),
|
|
1938
|
+
redo: 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."),
|
|
1820
1939
|
cast: z.array(
|
|
1821
1940
|
z.object({
|
|
1822
1941
|
name: z.string().trim().min(1).max(60),
|
|
@@ -1851,11 +1970,22 @@ var init_film = __esm({
|
|
|
1851
1970
|
lyrics: { type: "string", description: "Plan step, music video mode (optional): the song lyrics, so shots land on the right lines." },
|
|
1852
1971
|
aspect: { type: "string", enum: ["16:9", "9:16"], description: "Plan step: frame aspect ratio. Default 16:9." },
|
|
1853
1972
|
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." },
|
|
1854
|
-
film_id: { type: "string", description: 'The film_id returned by the plan step. Required for produce and
|
|
1973
|
+
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".' },
|
|
1855
1974
|
action: {
|
|
1856
1975
|
type: "string",
|
|
1857
|
-
enum: ["plan", "produce", "status", "cast"],
|
|
1858
|
-
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."
|
|
1976
|
+
enum: ["plan", "produce", "status", "cast", "redo"],
|
|
1977
|
+
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."
|
|
1978
|
+
},
|
|
1979
|
+
shots: {
|
|
1980
|
+
type: "array",
|
|
1981
|
+
items: { type: "string" },
|
|
1982
|
+
maxItems: 12,
|
|
1983
|
+
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.'
|
|
1984
|
+
},
|
|
1985
|
+
redo: {
|
|
1986
|
+
type: "string",
|
|
1987
|
+
enum: ["clip", "frame"],
|
|
1988
|
+
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."
|
|
1859
1989
|
},
|
|
1860
1990
|
cast: {
|
|
1861
1991
|
type: "array",
|
|
@@ -1895,6 +2025,7 @@ var init_film = __esm({
|
|
|
1895
2025
|
}
|
|
1896
2026
|
};
|
|
1897
2027
|
shots = (f) => f.scenes.flatMap((s) => s.shots);
|
|
2028
|
+
benignDispatchRefusal = (status2, error) => status2 === 429 || status2 === 409 && /already generating/i.test(error ?? "");
|
|
1898
2029
|
count = (f, pred) => shots(f).filter(pred).length;
|
|
1899
2030
|
frameDone = (sh) => Boolean(sh.frameUrl) && sh.status !== "idle" && sh.status !== "frame-generating";
|
|
1900
2031
|
RETRY_CLEARS = /automatic audio check flagged|output audio\b[\s\S]*?sensitive information|temporarily unavailable/i;
|
|
@@ -1925,8 +2056,8 @@ var init_film = __esm({
|
|
|
1925
2056
|
};
|
|
1926
2057
|
filmSkill = {
|
|
1927
2058
|
name: "pixeldojo_film",
|
|
1928
|
-
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).",
|
|
1929
|
-
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).",
|
|
2059
|
+
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.",
|
|
2060
|
+
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.",
|
|
1930
2061
|
inputZod: InputSchema8,
|
|
1931
2062
|
inputJsonSchema: InputJsonSchema8,
|
|
1932
2063
|
allowedModels: [],
|
|
@@ -2140,7 +2271,7 @@ var init_video = __esm({
|
|
|
2140
2271
|
},
|
|
2141
2272
|
prompt: {
|
|
2142
2273
|
type: "string",
|
|
2143
|
-
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)."
|
|
2274
|
+
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)."
|
|
2144
2275
|
},
|
|
2145
2276
|
video_url: {
|
|
2146
2277
|
type: "string",
|
|
@@ -2743,7 +2874,7 @@ async function run13(input, ctx) {
|
|
|
2743
2874
|
);
|
|
2744
2875
|
return {
|
|
2745
2876
|
kind: "handoff",
|
|
2746
|
-
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.`),
|
|
2877
|
+
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.`),
|
|
2747
2878
|
jobId: result.campaignId,
|
|
2748
2879
|
statusUrl: result.statusUrl ?? ctx.buildStatusUrl(result.campaignId)
|
|
2749
2880
|
};
|
|
@@ -2923,6 +3054,26 @@ var init_audio = __esm({
|
|
|
2923
3054
|
}
|
|
2924
3055
|
});
|
|
2925
3056
|
async function run16(input, ctx) {
|
|
3057
|
+
if (input.source === "characters") {
|
|
3058
|
+
const { characters } = await ctx.client.listCharacters({ q: input.q, limit: input.limit ?? 25 });
|
|
3059
|
+
if (characters.length === 0) {
|
|
3060
|
+
return {
|
|
3061
|
+
kind: "text",
|
|
3062
|
+
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."
|
|
3063
|
+
};
|
|
3064
|
+
}
|
|
3065
|
+
const lines2 = characters.map(
|
|
3066
|
+
(c) => `- ${c.mention} (${c.kind})${c.description ? `: ${c.description.slice(0, 100)}` : ""}
|
|
3067
|
+
images: ${c.image_urls.slice(0, 4).join(" ")}`
|
|
3068
|
+
);
|
|
3069
|
+
return {
|
|
3070
|
+
kind: "text",
|
|
3071
|
+
text: `${characters.length} saved character(s)/element(s):
|
|
3072
|
+
${lines2.join("\n")}
|
|
3073
|
+
|
|
3074
|
+
Write the @Name in a pixeldojo_generate / pixeldojo_video / pixeldojo_edit prompt and their photos are attached automatically (models that take reference images).`
|
|
3075
|
+
};
|
|
3076
|
+
}
|
|
2926
3077
|
const res = await ctx.client.listLibrary({
|
|
2927
3078
|
q: input.q,
|
|
2928
3079
|
model: input.model,
|
|
@@ -2957,6 +3108,7 @@ var InputSchema16, InputJsonSchema16, librarySkill;
|
|
|
2957
3108
|
var init_library = __esm({
|
|
2958
3109
|
"src/skills/library.ts"() {
|
|
2959
3110
|
InputSchema16 = z.object({
|
|
3111
|
+
source: z.enum(["media", "characters"]).optional().describe("media (default): saved and recent results. characters: saved Character Studio characters and elements, usable as @Name in prompts."),
|
|
2960
3112
|
q: z.string().optional().describe("Substring match against the prompt that produced the asset."),
|
|
2961
3113
|
model: z.string().optional().describe("Restrict to a single model (apiId)."),
|
|
2962
3114
|
modality: z.enum(["image", "video", "audio"]).optional().describe("Restrict to one output type."),
|
|
@@ -2965,6 +3117,11 @@ var init_library = __esm({
|
|
|
2965
3117
|
InputJsonSchema16 = {
|
|
2966
3118
|
type: "object",
|
|
2967
3119
|
properties: {
|
|
3120
|
+
source: {
|
|
3121
|
+
type: "string",
|
|
3122
|
+
enum: ["media", "characters"],
|
|
3123
|
+
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."
|
|
3124
|
+
},
|
|
2968
3125
|
q: {
|
|
2969
3126
|
type: "string",
|
|
2970
3127
|
description: "Substring match against the prompt that produced the asset. Omit to list the newest items."
|
|
@@ -2981,7 +3138,7 @@ var init_library = __esm({
|
|
|
2981
3138
|
};
|
|
2982
3139
|
librarySkill = {
|
|
2983
3140
|
name: "pixeldojo_library",
|
|
2984
|
-
description: "Search your Library: saved media (permanent) and recent API/MCP generations (deleted 1 hour after they finish),
|
|
3141
|
+
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.",
|
|
2985
3142
|
inputZod: InputSchema16,
|
|
2986
3143
|
inputJsonSchema: InputJsonSchema16,
|
|
2987
3144
|
allowedModels: [],
|
|
@@ -2990,6 +3147,100 @@ var init_library = __esm({
|
|
|
2990
3147
|
};
|
|
2991
3148
|
}
|
|
2992
3149
|
});
|
|
3150
|
+
async function run17(input, ctx) {
|
|
3151
|
+
const res = await ctx.client.saveMedia({
|
|
3152
|
+
url: input.url,
|
|
3153
|
+
prompt: input.prompt,
|
|
3154
|
+
tool_name: input.tool_name,
|
|
3155
|
+
modality: input.modality,
|
|
3156
|
+
job_id: input.job_id
|
|
3157
|
+
});
|
|
3158
|
+
return {
|
|
3159
|
+
kind: "text",
|
|
3160
|
+
text: `Saved to My Media (permanent): ${res.url}
|
|
3161
|
+
It will appear at https://pixeldojo.ai/my-images and in pixeldojo_library searches.`
|
|
3162
|
+
};
|
|
3163
|
+
}
|
|
3164
|
+
var InputSchema17, InputJsonSchema17, saveSkill;
|
|
3165
|
+
var init_save = __esm({
|
|
3166
|
+
"src/skills/save.ts"() {
|
|
3167
|
+
InputSchema17 = z.object({
|
|
3168
|
+
url: z.string().url().describe("The asset URL to save (image or video)."),
|
|
3169
|
+
prompt: z.string().optional().describe("The prompt that produced it \u2014 makes it findable in the library later."),
|
|
3170
|
+
tool_name: z.string().optional().describe("Tool/model slug that produced it."),
|
|
3171
|
+
modality: z.enum(["image", "video", "audio"]).optional(),
|
|
3172
|
+
job_id: z.string().optional().describe("The jobId that produced this asset, when known.")
|
|
3173
|
+
});
|
|
3174
|
+
InputJsonSchema17 = {
|
|
3175
|
+
type: "object",
|
|
3176
|
+
properties: {
|
|
3177
|
+
url: { type: "string", format: "uri", description: "The result URL to keep (image or video from a PixelDojo job)." },
|
|
3178
|
+
prompt: { type: "string", description: "The prompt that produced it, so it is findable in the library later." },
|
|
3179
|
+
tool_name: { type: "string", description: "Model apiId that produced it, when known." },
|
|
3180
|
+
modality: { type: "string", enum: ["image", "video", "audio"], description: "Output type, when the URL does not make it obvious." },
|
|
3181
|
+
job_id: { type: "string", description: "The jobId that produced it, when known." }
|
|
3182
|
+
},
|
|
3183
|
+
required: ["url"]
|
|
3184
|
+
};
|
|
3185
|
+
saveSkill = {
|
|
3186
|
+
name: "pixeldojo_save",
|
|
3187
|
+
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.`,
|
|
3188
|
+
inputZod: InputSchema17,
|
|
3189
|
+
inputJsonSchema: InputJsonSchema17,
|
|
3190
|
+
allowedModels: [],
|
|
3191
|
+
defaultModel: "",
|
|
3192
|
+
run: run17
|
|
3193
|
+
};
|
|
3194
|
+
}
|
|
3195
|
+
});
|
|
3196
|
+
async function run18(input, ctx) {
|
|
3197
|
+
const res = await ctx.client.saveCharacter({
|
|
3198
|
+
name: input.name,
|
|
3199
|
+
kind: input.kind ?? "character",
|
|
3200
|
+
image_urls: input.image_urls,
|
|
3201
|
+
description: input.description
|
|
3202
|
+
});
|
|
3203
|
+
return {
|
|
3204
|
+
kind: "text",
|
|
3205
|
+
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.`
|
|
3206
|
+
};
|
|
3207
|
+
}
|
|
3208
|
+
var KINDS, InputSchema18, InputJsonSchema18, saveCharacterSkill;
|
|
3209
|
+
var init_save_character = __esm({
|
|
3210
|
+
"src/skills/save-character.ts"() {
|
|
3211
|
+
KINDS = ["character", "product", "outfit", "location", "object", "style"];
|
|
3212
|
+
InputSchema18 = z.object({
|
|
3213
|
+
name: z.string().min(1).max(60).describe("What to call it. Used as @Name in prompts."),
|
|
3214
|
+
kind: z.enum(KINDS).optional().describe("Default character."),
|
|
3215
|
+
image_urls: z.array(z.string().url()).max(8).optional().describe("3 to 5 photos from different angles works best for a character."),
|
|
3216
|
+
description: z.string().max(1500).optional().describe("Appearance notes, or the scene text for an element.")
|
|
3217
|
+
});
|
|
3218
|
+
InputJsonSchema18 = {
|
|
3219
|
+
type: "object",
|
|
3220
|
+
properties: {
|
|
3221
|
+
name: { type: "string", description: "What to call it. Used as @Name in prompts afterwards." },
|
|
3222
|
+
kind: { type: "string", enum: KINDS, description: "character (default), product, outfit, location, object (a prop) or style." },
|
|
3223
|
+
image_urls: {
|
|
3224
|
+
type: "array",
|
|
3225
|
+
items: { type: "string", format: "uri" },
|
|
3226
|
+
maxItems: 8,
|
|
3227
|
+
description: "Photos of it. For a person, 3 to 5 clear photos from different angles. Required for a character."
|
|
3228
|
+
},
|
|
3229
|
+
description: { type: "string", description: "Optional appearance notes, or the scene text for an element without photos." }
|
|
3230
|
+
},
|
|
3231
|
+
required: ["name"]
|
|
3232
|
+
};
|
|
3233
|
+
saveCharacterSkill = {
|
|
3234
|
+
name: "pixeldojo_save_character",
|
|
3235
|
+
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.",
|
|
3236
|
+
inputZod: InputSchema18,
|
|
3237
|
+
inputJsonSchema: InputJsonSchema18,
|
|
3238
|
+
allowedModels: [],
|
|
3239
|
+
defaultModel: "",
|
|
3240
|
+
run: run18
|
|
3241
|
+
};
|
|
3242
|
+
}
|
|
3243
|
+
});
|
|
2993
3244
|
function substitutePlaceholders(args, inputs, prevUrl) {
|
|
2994
3245
|
const missing = [];
|
|
2995
3246
|
const fill = (value) => {
|
|
@@ -3246,6 +3497,8 @@ __export(skills_exports, {
|
|
|
3246
3497
|
librarySkill: () => librarySkill,
|
|
3247
3498
|
listWorkflowsSkill: () => listWorkflowsSkill,
|
|
3248
3499
|
runWorkflowSkill: () => runWorkflowSkill,
|
|
3500
|
+
saveCharacterSkill: () => saveCharacterSkill,
|
|
3501
|
+
saveSkill: () => saveSkill,
|
|
3249
3502
|
saveWorkflowSkill: () => saveWorkflowSkill,
|
|
3250
3503
|
statusSkill: () => statusSkill,
|
|
3251
3504
|
storyboardSkill: () => storyboardSkill,
|
|
@@ -3291,6 +3544,8 @@ var init_skills = __esm({
|
|
|
3291
3544
|
init_upload();
|
|
3292
3545
|
init_audio();
|
|
3293
3546
|
init_library();
|
|
3547
|
+
init_save();
|
|
3548
|
+
init_save_character();
|
|
3294
3549
|
init_workflows();
|
|
3295
3550
|
ALL_SKILLS = [
|
|
3296
3551
|
generateSkill,
|
|
@@ -3300,6 +3555,8 @@ var init_skills = __esm({
|
|
|
3300
3555
|
upscaleSkill,
|
|
3301
3556
|
audioSkill,
|
|
3302
3557
|
librarySkill,
|
|
3558
|
+
saveSkill,
|
|
3559
|
+
saveCharacterSkill,
|
|
3303
3560
|
saveWorkflowSkill,
|
|
3304
3561
|
runWorkflowSkill,
|
|
3305
3562
|
listWorkflowsSkill,
|
|
@@ -3347,6 +3604,8 @@ var init_skills = __esm({
|
|
|
3347
3604
|
pixeldojo_status: read("Check job status"),
|
|
3348
3605
|
pixeldojo_campaign_status: read("Check campaign status"),
|
|
3349
3606
|
pixeldojo_library: read("Search media library"),
|
|
3607
|
+
pixeldojo_save: render("Save to My Media"),
|
|
3608
|
+
pixeldojo_save_character: render("Save character or element"),
|
|
3350
3609
|
pixeldojo_list_workflows: read("List saved workflows"),
|
|
3351
3610
|
pixeldojo_from_url: { ...read("Read product page"), openWorldHint: true }
|
|
3352
3611
|
};
|
|
@@ -3403,8 +3662,8 @@ function makeRedactedLogger(target = console) {
|
|
|
3403
3662
|
}
|
|
3404
3663
|
|
|
3405
3664
|
// src/server.ts
|
|
3406
|
-
var PACKAGE_VERSION = "0.
|
|
3407
|
-
var SERVER_INSTRUCTIONS = "
|
|
3665
|
+
var PACKAGE_VERSION = "0.19.0" ;
|
|
3666
|
+
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.";
|
|
3408
3667
|
function buildServer(options) {
|
|
3409
3668
|
if (!options.apiKey) {
|
|
3410
3669
|
throw new Error(
|