@pixeldojo/mcp 0.18.0 → 0.20.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 +426 -110
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +426 -110
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +426 -110
- 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 +426 -110
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -458,14 +458,6 @@ function buildPayload(apiId, input, src, slot, audioSlot) {
|
|
|
458
458
|
async function run(input, ctx) {
|
|
459
459
|
const paramsGate = paramsWithoutModel(input.params, input.model);
|
|
460
460
|
if (paramsGate) return { kind: "text", text: paramsGate };
|
|
461
|
-
if (input.upgrade_draft) return upgradeDraft(input, ctx);
|
|
462
|
-
if (input.draft && input.model && input.model !== DRAFT_MODEL) {
|
|
463
|
-
return {
|
|
464
|
-
kind: "text",
|
|
465
|
-
text: `Draft mode runs on ${DRAFT_MODEL} only. Drop model (or set it to ${DRAFT_MODEL}) to render a 480p draft.`
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
if (input.draft) input = { ...input, model: DRAFT_MODEL, resolution: DRAFT_RESOLUTION };
|
|
469
461
|
let resolved = input;
|
|
470
462
|
let referenceUrls = [];
|
|
471
463
|
try {
|
|
@@ -527,8 +519,6 @@ async function run(input, ctx) {
|
|
|
527
519
|
}
|
|
528
520
|
const body = buildPayload(apiId, resolved, src, refField, audioSlot);
|
|
529
521
|
const paramsNote = applyParams(body, input.params, input.model, apiId);
|
|
530
|
-
const draftDuration = input.draft ? Number(body.duration ?? DRAFT_DEFAULT_DURATION) : void 0;
|
|
531
|
-
if (input.draft) Object.assign(body, { draft: true, resolution: DRAFT_RESOLUTION, duration: draftDuration });
|
|
532
522
|
const note = [routeNote, paramsNote].filter(Boolean).join(" ");
|
|
533
523
|
const submitted = await ctx.client.run(apiId, body);
|
|
534
524
|
const result = await pollJobUntilDoneOrHandoff(
|
|
@@ -540,45 +530,15 @@ async function run(input, ctx) {
|
|
|
540
530
|
sleep: ctx.pollOverrides?.sleep
|
|
541
531
|
}
|
|
542
532
|
);
|
|
543
|
-
|
|
533
|
+
return prefixNote(
|
|
544
534
|
pollResultToSkillResult(
|
|
545
535
|
result,
|
|
546
536
|
(urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : urls.length === 1 ? `Generated. Asset: ${urls[0]}` : `Generated ${urls.length} assets. URLs: ${urls.join(", ")}`
|
|
547
537
|
),
|
|
548
538
|
note
|
|
549
539
|
);
|
|
550
|
-
return draftDuration === void 0 ? out : { ...out, text: `${out.text}
|
|
551
|
-
|
|
552
|
-
${draftLine(submitted.jobId, draftDuration)}` };
|
|
553
|
-
}
|
|
554
|
-
function draftLine(jobId, duration) {
|
|
555
|
-
return `This is a 480p draft (draft_job_id: ${jobId}, duration: ${duration}). When the user likes it, call pixeldojo_generate with {"upgrade_draft": "${jobId}", "duration": ${duration}} to render the same take at ${DRAFT_FINAL_RESOLUTION} (billed as a ${DRAFT_FINAL_RESOLUTION} run; upgrade it within a day).`;
|
|
556
|
-
}
|
|
557
|
-
async function upgradeDraft(input, ctx) {
|
|
558
|
-
if (input.duration === void 0) {
|
|
559
|
-
return {
|
|
560
|
-
kind: "text",
|
|
561
|
-
text: `upgrade_draft also needs duration: the draft's own length (its reply says "duration: N"; ${DRAFT_DEFAULT_DURATION} when it was made without one).`
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
const submitted = await ctx.client.run(DRAFT_MODEL, {
|
|
565
|
-
draft_job_id: input.upgrade_draft,
|
|
566
|
-
resolution: DRAFT_FINAL_RESOLUTION,
|
|
567
|
-
duration: input.duration
|
|
568
|
-
});
|
|
569
|
-
const result = await pollJobUntilDoneOrHandoff(submitted.jobId, (id) => ctx.client.getJob(id), ctx.buildStatusUrl, {
|
|
570
|
-
now: ctx.pollOverrides?.now,
|
|
571
|
-
sleep: ctx.pollOverrides?.sleep
|
|
572
|
-
});
|
|
573
|
-
return prefixNote(
|
|
574
|
-
pollResultToSkillResult(
|
|
575
|
-
result,
|
|
576
|
-
(urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Upgraded to ${DRAFT_FINAL_RESOLUTION}. Asset: ${urls[0]}`
|
|
577
|
-
),
|
|
578
|
-
`Rendering draft ${input.upgrade_draft} at ${DRAFT_FINAL_RESOLUTION}.`
|
|
579
|
-
);
|
|
580
540
|
}
|
|
581
|
-
var
|
|
541
|
+
var InputSchema, DEFAULT_MODEL, VIDEO_REFERENCE_MODEL, VIDEO_REFERENCE_SLOTS, VIDEO_INPUT_MODELS, InputJsonSchema, generateSkill;
|
|
582
542
|
var init_generate = __esm({
|
|
583
543
|
"src/skills/generate.ts"() {
|
|
584
544
|
init_poll();
|
|
@@ -586,14 +546,8 @@ var init_generate = __esm({
|
|
|
586
546
|
init_upload_helpers();
|
|
587
547
|
init_schema_snap();
|
|
588
548
|
init_params();
|
|
589
|
-
DRAFT_MODEL = "seedance-2-5";
|
|
590
|
-
DRAFT_RESOLUTION = "480p";
|
|
591
|
-
DRAFT_FINAL_RESOLUTION = "1080p";
|
|
592
|
-
DRAFT_DEFAULT_DURATION = 5;
|
|
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
|
-
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
549
|
InputSchema = z.object({
|
|
596
|
-
prompt: z.string().min(1).max(8e3).describe(
|
|
550
|
+
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
551
|
model: z.string().optional().describe(
|
|
598
552
|
`Model apiId. Recommended (the models the PixelDojo studios use): "seedream-5" for images (the default), "seedance-2-5" for video (prompt only or a starting image), "wan-3-0-video" for video the others refuse. Use another model only when the user names one. Full catalog: https://pixeldojo.ai/api/v1/models; one model's fields: https://pixeldojo.ai/llm/{apiId}.txt. Omit for seedream-5, which makes still images only; name a video model to get a video.`
|
|
599
553
|
),
|
|
@@ -610,8 +564,6 @@ var init_generate = __esm({
|
|
|
610
564
|
"Source video clip to animate / restyle / extend. Use this for videos; image_url is for still frames. With no model, a clip runs on seedance-2-reference; a named model with no clip input is swapped for it and the result says so."
|
|
611
565
|
),
|
|
612
566
|
params: ParamsInput,
|
|
613
|
-
draft: z.boolean().optional().describe(DRAFT_DESCRIPTION),
|
|
614
|
-
upgrade_draft: z.string().regex(/^[A-Za-z0-9_-]{1,128}$/).optional().describe(UPGRADE_DESCRIPTION),
|
|
615
567
|
image_path: z.string().min(1).optional().describe(
|
|
616
568
|
"Absolute local file path to a reference image/video. The skill uploads it to PixelDojo first (24h temp storage) and then dispatches. Prefer this over image_url when the user has the file on disk."
|
|
617
569
|
),
|
|
@@ -633,7 +585,7 @@ var init_generate = __esm({
|
|
|
633
585
|
InputJsonSchema = {
|
|
634
586
|
type: "object",
|
|
635
587
|
properties: {
|
|
636
|
-
prompt: { type: "string", description:
|
|
588
|
+
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
589
|
model: {
|
|
638
590
|
type: "string",
|
|
639
591
|
description: `Model apiId. Recommended (the models the PixelDojo studios use): "seedream-5" for images (the default), "seedance-2-5" for video (prompt only or a starting image), "wan-3-0-video" for video the others refuse. Use another model only when the user names one. Full catalog: https://pixeldojo.ai/api/v1/models; one model's fields: https://pixeldojo.ai/llm/{apiId}.txt. Omit for seedream-5, which makes still images only; name a video model to get a video.`
|
|
@@ -665,8 +617,6 @@ var init_generate = __esm({
|
|
|
665
617
|
description: "Source video clip to animate / restyle / extend. Use this for videos; image_url is for still frames. With no model, a clip runs on seedance-2-reference; a named model with no clip input is swapped for it and the result says so."
|
|
666
618
|
},
|
|
667
619
|
params: PARAMS_JSON_SCHEMA,
|
|
668
|
-
draft: { type: "boolean", description: DRAFT_DESCRIPTION },
|
|
669
|
-
upgrade_draft: { type: "string", description: UPGRADE_DESCRIPTION },
|
|
670
620
|
image_urls: {
|
|
671
621
|
type: "array",
|
|
672
622
|
items: { type: "string", format: "uri" },
|
|
@@ -686,8 +636,8 @@ var init_generate = __esm({
|
|
|
686
636
|
};
|
|
687
637
|
generateSkill = {
|
|
688
638
|
name: "pixeldojo_generate",
|
|
689
|
-
description: "Generate an image or video from a prompt with any model in the PixelDojo catalog. With no `model`, a prompt runs on seedream-5, which makes still images only. For a video, name seedance-2-5, which works from a prompt alone or a starting image.
|
|
690
|
-
hostedDescription: "Generate an image or video from a prompt with any model in the PixelDojo catalog. With no `model`, a prompt runs on seedream-5, which makes still images only. For a video, name seedance-2-5, which works from a prompt alone or a starting image.
|
|
639
|
+
description: "Generate an image or video from a prompt with any model in the PixelDojo catalog. With no `model`, a prompt runs on seedream-5, which makes still images only. For a video, name seedance-2-5, which works from a prompt alone or a starting image. If a provider refuses the content, retry once on wan-3-0-video (video) or seedream-5 (images), the least-restricted models. For a multi-shot film or a music video synced to a song use pixeldojo_film; to change, move or extend an existing clip use pixeldojo_video. These are the models the PixelDojo studios use; name another only when the user asks for it. References: `image_url`/`image_urls` for stills, `video_url` for a source clip, `image_path`/`image_paths` for absolute local files (uploaded to 24h temp storage first). A clip or an audio track URL with no `model` runs on seedance-2-reference; a named model that cannot read a clip is swapped for it and the result says so, while a named model with no audio input drops the track. Model-specific settings go in `params` alongside an explicit `model`. Returns asset URLs, or a jobId for pixeldojo_status when the job outlasts the 30s wait.",
|
|
640
|
+
hostedDescription: "Generate an image or video from a prompt with any model in the PixelDojo catalog. With no `model`, a prompt runs on seedream-5, which makes still images only. For a video, name seedance-2-5, which works from a prompt alone or a starting image. If a provider refuses the content, retry once on wan-3-0-video (video) or seedream-5 (images), the least-restricted models. For a multi-shot film or a music video synced to a song use pixeldojo_film; to change, move or extend an existing clip use pixeldojo_video. These are the models the PixelDojo studios use; name another only when the user asks for it. References: `image_url`/`image_urls` for stills and `video_url` for a source clip, all public http(s) URLs. A clip or an audio track URL with no `model` runs on seedance-2-reference; a named model that cannot read a clip is swapped for it and the result says so, while a named model with no audio input drops the track. Model-specific settings go in `params` alongside an explicit `model`. Returns asset URLs, or a jobId for pixeldojo_status when the job outlasts the 30s wait.",
|
|
691
641
|
inputZod: InputSchema,
|
|
692
642
|
inputJsonSchema: InputJsonSchema,
|
|
693
643
|
allowedModels: null,
|
|
@@ -847,7 +797,7 @@ var init_edit = __esm({
|
|
|
847
797
|
InputJsonSchema2 = {
|
|
848
798
|
type: "object",
|
|
849
799
|
properties: {
|
|
850
|
-
prompt: { type: "string", description:
|
|
800
|
+
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
801
|
image_url: {
|
|
852
802
|
type: "string",
|
|
853
803
|
format: "uri",
|
|
@@ -1453,6 +1403,54 @@ async function filmApi(ctx, method, path, body) {
|
|
|
1453
1403
|
const data = await res.json().catch(() => ({}));
|
|
1454
1404
|
return { status: res.status, data };
|
|
1455
1405
|
}
|
|
1406
|
+
function noFilm(status2, data, filmId, retry) {
|
|
1407
|
+
if (status2 === 404) return { kind: "text", text: `Film not found: ${data.error ?? filmId}` };
|
|
1408
|
+
const input = { film_id: filmId, ...Object.fromEntries(Object.entries(retry).filter(([, v]) => v !== void 0)) };
|
|
1409
|
+
return {
|
|
1410
|
+
kind: "text",
|
|
1411
|
+
text: `Could not reach film ${filmId} just now (${status2 ? `HTTP ${status2}` : "no response"}). The film is safe and nothing changed. Call pixeldojo_film with ${JSON.stringify(input)} again in about 30 seconds. Do not plan or start a new film.`,
|
|
1412
|
+
...input.action === "status" ? { followUp: { input, afterSec: 30 } } : {}
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
function keepGoing(filmId, afterSec, extra = {}) {
|
|
1416
|
+
const set = Object.fromEntries(Object.entries(extra).filter(([, v]) => v !== void 0));
|
|
1417
|
+
return { input: { film_id: filmId, action: "status", ...set }, afterSec };
|
|
1418
|
+
}
|
|
1419
|
+
function flaggedClip(sh) {
|
|
1420
|
+
const qa = sh.clipQa;
|
|
1421
|
+
const file = (u) => u?.split("/").pop();
|
|
1422
|
+
return Boolean(qa && qa.ok === false && sh.videoUrl && file(qa.url) === file(sh.videoUrl));
|
|
1423
|
+
}
|
|
1424
|
+
function editReviewing(sh, nowMs) {
|
|
1425
|
+
const e = sh.editSuggestion;
|
|
1426
|
+
const file = (u) => u?.split("/").pop();
|
|
1427
|
+
return Boolean(e?.pending && sh.videoUrl && file(e.url) === file(sh.videoUrl) && nowMs - Date.parse(e.checkedAt) < 4 * 6e4);
|
|
1428
|
+
}
|
|
1429
|
+
function openEdit(sh) {
|
|
1430
|
+
const e = sh.editSuggestion;
|
|
1431
|
+
const file = (u) => u?.split("/").pop();
|
|
1432
|
+
return Boolean(e && !e.pending && !e.appliedAt && !e.skipped && e.inSec !== void 0 && sh.videoUrl && file(e.url) === file(sh.videoUrl));
|
|
1433
|
+
}
|
|
1434
|
+
function cutSec(sh) {
|
|
1435
|
+
if (sh.leftOut) return 0;
|
|
1436
|
+
const speed = sh.speed && sh.speed > 1 ? sh.speed : 1;
|
|
1437
|
+
return Math.max(1, (sh.videoDurationSec ?? sh.durationSec ?? 0) - (sh.trimStartSec ?? 0) - (sh.trimEndSec ?? 0)) / speed;
|
|
1438
|
+
}
|
|
1439
|
+
function editorCutting(f, nowMs) {
|
|
1440
|
+
const c = f.editorCut;
|
|
1441
|
+
const fresh = (iso) => Boolean(iso && nowMs - Date.parse(iso) < 4 * 6e4);
|
|
1442
|
+
if (c?.pending) return fresh(c.startedAt);
|
|
1443
|
+
const lastReview = shots(f).map((sh) => sh.editSuggestion?.checkedAt).filter((t) => Boolean(t)).sort().pop();
|
|
1444
|
+
if (!lastReview || !fresh(lastReview)) return false;
|
|
1445
|
+
return !(c?.builtAt && c.builtAt >= lastReview);
|
|
1446
|
+
}
|
|
1447
|
+
function runtimeText(f) {
|
|
1448
|
+
const sec = Math.round(shots(f).reduce((n, sh) => n + cutSec(sh), 0));
|
|
1449
|
+
return sec < 90 ? `${sec}s` : `${Math.floor(sec / 60)}m ${sec % 60}s`;
|
|
1450
|
+
}
|
|
1451
|
+
function shotLabels(f) {
|
|
1452
|
+
return new Map(f.scenes.flatMap((s, si) => s.shots.map((sh, i) => [sh.id, `${si + 1}.${i + 1}`])));
|
|
1453
|
+
}
|
|
1456
1454
|
async function applyCast(ctx, filmId, cast) {
|
|
1457
1455
|
let charged = false;
|
|
1458
1456
|
const lines = await Promise.all(cast.map(async (c) => {
|
|
@@ -1557,6 +1555,7 @@ async function plan(input, ctx) {
|
|
|
1557
1555
|
aspect: input.aspect,
|
|
1558
1556
|
look: input.look ?? (music ? MUSIC_VIDEO_LOOKS[0] : "live-action"),
|
|
1559
1557
|
lengthTarget: input.length,
|
|
1558
|
+
...!music && input.pacing === "trailer" ? { pacing: "trailer" } : {},
|
|
1560
1559
|
assetIds: [],
|
|
1561
1560
|
...music ? {
|
|
1562
1561
|
// No script step names a music video, so it would stay "Untitled film".
|
|
@@ -1572,12 +1571,13 @@ async function plan(input, ctx) {
|
|
|
1572
1571
|
const castStarted = Date.now();
|
|
1573
1572
|
const cast = input.cast?.length ? await applyCast(ctx, id, input.cast) : { lines: [], charged: false };
|
|
1574
1573
|
const pollBudget = Math.max(0, PLANNER_MAX_POLLS - Math.ceil((Date.now() - castStarted) / PLANNER_POLL_MS));
|
|
1575
|
-
const reply = (text) => ({
|
|
1574
|
+
const reply = (text, followUp) => ({
|
|
1576
1575
|
kind: "text",
|
|
1577
1576
|
text: [
|
|
1578
1577
|
...cast.lines,
|
|
1579
1578
|
cast.charged ? text.replace(/Nothing has been charged( yet)?\./g, "Only the cast portraits above have been charged.") : text
|
|
1580
|
-
].join("\n")
|
|
1579
|
+
].join("\n"),
|
|
1580
|
+
...followUp ? { followUp } : {}
|
|
1581
1581
|
});
|
|
1582
1582
|
let planned;
|
|
1583
1583
|
if (music) {
|
|
@@ -1588,14 +1588,14 @@ ${input.lyrics.trim()}` : ""].filter(Boolean).join("\n\n");
|
|
|
1588
1588
|
let script = input.script?.trim() ?? "";
|
|
1589
1589
|
if (!script) {
|
|
1590
1590
|
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"));
|
|
1591
|
+
if ("pending" in wrote) return reply(stillPlanningText(id, "script"), keepGoing(id, 30, { soundtrack: input.soundtrack }));
|
|
1592
1592
|
if ("error" in wrote) return reply(`Script writing failed: ${wrote.error}`);
|
|
1593
1593
|
script = wrote.film.script ?? "";
|
|
1594
1594
|
if (!script) return reply("Script writing failed: the studio returned an empty script.");
|
|
1595
1595
|
}
|
|
1596
1596
|
planned = await runPlanner(ctx, id, `/api/films/${id}/plan`, { script });
|
|
1597
1597
|
}
|
|
1598
|
-
if ("pending" in planned) return reply(stillPlanningText(id));
|
|
1598
|
+
if ("pending" in planned) return reply(stillPlanningText(id), keepGoing(id, 30, { soundtrack: input.soundtrack }));
|
|
1599
1599
|
if ("error" in planned) return reply(`Shot planning failed: ${planned.error}`);
|
|
1600
1600
|
const film = planned.film;
|
|
1601
1601
|
if (needsSlice(film)) {
|
|
@@ -1608,17 +1608,17 @@ ${err} Call action "status" to retry the cut.`);
|
|
|
1608
1608
|
return reply([plannedText(film, ctx), uncast.length ? `
|
|
1609
1609
|
${castNeededText(id, uncast)}` : ""].filter(Boolean).join("\n"));
|
|
1610
1610
|
}
|
|
1611
|
-
async function resumePlanning(film, ctx) {
|
|
1611
|
+
async function resumePlanning(film, ctx, soundtrack) {
|
|
1612
1612
|
if (film.scenes.length > 0 && film.llmJob?.status !== "running") return null;
|
|
1613
1613
|
if (film.llmJob?.status === "running") {
|
|
1614
|
-
return { kind: "text", text: stillPlanningText(film.id, film.llmJob.kind === "script" ? "script" : "plan") };
|
|
1614
|
+
return { kind: "text", text: stillPlanningText(film.id, film.llmJob.kind === "script" ? "script" : "plan"), followUp: keepGoing(film.id, 30, { soundtrack }) };
|
|
1615
1615
|
}
|
|
1616
1616
|
const prior = film.llmJob?.status === "error" ? `The last planning run failed (${film.llmJob.error ?? "unknown"}). ` : "";
|
|
1617
1617
|
if (!film.script?.trim()) {
|
|
1618
1618
|
return { kind: "text", text: `${prior}This film has nothing to plan from. Start a new one with action "plan".` };
|
|
1619
1619
|
}
|
|
1620
1620
|
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)}
|
|
1621
|
+
if ("pending" in planned) return { kind: "text", text: `${prior}${stillPlanningText(film.id)}`, followUp: keepGoing(film.id, 30, { soundtrack }) };
|
|
1622
1622
|
if ("error" in planned) return { kind: "text", text: `Shot planning failed: ${planned.error} Call action "status" to retry.` };
|
|
1623
1623
|
if (needsSlice(planned.film)) {
|
|
1624
1624
|
const err = await sliceSong(ctx, planned.film);
|
|
@@ -1628,28 +1628,49 @@ ${err} Call action "status" to retry the cut.` };
|
|
|
1628
1628
|
}
|
|
1629
1629
|
return { kind: "text", text: plannedText(planned.film, ctx) };
|
|
1630
1630
|
}
|
|
1631
|
-
async function produce(filmId, ctx) {
|
|
1632
|
-
const { data } = await filmApi(ctx, "GET", `/api/films/${filmId}`);
|
|
1633
|
-
if (!data.film) return {
|
|
1634
|
-
const planning = await resumePlanning(data.film, ctx);
|
|
1631
|
+
async function produce(filmId, soundtrack, ctx) {
|
|
1632
|
+
const { status: status2, data } = await filmApi(ctx, "GET", `/api/films/${filmId}`);
|
|
1633
|
+
if (!data.film) return noFilm(status2, data, filmId, { action: "produce", soundtrack });
|
|
1634
|
+
const planning = await resumePlanning(data.film, ctx, soundtrack);
|
|
1635
1635
|
if (planning) return planning;
|
|
1636
1636
|
const uncast = await uncastRolesOf(ctx, filmId);
|
|
1637
1637
|
if (uncast.length) return { kind: "text", text: `Nothing was charged. ${castNeededText(filmId, uncast)}` };
|
|
1638
|
+
let propsNote = "";
|
|
1639
|
+
if (!shots(data.film).some((sh) => sh.frameUrl)) {
|
|
1640
|
+
const locked = await filmApi(
|
|
1641
|
+
ctx,
|
|
1642
|
+
"POST",
|
|
1643
|
+
`/api/films/${filmId}/props`,
|
|
1644
|
+
{}
|
|
1645
|
+
).catch(() => null);
|
|
1646
|
+
const made = locked?.status === 200 ? locked.data.created ?? [] : [];
|
|
1647
|
+
if (made.length) {
|
|
1648
|
+
propsNote = ` Locked in ${made.length} recurring prop${made.length === 1 ? "" : "s"} first so ${made.length === 1 ? "it looks" : "they look"} the same in every shot: ${made.map((m) => `${m.name} (${m.shots} shots)`).join(", ")}${locked?.data.creditCost ? `, ${locked.data.creditCost} credits` : ""}.`;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1638
1651
|
const pending = shots(data.film).filter((sh) => sh.status === "idle" || sh.status === "failed" && !sh.frameUrl);
|
|
1639
1652
|
let fired = 0;
|
|
1653
|
+
let refused;
|
|
1640
1654
|
for (const sh of pending.slice(0, 6)) {
|
|
1641
1655
|
const r = await filmApi(ctx, "POST", `/api/films/${filmId}/shots/${sh.id}/generate`, { kind: "frame" });
|
|
1642
1656
|
if (r.status === 200) fired += 1;
|
|
1657
|
+
else if (!benignDispatchRefusal(r.status, r.data.error)) refused ??= r.data.error ?? `HTTP ${r.status}`;
|
|
1658
|
+
}
|
|
1659
|
+
if (fired === 0 && refused) {
|
|
1660
|
+
return { kind: "text", text: `Production could not start on "${data.film.title}": ${refused}` };
|
|
1643
1661
|
}
|
|
1644
1662
|
return {
|
|
1645
1663
|
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)
|
|
1664
|
+
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).${propsNote}${refused ? ` Some could not start: ${refused}` : ""} Poll pixeldojo_film with {"film_id": "${filmId}", "action": "status"} every ~30 seconds \u2014 each poll advances the pipeline.`,
|
|
1665
|
+
// The caller's soundtrack choice rides along: the follow-up status calls
|
|
1666
|
+
// are the ones that reach the score.
|
|
1667
|
+
followUp: keepGoing(filmId, 30, { soundtrack })
|
|
1647
1668
|
};
|
|
1648
1669
|
}
|
|
1649
1670
|
function buildScorePlanBody(film) {
|
|
1650
1671
|
const scenes = film.scenes.filter((s) => s.shots.length > 0);
|
|
1651
1672
|
const sections = scenes.flatMap((scene, i) => {
|
|
1652
|
-
const runtime = Math.max(3e3, scene.shots.reduce((n, sh) => n + (sh
|
|
1673
|
+
const runtime = Math.max(3e3, Math.round(scene.shots.reduce((n, sh) => n + cutSec(sh), 0) * 1e3));
|
|
1653
1674
|
const styles = i === 0 ? ARC.opening : i === scenes.length - 1 ? ARC.finale : ARC.middle;
|
|
1654
1675
|
const parts = Math.max(1, Math.ceil(runtime / 12e4));
|
|
1655
1676
|
const partMs = Math.round(runtime / parts);
|
|
@@ -1672,7 +1693,7 @@ function buildScorePlanBody(film) {
|
|
|
1672
1693
|
};
|
|
1673
1694
|
}
|
|
1674
1695
|
function buildFlatMusicBody(film, all) {
|
|
1675
|
-
const runtimeSec = all.reduce((n, sh) => n + (sh
|
|
1696
|
+
const runtimeSec = all.reduce((n, sh) => n + cutSec(sh), 0);
|
|
1676
1697
|
const ms = Math.min(3e5, Math.max(3e4, Math.ceil(runtimeSec * 1e3 / 5e3) * 5e3));
|
|
1677
1698
|
return {
|
|
1678
1699
|
prompt: `${LOOK_MUSIC[film.look] ?? LOOK_MUSIC["live-action"]}. Instrumental only, no vocals. One consistent theme, loopable as a background bed under dialogue.`,
|
|
@@ -1682,37 +1703,57 @@ function buildFlatMusicBody(film, all) {
|
|
|
1682
1703
|
};
|
|
1683
1704
|
}
|
|
1684
1705
|
async function advance(filmId, soundtrack, musicJobId, ctx) {
|
|
1685
|
-
const { data } = await filmApi(ctx, "GET", `/api/films/${filmId}?refresh=1`);
|
|
1706
|
+
const { status: status2, data } = await filmApi(ctx, "GET", `/api/films/${filmId}?refresh=1`);
|
|
1686
1707
|
const film = data.film;
|
|
1687
|
-
if (!film) return {
|
|
1708
|
+
if (!film) return noFilm(status2, data, filmId, { action: "status", soundtrack });
|
|
1688
1709
|
const all = shots(film);
|
|
1689
1710
|
if (film.status === "exported" && film.exportUrl) {
|
|
1690
1711
|
return {
|
|
1691
1712
|
kind: "text",
|
|
1692
|
-
text:
|
|
1713
|
+
text: [
|
|
1714
|
+
`Film finished: "${film.title}" (${runtimeText(film)})
|
|
1693
1715
|
Watch/download: ${film.exportUrl}
|
|
1694
1716
|
Workspace: ${ctx.baseUrl}/film-studio/${film.id}
|
|
1695
|
-
It is saved in the Library folder "Film: ${film.title}"
|
|
1717
|
+
It is saved in the Library folder "Film: ${film.title}".`,
|
|
1718
|
+
stillFlagged(film),
|
|
1719
|
+
shotIndex(film)
|
|
1720
|
+
].filter(Boolean).join("\n")
|
|
1696
1721
|
};
|
|
1697
1722
|
}
|
|
1698
|
-
|
|
1699
|
-
|
|
1723
|
+
const startExport = async () => {
|
|
1724
|
+
const r = await filmApi(ctx, "POST", `/api/films/${filmId}/export`, {});
|
|
1725
|
+
return r.status === 200 || r.status === 409 ? null : r.data.error ?? `HTTP ${r.status}`;
|
|
1726
|
+
};
|
|
1727
|
+
if (film.exportError && all.length > 0 && all.every((sh) => sh.status === "clip-ready")) {
|
|
1728
|
+
const again = await startExport();
|
|
1729
|
+
return {
|
|
1730
|
+
kind: "text",
|
|
1731
|
+
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.`
|
|
1732
|
+
};
|
|
1700
1733
|
}
|
|
1701
|
-
const planning = await resumePlanning(film, ctx);
|
|
1734
|
+
const planning = await resumePlanning(film, ctx, soundtrack);
|
|
1702
1735
|
if (planning) return planning;
|
|
1703
1736
|
if (needsSlice(film)) {
|
|
1704
1737
|
const err = await sliceSong(ctx, film);
|
|
1705
1738
|
if (err) return { kind: "text", text: `${err} Poll again with action "status" to retry.` };
|
|
1706
|
-
return {
|
|
1739
|
+
return {
|
|
1740
|
+
kind: "text",
|
|
1741
|
+
text: `Song cut into one slice per shot. Poll again with {"film_id": "${filmId}", "action": "status"} to keep production moving.`,
|
|
1742
|
+
followUp: keepGoing(filmId, 5, { soundtrack })
|
|
1743
|
+
};
|
|
1707
1744
|
}
|
|
1708
1745
|
if (shots(film).length > 0 && shots(film).every((sh) => sh.status === "idle")) {
|
|
1709
1746
|
return { kind: "text", text: plannedText(film, ctx) };
|
|
1710
1747
|
}
|
|
1711
1748
|
let started = 0;
|
|
1712
1749
|
const resent = /* @__PURE__ */ new Set();
|
|
1750
|
+
const refusedStarts = [];
|
|
1713
1751
|
const dispatch = async (sh, kind) => {
|
|
1714
1752
|
const r = await filmApi(ctx, "POST", `/api/films/${filmId}/shots/${sh.id}/generate`, { kind });
|
|
1715
|
-
if (r.status !== 200)
|
|
1753
|
+
if (r.status !== 200) {
|
|
1754
|
+
if (!benignDispatchRefusal(r.status, r.data.error)) refusedStarts.push(r.data.error ?? `HTTP ${r.status}`);
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1716
1757
|
started += 1;
|
|
1717
1758
|
if (sh.status === "failed") resent.add(sh.id);
|
|
1718
1759
|
};
|
|
@@ -1730,7 +1771,45 @@ It is saved in the Library folder "Film: ${film.title}".`
|
|
|
1730
1771
|
const clipQueue = framesDone ? all.filter((sh) => sh.status === "frame-ready" || sh.status === "failed" && frameDone(sh) && !isRefused(sh)) : [];
|
|
1731
1772
|
for (const sh of clipQueue.slice(0, Math.max(0, 4 - inFlight))) await dispatch(sh, "clip");
|
|
1732
1773
|
const clipsDone = all.length > 0 && all.every((sh) => sh.status === "clip-ready");
|
|
1774
|
+
const toRedo = clipsDone ? all.filter((sh) => flaggedClip(sh) && !(sh.manualClipRedos ?? 0)) : [];
|
|
1775
|
+
if (toRedo.length) {
|
|
1776
|
+
const labels2 = shotLabels(film);
|
|
1777
|
+
const redone = [];
|
|
1778
|
+
let refusedRedo;
|
|
1779
|
+
for (const sh of toRedo) {
|
|
1780
|
+
const r = await filmApi(ctx, "POST", `/api/films/${filmId}/shots/${sh.id}/generate`, { kind: "clip" });
|
|
1781
|
+
if (r.status === 200) redone.push(`${labels2.get(sh.id)} (${sh.clipQa.note || sh.clipQa.issues.join(", ")})`);
|
|
1782
|
+
else if (!benignDispatchRefusal(r.status, r.data.error)) refusedRedo ??= r.data.error ?? `HTTP ${r.status}`;
|
|
1783
|
+
}
|
|
1784
|
+
if (redone.length) {
|
|
1785
|
+
return {
|
|
1786
|
+
kind: "text",
|
|
1787
|
+
text: `All ${all.length} clips rendered. The clip check flagged ${redone.length}, so each gets one more take before the final cut (a normal clip charge each): ${redone.join("; ")}.${refusedRedo ? ` Another could not start: ${refusedRedo}` : ""} Poll again in ~30s with {"film_id": "${filmId}", "action": "status"}.`,
|
|
1788
|
+
followUp: keepGoing(filmId, 30, { soundtrack })
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1733
1792
|
if (clipsDone) {
|
|
1793
|
+
const reviewing = all.filter((sh) => editReviewing(sh, Date.now())).length;
|
|
1794
|
+
if (reviewing) {
|
|
1795
|
+
return {
|
|
1796
|
+
kind: "text",
|
|
1797
|
+
text: `All ${all.length} clips ready. The editor is marking where ${reviewing === 1 ? "the last clip starts and lands" : `${reviewing} clips start and land`} so the cut drops the waiting. Poll again in ~20s with {"film_id": "${filmId}", "action": "status"}.`,
|
|
1798
|
+
followUp: keepGoing(filmId, 20, { soundtrack })
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
if (editorCutting(film, Date.now())) {
|
|
1802
|
+
return {
|
|
1803
|
+
kind: "text",
|
|
1804
|
+
text: `All ${all.length} clips ready. The editor is watching the whole film and making the cut. Poll again in ~30s with {"film_id": "${filmId}", "action": "status"}.`,
|
|
1805
|
+
followUp: keepGoing(filmId, 30, { soundtrack })
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
const openCut = Boolean(film.editorCut?.shots?.length && !film.editorCut.appliedAt && !film.editorCut.skipped);
|
|
1809
|
+
if (openCut || all.some(openEdit)) {
|
|
1810
|
+
const cut = await filmApi(ctx, "POST", `/api/films/${filmId}/edit`, { action: "apply" });
|
|
1811
|
+
if (cut.data.film) film.scenes = cut.data.film.scenes;
|
|
1812
|
+
}
|
|
1734
1813
|
if (all.some((sh) => needsSave(sh.videoUrl))) {
|
|
1735
1814
|
await filmApi(ctx, "POST", `/api/films/${filmId}/save`, { kind: "clip" });
|
|
1736
1815
|
}
|
|
@@ -1742,27 +1821,47 @@ It is saved in the Library folder "Film: ${film.title}".`
|
|
|
1742
1821
|
const outUrl = out?.audio ?? out?.video ?? out?.images?.[0];
|
|
1743
1822
|
if (job.status === "completed" && typeof outUrl === "string") {
|
|
1744
1823
|
await filmApi(ctx, "PATCH", `/api/films/${filmId}`, { audioStems: [{ kind: "music", url: outUrl }] });
|
|
1745
|
-
|
|
1746
|
-
return { kind: "text", text: `Score attached
|
|
1824
|
+
const exportError2 = await startExport();
|
|
1825
|
+
if (exportError2) return { kind: "text", text: `Score attached, but the export could not start: ${exportError2}` };
|
|
1826
|
+
return {
|
|
1827
|
+
kind: "text",
|
|
1828
|
+
text: `Score attached \u2014 export started. Poll again in ~60s with {"film_id": "${filmId}", "action": "status"} for the finished film URL.`,
|
|
1829
|
+
followUp: keepGoing(filmId, 60, { soundtrack })
|
|
1830
|
+
};
|
|
1747
1831
|
}
|
|
1748
1832
|
if (job.status === "failed") {
|
|
1749
1833
|
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
1834
|
}
|
|
1751
|
-
return {
|
|
1835
|
+
return {
|
|
1836
|
+
kind: "text",
|
|
1837
|
+
text: `Score still composing. Poll again in ~20s with {"film_id": "${filmId}", "action": "status", "music_job_id": "${musicJobId}"}.`,
|
|
1838
|
+
followUp: keepGoing(filmId, 20, { soundtrack, music_job_id: musicJobId })
|
|
1839
|
+
};
|
|
1752
1840
|
}
|
|
1753
|
-
const body = soundtrack === "scene-synced" ? buildScorePlanBody(film) : buildFlatMusicBody(film,
|
|
1841
|
+
const body = soundtrack === "scene-synced" ? buildScorePlanBody(film) : buildFlatMusicBody(film, shots(film));
|
|
1754
1842
|
const dispatch2 = await ctx.client.run("text-to-music", body);
|
|
1755
|
-
return {
|
|
1843
|
+
return {
|
|
1844
|
+
kind: "text",
|
|
1845
|
+
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}"}.`,
|
|
1846
|
+
followUp: keepGoing(filmId, 20, { soundtrack, music_job_id: dispatch2.jobId })
|
|
1847
|
+
};
|
|
1756
1848
|
}
|
|
1757
|
-
|
|
1758
|
-
return { kind: "text", text: `All clips ready
|
|
1849
|
+
const exportError = await startExport();
|
|
1850
|
+
if (exportError) return { kind: "text", text: `All clips ready, but the export could not start: ${exportError}` };
|
|
1851
|
+
return {
|
|
1852
|
+
kind: "text",
|
|
1853
|
+
text: `All clips ready \u2014 export started (${progressLine(film)}). Poll again in ~60s with action "status" for the finished film URL.`,
|
|
1854
|
+
followUp: keepGoing(filmId, 60, { soundtrack })
|
|
1855
|
+
};
|
|
1759
1856
|
}
|
|
1760
1857
|
const failed = all.filter((sh) => sh.status === "failed");
|
|
1761
1858
|
const refused = failed.filter(isRefused);
|
|
1762
1859
|
const retrying = failed.filter((sh) => !isRefused(sh));
|
|
1763
1860
|
const sentAgain = retrying.filter((sh) => resent.has(sh.id)).length;
|
|
1764
|
-
const
|
|
1765
|
-
const
|
|
1861
|
+
const rendering = all.some((sh) => sh.status === "frame-generating" || sh.status === "clip-generating");
|
|
1862
|
+
const paused = refused.length > 0 && started === 0 && !rendering;
|
|
1863
|
+
const moving = started > 0 || rendering;
|
|
1864
|
+
const labels = shotLabels(film);
|
|
1766
1865
|
const reason = (sh) => {
|
|
1767
1866
|
const text = (sh.errorMessage ?? "no reason given").replace(/\s*Retry (?:the shot|it)\.\s*$/i, "");
|
|
1768
1867
|
return text.length > 300 ? `${text.slice(0, 297)}...` : text;
|
|
@@ -1774,14 +1873,82 @@ It is saved in the Library folder "Film: ${film.title}".`
|
|
|
1774
1873
|
`${paused ? "Paused" : "In progress"}: ${progressLine(film)}.`,
|
|
1775
1874
|
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
1875
|
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
|
-
|
|
1876
|
+
refusedStarts.length > 0 ? `${refusedStarts.length} render(s) could not start: ${refusedStarts[0]}` : "",
|
|
1877
|
+
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.`
|
|
1878
|
+
].filter(Boolean).join("\n"),
|
|
1879
|
+
...moving && !paused ? { followUp: keepGoing(filmId, 30, { soundtrack }) } : {}
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
function stillFlagged(f) {
|
|
1883
|
+
const labels = shotLabels(f);
|
|
1884
|
+
const flagged = shots(f).filter(flaggedClip);
|
|
1885
|
+
if (!flagged.length) return "";
|
|
1886
|
+
return `The clip check still flags ${flagged.length} after one redo each: ${flagged.map((sh) => `${labels.get(sh.id)} (${sh.clipQa.note || sh.clipQa.issues.join(", ")})`).join("; ")}. Ask the user before another take (action "redo", a clip charge each).`;
|
|
1887
|
+
}
|
|
1888
|
+
function shotIndex(f) {
|
|
1889
|
+
const all = shots(f);
|
|
1890
|
+
if (all.length === 0 || all.length > 24) return "";
|
|
1891
|
+
const labels = shotLabels(f);
|
|
1892
|
+
let at = 0;
|
|
1893
|
+
const clock = (sec) => `${Math.floor(sec / 60)}:${String(Math.round(sec % 60)).padStart(2, "0")}`;
|
|
1894
|
+
const lines = all.map((sh) => {
|
|
1895
|
+
const from = at;
|
|
1896
|
+
at += cutSec(sh);
|
|
1897
|
+
const what = sh.prompt.replace(/\s+/g, " ").trim();
|
|
1898
|
+
const when = sh.leftOut ? "left out of the cut" : `about ${clock(from)}-${clock(at)}`;
|
|
1899
|
+
return ` ${labels.get(sh.id)} (${when}): ${what.length > 70 ? `${what.slice(0, 67)}...` : what}`;
|
|
1900
|
+
});
|
|
1901
|
+
return ['Shots (redo any with action "redo" and its label):', ...lines].join("\n");
|
|
1902
|
+
}
|
|
1903
|
+
async function redoShots(input, ctx) {
|
|
1904
|
+
const filmId = input.film_id;
|
|
1905
|
+
if (!input.shots?.length) {
|
|
1906
|
+
return { kind: "text", text: 'Pass shots: the labels status reports, like ["2.3"] for scene 2, shot 3.' };
|
|
1907
|
+
}
|
|
1908
|
+
const { status: status2, data } = await filmApi(ctx, "GET", `/api/films/${filmId}`);
|
|
1909
|
+
const film = data.film;
|
|
1910
|
+
if (!film) return noFilm(status2, data, filmId, { action: "redo", shots: input.shots, redo: input.redo });
|
|
1911
|
+
const labels = shotLabels(film);
|
|
1912
|
+
const byLabel = new Map(shots(film).map((sh) => [labels.get(sh.id), sh]));
|
|
1913
|
+
const byId = new Map(shots(film).map((sh) => [sh.id, sh]));
|
|
1914
|
+
const kind = input.redo;
|
|
1915
|
+
const lines = [];
|
|
1916
|
+
let started = 0;
|
|
1917
|
+
let credits = 0;
|
|
1918
|
+
for (const ref of Array.from(new Set(input.shots))) {
|
|
1919
|
+
const sh = byLabel.get(ref) ?? byId.get(ref);
|
|
1920
|
+
if (!sh) {
|
|
1921
|
+
lines.push(`No shot ${ref} in this film (shots run 1.1 to ${Array.from(byLabel.keys()).pop()}).`);
|
|
1922
|
+
continue;
|
|
1923
|
+
}
|
|
1924
|
+
const label = labels.get(sh.id);
|
|
1925
|
+
if (kind === "clip" && !sh.frameUrl) {
|
|
1926
|
+
lines.push(`Shot ${label} has no frame yet, so there is no clip to redo. Redo it with redo "frame".`);
|
|
1927
|
+
continue;
|
|
1928
|
+
}
|
|
1929
|
+
const r = await filmApi(ctx, "POST", `/api/films/${filmId}/shots/${sh.id}/generate`, { kind });
|
|
1930
|
+
if (r.status === 200) {
|
|
1931
|
+
started += 1;
|
|
1932
|
+
credits += r.data.creditCost ?? 0;
|
|
1933
|
+
lines.push(`Shot ${label}: new ${kind} rendering${r.data.creditCost ? ` (${r.data.creditCost} credits)` : ""}.`);
|
|
1934
|
+
} else {
|
|
1935
|
+
lines.push(`Shot ${label}: could not start: ${r.data.error ?? `HTTP ${r.status}`}`);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
if (started === 0) return { kind: "text", text: [...lines, "Nothing was started."].join("\n") };
|
|
1939
|
+
return {
|
|
1940
|
+
kind: "text",
|
|
1941
|
+
text: [
|
|
1942
|
+
...lines,
|
|
1943
|
+
`${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.`
|
|
1944
|
+
].join("\n"),
|
|
1945
|
+
followUp: keepGoing(filmId, 30, { soundtrack: input.soundtrack })
|
|
1779
1946
|
};
|
|
1780
1947
|
}
|
|
1781
1948
|
async function run8(input, ctx) {
|
|
1782
1949
|
const action = input.action === "plan" && input.film_id ? input.cast?.length ? "cast" : "status" : input.action;
|
|
1783
1950
|
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." };
|
|
1951
|
+
if (!input.film_id) return { kind: "text", text: "produce/status/cast/redo need the film_id returned by the plan step." };
|
|
1785
1952
|
if (action === "cast") {
|
|
1786
1953
|
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
1954
|
const { lines } = await applyCast(ctx, input.film_id, input.cast);
|
|
@@ -1794,10 +1961,11 @@ async function run8(input, ctx) {
|
|
|
1794
1961
|
].join("\n")
|
|
1795
1962
|
};
|
|
1796
1963
|
}
|
|
1797
|
-
if (action === "produce") return produce(input.film_id, ctx);
|
|
1964
|
+
if (action === "produce") return produce(input.film_id, input.soundtrack, ctx);
|
|
1965
|
+
if (action === "redo") return redoShots(input, ctx);
|
|
1798
1966
|
return advance(input.film_id, input.soundtrack, input.music_job_id, ctx);
|
|
1799
1967
|
}
|
|
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;
|
|
1968
|
+
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
1969
|
var init_film = __esm({
|
|
1802
1970
|
"src/skills/film.ts"() {
|
|
1803
1971
|
init_upload_helpers();
|
|
@@ -1814,9 +1982,12 @@ var init_film = __esm({
|
|
|
1814
1982
|
song_duration_sec: z.number().positive().max(7200).optional().describe("Music video mode (optional): the song length in seconds. Omit it and the studio measures the song."),
|
|
1815
1983
|
lyrics: z.string().max(1e4).optional().describe("Music video mode: the song lyrics, so shots land on the right lines. Optional."),
|
|
1816
1984
|
aspect: z.enum(["16:9", "9:16"]).default("16:9"),
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1985
|
+
pacing: z.enum(["story", "trailer"]).default("story").describe("'story' (default) plays every shot's action whole. 'trailer' cuts it like a trailer or promo: most shots keep only their first 1.5-3 seconds."),
|
|
1986
|
+
length: z.enum(["30s", "60s", "2-3min", "5min"]).default("60s").describe("Target length of the finished film."),
|
|
1987
|
+
film_id: z.string().max(40).optional().describe("Existing film id (from a previous call) for produce/status/cast/redo."),
|
|
1988
|
+
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."),
|
|
1989
|
+
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.'),
|
|
1990
|
+
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
1991
|
cast: z.array(
|
|
1821
1992
|
z.object({
|
|
1822
1993
|
name: z.string().trim().min(1).max(60),
|
|
@@ -1833,7 +2004,7 @@ var init_film = __esm({
|
|
|
1833
2004
|
InputJsonSchema8 = {
|
|
1834
2005
|
type: "object",
|
|
1835
2006
|
properties: {
|
|
1836
|
-
idea: { type: "string", description:
|
|
2007
|
+
idea: { type: "string", description: `Plan step: one-to-three sentence film idea. The studio writes the screenplay (free). Provide idea or script. Keep the user's own words for who each character is, including their gender and pronouns ("she", "he") and species; the writer has nothing else to go on.` },
|
|
1837
2008
|
script: { type: "string", description: "Plan step: a full screenplay (sluglines, action lines, dialogue) to plan instead of an idea." },
|
|
1838
2009
|
look: { type: "string", enum: [...LOOKS], description: `Plan step: visual style carried through frames, clips and soundtrack. Story looks: ${STORY_LOOKS.join(", ")} (default live-action). Music video looks: ${MUSIC_VIDEO_LOOKS.join(", ")} (default rap-video).` },
|
|
1839
2010
|
song_url: {
|
|
@@ -1850,12 +2021,28 @@ var init_film = __esm({
|
|
|
1850
2021
|
},
|
|
1851
2022
|
lyrics: { type: "string", description: "Plan step, music video mode (optional): the song lyrics, so shots land on the right lines." },
|
|
1852
2023
|
aspect: { type: "string", enum: ["16:9", "9:16"], description: "Plan step: frame aspect ratio. Default 16:9." },
|
|
1853
|
-
length: { type: "string", enum: ["30s", "60s", "2-3min", "5min"], description: "Plan step: target length
|
|
1854
|
-
|
|
2024
|
+
length: { type: "string", enum: ["30s", "60s", "2-3min", "5min"], description: "Plan step: target length. Default 60s. Ignored for a music video, whose length is the song." },
|
|
2025
|
+
pacing: {
|
|
2026
|
+
type: "string",
|
|
2027
|
+
enum: ["story", "trailer"],
|
|
2028
|
+
description: "Plan step: 'story' (default) plays every shot's action whole, for any narrative film. 'trailer' cuts it like a trailer, promo or sizzle: most shots stay on screen 1.5-3 seconds. Ignored for a music video."
|
|
2029
|
+
},
|
|
2030
|
+
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
2031
|
action: {
|
|
1856
2032
|
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."
|
|
2033
|
+
enum: ["plan", "produce", "status", "cast", "redo"],
|
|
2034
|
+
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."
|
|
2035
|
+
},
|
|
2036
|
+
shots: {
|
|
2037
|
+
type: "array",
|
|
2038
|
+
items: { type: "string" },
|
|
2039
|
+
maxItems: 12,
|
|
2040
|
+
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.'
|
|
2041
|
+
},
|
|
2042
|
+
redo: {
|
|
2043
|
+
type: "string",
|
|
2044
|
+
enum: ["clip", "frame"],
|
|
2045
|
+
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
2046
|
},
|
|
1860
2047
|
cast: {
|
|
1861
2048
|
type: "array",
|
|
@@ -1895,9 +2082,10 @@ var init_film = __esm({
|
|
|
1895
2082
|
}
|
|
1896
2083
|
};
|
|
1897
2084
|
shots = (f) => f.scenes.flatMap((s) => s.shots);
|
|
2085
|
+
benignDispatchRefusal = (status2, error) => status2 === 429 || status2 === 409 && /already generating/i.test(error ?? "");
|
|
1898
2086
|
count = (f, pred) => shots(f).filter(pred).length;
|
|
1899
2087
|
frameDone = (sh) => Boolean(sh.frameUrl) && sh.status !== "idle" && sh.status !== "frame-generating";
|
|
1900
|
-
RETRY_CLEARS = /automatic audio check flagged|output audio\b[\s\S]*?sensitive information|temporarily unavailable/i;
|
|
2088
|
+
RETRY_CLEARS = /automatic audio check flagged|output audio\b[\s\S]*?sensitive information|temporarily unavailable|output audio\b[\s\S]*?copyright|soundtrack was blocked by the model provider/i;
|
|
1901
2089
|
REFUSED = /copyright|infringement|content filter|safety filter|moderation|blocked|flagged|sensitive|real (?:person|human)|nsfw|content policy|policy violation|inappropriate/i;
|
|
1902
2090
|
isRefused = (sh) => sh.status === "failed" && Boolean(sh.errorMessage) && !RETRY_CLEARS.test(sh.errorMessage) && REFUSED.test(sh.errorMessage);
|
|
1903
2091
|
PLANNER_POLL_MS = 3e3;
|
|
@@ -1925,8 +2113,8 @@ var init_film = __esm({
|
|
|
1925
2113
|
};
|
|
1926
2114
|
filmSkill = {
|
|
1927
2115
|
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).",
|
|
2116
|
+
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.",
|
|
2117
|
+
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
2118
|
inputZod: InputSchema8,
|
|
1931
2119
|
inputJsonSchema: InputJsonSchema8,
|
|
1932
2120
|
allowedModels: [],
|
|
@@ -2140,7 +2328,7 @@ var init_video = __esm({
|
|
|
2140
2328
|
},
|
|
2141
2329
|
prompt: {
|
|
2142
2330
|
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)."
|
|
2331
|
+
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
2332
|
},
|
|
2145
2333
|
video_url: {
|
|
2146
2334
|
type: "string",
|
|
@@ -2743,7 +2931,7 @@ async function run13(input, ctx) {
|
|
|
2743
2931
|
);
|
|
2744
2932
|
return {
|
|
2745
2933
|
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.`),
|
|
2934
|
+
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
2935
|
jobId: result.campaignId,
|
|
2748
2936
|
statusUrl: result.statusUrl ?? ctx.buildStatusUrl(result.campaignId)
|
|
2749
2937
|
};
|
|
@@ -2923,6 +3111,26 @@ var init_audio = __esm({
|
|
|
2923
3111
|
}
|
|
2924
3112
|
});
|
|
2925
3113
|
async function run16(input, ctx) {
|
|
3114
|
+
if (input.source === "characters") {
|
|
3115
|
+
const { characters } = await ctx.client.listCharacters({ q: input.q, limit: input.limit ?? 25 });
|
|
3116
|
+
if (characters.length === 0) {
|
|
3117
|
+
return {
|
|
3118
|
+
kind: "text",
|
|
3119
|
+
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."
|
|
3120
|
+
};
|
|
3121
|
+
}
|
|
3122
|
+
const lines2 = characters.map(
|
|
3123
|
+
(c) => `- ${c.mention} (${c.kind})${c.description ? `: ${c.description.slice(0, 100)}` : ""}
|
|
3124
|
+
images: ${c.image_urls.slice(0, 4).join(" ")}`
|
|
3125
|
+
);
|
|
3126
|
+
return {
|
|
3127
|
+
kind: "text",
|
|
3128
|
+
text: `${characters.length} saved character(s)/element(s):
|
|
3129
|
+
${lines2.join("\n")}
|
|
3130
|
+
|
|
3131
|
+
Write the @Name in a pixeldojo_generate / pixeldojo_video / pixeldojo_edit prompt and their photos are attached automatically (models that take reference images).`
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
2926
3134
|
const res = await ctx.client.listLibrary({
|
|
2927
3135
|
q: input.q,
|
|
2928
3136
|
model: input.model,
|
|
@@ -2957,6 +3165,7 @@ var InputSchema16, InputJsonSchema16, librarySkill;
|
|
|
2957
3165
|
var init_library = __esm({
|
|
2958
3166
|
"src/skills/library.ts"() {
|
|
2959
3167
|
InputSchema16 = z.object({
|
|
3168
|
+
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
3169
|
q: z.string().optional().describe("Substring match against the prompt that produced the asset."),
|
|
2961
3170
|
model: z.string().optional().describe("Restrict to a single model (apiId)."),
|
|
2962
3171
|
modality: z.enum(["image", "video", "audio"]).optional().describe("Restrict to one output type."),
|
|
@@ -2965,6 +3174,11 @@ var init_library = __esm({
|
|
|
2965
3174
|
InputJsonSchema16 = {
|
|
2966
3175
|
type: "object",
|
|
2967
3176
|
properties: {
|
|
3177
|
+
source: {
|
|
3178
|
+
type: "string",
|
|
3179
|
+
enum: ["media", "characters"],
|
|
3180
|
+
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."
|
|
3181
|
+
},
|
|
2968
3182
|
q: {
|
|
2969
3183
|
type: "string",
|
|
2970
3184
|
description: "Substring match against the prompt that produced the asset. Omit to list the newest items."
|
|
@@ -2981,7 +3195,7 @@ var init_library = __esm({
|
|
|
2981
3195
|
};
|
|
2982
3196
|
librarySkill = {
|
|
2983
3197
|
name: "pixeldojo_library",
|
|
2984
|
-
description: "Search your Library: saved media (permanent) and recent API/MCP generations (deleted 1 hour after they finish),
|
|
3198
|
+
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
3199
|
inputZod: InputSchema16,
|
|
2986
3200
|
inputJsonSchema: InputJsonSchema16,
|
|
2987
3201
|
allowedModels: [],
|
|
@@ -2990,6 +3204,100 @@ var init_library = __esm({
|
|
|
2990
3204
|
};
|
|
2991
3205
|
}
|
|
2992
3206
|
});
|
|
3207
|
+
async function run17(input, ctx) {
|
|
3208
|
+
const res = await ctx.client.saveMedia({
|
|
3209
|
+
url: input.url,
|
|
3210
|
+
prompt: input.prompt,
|
|
3211
|
+
tool_name: input.tool_name,
|
|
3212
|
+
modality: input.modality,
|
|
3213
|
+
job_id: input.job_id
|
|
3214
|
+
});
|
|
3215
|
+
return {
|
|
3216
|
+
kind: "text",
|
|
3217
|
+
text: `Saved to My Media (permanent): ${res.url}
|
|
3218
|
+
It will appear at https://pixeldojo.ai/my-images and in pixeldojo_library searches.`
|
|
3219
|
+
};
|
|
3220
|
+
}
|
|
3221
|
+
var InputSchema17, InputJsonSchema17, saveSkill;
|
|
3222
|
+
var init_save = __esm({
|
|
3223
|
+
"src/skills/save.ts"() {
|
|
3224
|
+
InputSchema17 = z.object({
|
|
3225
|
+
url: z.string().url().describe("The asset URL to save (image or video)."),
|
|
3226
|
+
prompt: z.string().optional().describe("The prompt that produced it \u2014 makes it findable in the library later."),
|
|
3227
|
+
tool_name: z.string().optional().describe("Tool/model slug that produced it."),
|
|
3228
|
+
modality: z.enum(["image", "video", "audio"]).optional(),
|
|
3229
|
+
job_id: z.string().optional().describe("The jobId that produced this asset, when known.")
|
|
3230
|
+
});
|
|
3231
|
+
InputJsonSchema17 = {
|
|
3232
|
+
type: "object",
|
|
3233
|
+
properties: {
|
|
3234
|
+
url: { type: "string", format: "uri", description: "The result URL to keep (image or video from a PixelDojo job)." },
|
|
3235
|
+
prompt: { type: "string", description: "The prompt that produced it, so it is findable in the library later." },
|
|
3236
|
+
tool_name: { type: "string", description: "Model apiId that produced it, when known." },
|
|
3237
|
+
modality: { type: "string", enum: ["image", "video", "audio"], description: "Output type, when the URL does not make it obvious." },
|
|
3238
|
+
job_id: { type: "string", description: "The jobId that produced it, when known." }
|
|
3239
|
+
},
|
|
3240
|
+
required: ["url"]
|
|
3241
|
+
};
|
|
3242
|
+
saveSkill = {
|
|
3243
|
+
name: "pixeldojo_save",
|
|
3244
|
+
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.`,
|
|
3245
|
+
inputZod: InputSchema17,
|
|
3246
|
+
inputJsonSchema: InputJsonSchema17,
|
|
3247
|
+
allowedModels: [],
|
|
3248
|
+
defaultModel: "",
|
|
3249
|
+
run: run17
|
|
3250
|
+
};
|
|
3251
|
+
}
|
|
3252
|
+
});
|
|
3253
|
+
async function run18(input, ctx) {
|
|
3254
|
+
const res = await ctx.client.saveCharacter({
|
|
3255
|
+
name: input.name,
|
|
3256
|
+
kind: input.kind ?? "character",
|
|
3257
|
+
image_urls: input.image_urls,
|
|
3258
|
+
description: input.description
|
|
3259
|
+
});
|
|
3260
|
+
return {
|
|
3261
|
+
kind: "text",
|
|
3262
|
+
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.`
|
|
3263
|
+
};
|
|
3264
|
+
}
|
|
3265
|
+
var KINDS, InputSchema18, InputJsonSchema18, saveCharacterSkill;
|
|
3266
|
+
var init_save_character = __esm({
|
|
3267
|
+
"src/skills/save-character.ts"() {
|
|
3268
|
+
KINDS = ["character", "product", "outfit", "location", "object", "style"];
|
|
3269
|
+
InputSchema18 = z.object({
|
|
3270
|
+
name: z.string().min(1).max(60).describe("What to call it. Used as @Name in prompts."),
|
|
3271
|
+
kind: z.enum(KINDS).optional().describe("Default character."),
|
|
3272
|
+
image_urls: z.array(z.string().url()).max(8).optional().describe("3 to 5 photos from different angles works best for a character."),
|
|
3273
|
+
description: z.string().max(1500).optional().describe("Appearance notes, or the scene text for an element.")
|
|
3274
|
+
});
|
|
3275
|
+
InputJsonSchema18 = {
|
|
3276
|
+
type: "object",
|
|
3277
|
+
properties: {
|
|
3278
|
+
name: { type: "string", description: "What to call it. Used as @Name in prompts afterwards." },
|
|
3279
|
+
kind: { type: "string", enum: KINDS, description: "character (default), product, outfit, location, object (a prop) or style." },
|
|
3280
|
+
image_urls: {
|
|
3281
|
+
type: "array",
|
|
3282
|
+
items: { type: "string", format: "uri" },
|
|
3283
|
+
maxItems: 8,
|
|
3284
|
+
description: "Photos of it. For a person, 3 to 5 clear photos from different angles. Required for a character."
|
|
3285
|
+
},
|
|
3286
|
+
description: { type: "string", description: "Optional appearance notes, or the scene text for an element without photos." }
|
|
3287
|
+
},
|
|
3288
|
+
required: ["name"]
|
|
3289
|
+
};
|
|
3290
|
+
saveCharacterSkill = {
|
|
3291
|
+
name: "pixeldojo_save_character",
|
|
3292
|
+
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.",
|
|
3293
|
+
inputZod: InputSchema18,
|
|
3294
|
+
inputJsonSchema: InputJsonSchema18,
|
|
3295
|
+
allowedModels: [],
|
|
3296
|
+
defaultModel: "",
|
|
3297
|
+
run: run18
|
|
3298
|
+
};
|
|
3299
|
+
}
|
|
3300
|
+
});
|
|
2993
3301
|
function substitutePlaceholders(args, inputs, prevUrl) {
|
|
2994
3302
|
const missing = [];
|
|
2995
3303
|
const fill = (value) => {
|
|
@@ -3246,6 +3554,8 @@ __export(skills_exports, {
|
|
|
3246
3554
|
librarySkill: () => librarySkill,
|
|
3247
3555
|
listWorkflowsSkill: () => listWorkflowsSkill,
|
|
3248
3556
|
runWorkflowSkill: () => runWorkflowSkill,
|
|
3557
|
+
saveCharacterSkill: () => saveCharacterSkill,
|
|
3558
|
+
saveSkill: () => saveSkill,
|
|
3249
3559
|
saveWorkflowSkill: () => saveWorkflowSkill,
|
|
3250
3560
|
statusSkill: () => statusSkill,
|
|
3251
3561
|
storyboardSkill: () => storyboardSkill,
|
|
@@ -3291,6 +3601,8 @@ var init_skills = __esm({
|
|
|
3291
3601
|
init_upload();
|
|
3292
3602
|
init_audio();
|
|
3293
3603
|
init_library();
|
|
3604
|
+
init_save();
|
|
3605
|
+
init_save_character();
|
|
3294
3606
|
init_workflows();
|
|
3295
3607
|
ALL_SKILLS = [
|
|
3296
3608
|
generateSkill,
|
|
@@ -3300,6 +3612,8 @@ var init_skills = __esm({
|
|
|
3300
3612
|
upscaleSkill,
|
|
3301
3613
|
audioSkill,
|
|
3302
3614
|
librarySkill,
|
|
3615
|
+
saveSkill,
|
|
3616
|
+
saveCharacterSkill,
|
|
3303
3617
|
saveWorkflowSkill,
|
|
3304
3618
|
runWorkflowSkill,
|
|
3305
3619
|
listWorkflowsSkill,
|
|
@@ -3347,6 +3661,8 @@ var init_skills = __esm({
|
|
|
3347
3661
|
pixeldojo_status: read("Check job status"),
|
|
3348
3662
|
pixeldojo_campaign_status: read("Check campaign status"),
|
|
3349
3663
|
pixeldojo_library: read("Search media library"),
|
|
3664
|
+
pixeldojo_save: render("Save to My Media"),
|
|
3665
|
+
pixeldojo_save_character: render("Save character or element"),
|
|
3350
3666
|
pixeldojo_list_workflows: read("List saved workflows"),
|
|
3351
3667
|
pixeldojo_from_url: { ...read("Read product page"), openWorldHint: true }
|
|
3352
3668
|
};
|
|
@@ -3403,8 +3719,8 @@ function makeRedactedLogger(target = console) {
|
|
|
3403
3719
|
}
|
|
3404
3720
|
|
|
3405
3721
|
// src/server.ts
|
|
3406
|
-
var PACKAGE_VERSION = "0.
|
|
3407
|
-
var SERVER_INSTRUCTIONS = "
|
|
3722
|
+
var PACKAGE_VERSION = "0.20.0" ;
|
|
3723
|
+
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
3724
|
function buildServer(options) {
|
|
3409
3725
|
if (!options.apiKey) {
|
|
3410
3726
|
throw new Error(
|