@pixeldojo/mcp 0.19.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/dist/cli.cjs +129 -72
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +129 -72
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +129 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +129 -72
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -460,14 +460,6 @@ function buildPayload(apiId, input, src, slot, audioSlot) {
|
|
|
460
460
|
async function run(input, ctx) {
|
|
461
461
|
const paramsGate = paramsWithoutModel(input.params, input.model);
|
|
462
462
|
if (paramsGate) return { kind: "text", text: paramsGate };
|
|
463
|
-
if (input.upgrade_draft) return upgradeDraft(input, ctx);
|
|
464
|
-
if (input.draft && input.model && input.model !== DRAFT_MODEL) {
|
|
465
|
-
return {
|
|
466
|
-
kind: "text",
|
|
467
|
-
text: `Draft mode runs on ${DRAFT_MODEL} only. Drop model (or set it to ${DRAFT_MODEL}) to render a 480p draft.`
|
|
468
|
-
};
|
|
469
|
-
}
|
|
470
|
-
if (input.draft) input = { ...input, model: DRAFT_MODEL, resolution: DRAFT_RESOLUTION };
|
|
471
463
|
let resolved = input;
|
|
472
464
|
let referenceUrls = [];
|
|
473
465
|
try {
|
|
@@ -529,8 +521,6 @@ async function run(input, ctx) {
|
|
|
529
521
|
}
|
|
530
522
|
const body = buildPayload(apiId, resolved, src, refField, audioSlot);
|
|
531
523
|
const paramsNote = applyParams(body, input.params, input.model, apiId);
|
|
532
|
-
const draftDuration = input.draft ? Number(body.duration ?? DRAFT_DEFAULT_DURATION) : void 0;
|
|
533
|
-
if (input.draft) Object.assign(body, { draft: true, resolution: DRAFT_RESOLUTION, duration: draftDuration });
|
|
534
524
|
const note = [routeNote, paramsNote].filter(Boolean).join(" ");
|
|
535
525
|
const submitted = await ctx.client.run(apiId, body);
|
|
536
526
|
const result = await pollJobUntilDoneOrHandoff(
|
|
@@ -542,45 +532,15 @@ async function run(input, ctx) {
|
|
|
542
532
|
sleep: ctx.pollOverrides?.sleep
|
|
543
533
|
}
|
|
544
534
|
);
|
|
545
|
-
|
|
535
|
+
return prefixNote(
|
|
546
536
|
pollResultToSkillResult(
|
|
547
537
|
result,
|
|
548
538
|
(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(", ")}`
|
|
549
539
|
),
|
|
550
540
|
note
|
|
551
541
|
);
|
|
552
|
-
return draftDuration === void 0 ? out : { ...out, text: `${out.text}
|
|
553
|
-
|
|
554
|
-
${draftLine(submitted.jobId, draftDuration)}` };
|
|
555
|
-
}
|
|
556
|
-
function draftLine(jobId, duration) {
|
|
557
|
-
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).`;
|
|
558
|
-
}
|
|
559
|
-
async function upgradeDraft(input, ctx) {
|
|
560
|
-
if (input.duration === void 0) {
|
|
561
|
-
return {
|
|
562
|
-
kind: "text",
|
|
563
|
-
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).`
|
|
564
|
-
};
|
|
565
|
-
}
|
|
566
|
-
const submitted = await ctx.client.run(DRAFT_MODEL, {
|
|
567
|
-
draft_job_id: input.upgrade_draft,
|
|
568
|
-
resolution: DRAFT_FINAL_RESOLUTION,
|
|
569
|
-
duration: input.duration
|
|
570
|
-
});
|
|
571
|
-
const result = await pollJobUntilDoneOrHandoff(submitted.jobId, (id) => ctx.client.getJob(id), ctx.buildStatusUrl, {
|
|
572
|
-
now: ctx.pollOverrides?.now,
|
|
573
|
-
sleep: ctx.pollOverrides?.sleep
|
|
574
|
-
});
|
|
575
|
-
return prefixNote(
|
|
576
|
-
pollResultToSkillResult(
|
|
577
|
-
result,
|
|
578
|
-
(urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Upgraded to ${DRAFT_FINAL_RESOLUTION}. Asset: ${urls[0]}`
|
|
579
|
-
),
|
|
580
|
-
`Rendering draft ${input.upgrade_draft} at ${DRAFT_FINAL_RESOLUTION}.`
|
|
581
|
-
);
|
|
582
542
|
}
|
|
583
|
-
var
|
|
543
|
+
var InputSchema, DEFAULT_MODEL, VIDEO_REFERENCE_MODEL, VIDEO_REFERENCE_SLOTS, VIDEO_INPUT_MODELS, InputJsonSchema, generateSkill;
|
|
584
544
|
var init_generate = __esm({
|
|
585
545
|
"src/skills/generate.ts"() {
|
|
586
546
|
init_poll();
|
|
@@ -588,12 +548,6 @@ var init_generate = __esm({
|
|
|
588
548
|
init_upload_helpers();
|
|
589
549
|
init_schema_snap();
|
|
590
550
|
init_params();
|
|
591
|
-
DRAFT_MODEL = "seedance-2-5";
|
|
592
|
-
DRAFT_RESOLUTION = "480p";
|
|
593
|
-
DRAFT_FINAL_RESOLUTION = "1080p";
|
|
594
|
-
DRAFT_DEFAULT_DURATION = 5;
|
|
595
|
-
DRAFT_DESCRIPTION = "Seedance 2.5 draft mode: render a cheap 480p draft (runs on seedance-2-5) so the user can try takes before paying for full resolution. The reply gives the draft's job id and duration; pass them to upgrade_draft to render that exact take at 1080p.";
|
|
596
|
-
UPGRADE_DESCRIPTION = "Job id of a finished Seedance 2.5 draft to render at 1080p, the same take with full detail. Also pass `duration` equal to the draft's (the draft reply states it). The draft's prompt and media are reused, so prompt and references are ignored. Billed as a 1080p run.";
|
|
597
551
|
InputSchema = zod.z.object({
|
|
598
552
|
prompt: zod.z.string().min(1).max(8e3).describe('Prompt describing what to generate. Write @Name to use a character or element the user saved in Character Studio ("@Mara on a beach"): its photos are attached automatically on models that take reference images. pixeldojo_library source=characters lists them.'),
|
|
599
553
|
model: zod.z.string().optional().describe(
|
|
@@ -612,8 +566,6 @@ var init_generate = __esm({
|
|
|
612
566
|
"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."
|
|
613
567
|
),
|
|
614
568
|
params: ParamsInput,
|
|
615
|
-
draft: zod.z.boolean().optional().describe(DRAFT_DESCRIPTION),
|
|
616
|
-
upgrade_draft: zod.z.string().regex(/^[A-Za-z0-9_-]{1,128}$/).optional().describe(UPGRADE_DESCRIPTION),
|
|
617
569
|
image_path: zod.z.string().min(1).optional().describe(
|
|
618
570
|
"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."
|
|
619
571
|
),
|
|
@@ -667,8 +619,6 @@ var init_generate = __esm({
|
|
|
667
619
|
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."
|
|
668
620
|
},
|
|
669
621
|
params: PARAMS_JSON_SCHEMA,
|
|
670
|
-
draft: { type: "boolean", description: DRAFT_DESCRIPTION },
|
|
671
|
-
upgrade_draft: { type: "string", description: UPGRADE_DESCRIPTION },
|
|
672
622
|
image_urls: {
|
|
673
623
|
type: "array",
|
|
674
624
|
items: { type: "string", format: "uri" },
|
|
@@ -688,8 +638,8 @@ var init_generate = __esm({
|
|
|
688
638
|
};
|
|
689
639
|
generateSkill = {
|
|
690
640
|
name: "pixeldojo_generate",
|
|
691
|
-
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.
|
|
692
|
-
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.
|
|
641
|
+
description: "Generate an image or video from a prompt with any model in the PixelDojo catalog. With no `model`, a prompt runs on seedream-5, which makes still images only. For a video, name seedance-2-5, which works from a prompt alone or a starting image. If a provider refuses the content, retry once on wan-3-0-video (video) or seedream-5 (images), the least-restricted models. For a multi-shot film or a music video synced to a song use pixeldojo_film; to change, move or extend an existing clip use pixeldojo_video. These are the models the PixelDojo studios use; name another only when the user asks for it. References: `image_url`/`image_urls` for stills, `video_url` for a source clip, `image_path`/`image_paths` for absolute local files (uploaded to 24h temp storage first). A clip or an audio track URL with no `model` runs on seedance-2-reference; a named model that cannot read a clip is swapped for it and the result says so, while a named model with no audio input drops the track. Model-specific settings go in `params` alongside an explicit `model`. Returns asset URLs, or a jobId for pixeldojo_status when the job outlasts the 30s wait.",
|
|
642
|
+
hostedDescription: "Generate an image or video from a prompt with any model in the PixelDojo catalog. With no `model`, a prompt runs on seedream-5, which makes still images only. For a video, name seedance-2-5, which works from a prompt alone or a starting image. If a provider refuses the content, retry once on wan-3-0-video (video) or seedream-5 (images), the least-restricted models. For a multi-shot film or a music video synced to a song use pixeldojo_film; to change, move or extend an existing clip use pixeldojo_video. These are the models the PixelDojo studios use; name another only when the user asks for it. References: `image_url`/`image_urls` for stills and `video_url` for a source clip, all public http(s) URLs. A clip or an audio track URL with no `model` runs on seedance-2-reference; a named model that cannot read a clip is swapped for it and the result says so, while a named model with no audio input drops the track. Model-specific settings go in `params` alongside an explicit `model`. Returns asset URLs, or a jobId for pixeldojo_status when the job outlasts the 30s wait.",
|
|
693
643
|
inputZod: InputSchema,
|
|
694
644
|
inputJsonSchema: InputJsonSchema,
|
|
695
645
|
allowedModels: null,
|
|
@@ -1455,10 +1405,51 @@ async function filmApi(ctx, method, path, body) {
|
|
|
1455
1405
|
const data = await res.json().catch(() => ({}));
|
|
1456
1406
|
return { status: res.status, data };
|
|
1457
1407
|
}
|
|
1408
|
+
function noFilm(status2, data, filmId, retry) {
|
|
1409
|
+
if (status2 === 404) return { kind: "text", text: `Film not found: ${data.error ?? filmId}` };
|
|
1410
|
+
const input = { film_id: filmId, ...Object.fromEntries(Object.entries(retry).filter(([, v]) => v !== void 0)) };
|
|
1411
|
+
return {
|
|
1412
|
+
kind: "text",
|
|
1413
|
+
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.`,
|
|
1414
|
+
...input.action === "status" ? { followUp: { input, afterSec: 30 } } : {}
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1458
1417
|
function keepGoing(filmId, afterSec, extra = {}) {
|
|
1459
1418
|
const set = Object.fromEntries(Object.entries(extra).filter(([, v]) => v !== void 0));
|
|
1460
1419
|
return { input: { film_id: filmId, action: "status", ...set }, afterSec };
|
|
1461
1420
|
}
|
|
1421
|
+
function flaggedClip(sh) {
|
|
1422
|
+
const qa = sh.clipQa;
|
|
1423
|
+
const file = (u) => u?.split("/").pop();
|
|
1424
|
+
return Boolean(qa && qa.ok === false && sh.videoUrl && file(qa.url) === file(sh.videoUrl));
|
|
1425
|
+
}
|
|
1426
|
+
function editReviewing(sh, nowMs) {
|
|
1427
|
+
const e = sh.editSuggestion;
|
|
1428
|
+
const file = (u) => u?.split("/").pop();
|
|
1429
|
+
return Boolean(e?.pending && sh.videoUrl && file(e.url) === file(sh.videoUrl) && nowMs - Date.parse(e.checkedAt) < 4 * 6e4);
|
|
1430
|
+
}
|
|
1431
|
+
function openEdit(sh) {
|
|
1432
|
+
const e = sh.editSuggestion;
|
|
1433
|
+
const file = (u) => u?.split("/").pop();
|
|
1434
|
+
return Boolean(e && !e.pending && !e.appliedAt && !e.skipped && e.inSec !== void 0 && sh.videoUrl && file(e.url) === file(sh.videoUrl));
|
|
1435
|
+
}
|
|
1436
|
+
function cutSec(sh) {
|
|
1437
|
+
if (sh.leftOut) return 0;
|
|
1438
|
+
const speed = sh.speed && sh.speed > 1 ? sh.speed : 1;
|
|
1439
|
+
return Math.max(1, (sh.videoDurationSec ?? sh.durationSec ?? 0) - (sh.trimStartSec ?? 0) - (sh.trimEndSec ?? 0)) / speed;
|
|
1440
|
+
}
|
|
1441
|
+
function editorCutting(f, nowMs) {
|
|
1442
|
+
const c = f.editorCut;
|
|
1443
|
+
const fresh = (iso) => Boolean(iso && nowMs - Date.parse(iso) < 4 * 6e4);
|
|
1444
|
+
if (c?.pending) return fresh(c.startedAt);
|
|
1445
|
+
const lastReview = shots(f).map((sh) => sh.editSuggestion?.checkedAt).filter((t) => Boolean(t)).sort().pop();
|
|
1446
|
+
if (!lastReview || !fresh(lastReview)) return false;
|
|
1447
|
+
return !(c?.builtAt && c.builtAt >= lastReview);
|
|
1448
|
+
}
|
|
1449
|
+
function runtimeText(f) {
|
|
1450
|
+
const sec = Math.round(shots(f).reduce((n, sh) => n + cutSec(sh), 0));
|
|
1451
|
+
return sec < 90 ? `${sec}s` : `${Math.floor(sec / 60)}m ${sec % 60}s`;
|
|
1452
|
+
}
|
|
1462
1453
|
function shotLabels(f) {
|
|
1463
1454
|
return new Map(f.scenes.flatMap((s, si) => s.shots.map((sh, i) => [sh.id, `${si + 1}.${i + 1}`])));
|
|
1464
1455
|
}
|
|
@@ -1566,6 +1557,7 @@ async function plan(input, ctx) {
|
|
|
1566
1557
|
aspect: input.aspect,
|
|
1567
1558
|
look: input.look ?? (music ? MUSIC_VIDEO_LOOKS[0] : "live-action"),
|
|
1568
1559
|
lengthTarget: input.length,
|
|
1560
|
+
...!music && input.pacing === "trailer" ? { pacing: "trailer" } : {},
|
|
1569
1561
|
assetIds: [],
|
|
1570
1562
|
...music ? {
|
|
1571
1563
|
// No script step names a music video, so it would stay "Untitled film".
|
|
@@ -1639,12 +1631,25 @@ ${err} Call action "status" to retry the cut.` };
|
|
|
1639
1631
|
return { kind: "text", text: plannedText(planned.film, ctx) };
|
|
1640
1632
|
}
|
|
1641
1633
|
async function produce(filmId, soundtrack, ctx) {
|
|
1642
|
-
const { data } = await filmApi(ctx, "GET", `/api/films/${filmId}`);
|
|
1643
|
-
if (!data.film) return {
|
|
1634
|
+
const { status: status2, data } = await filmApi(ctx, "GET", `/api/films/${filmId}`);
|
|
1635
|
+
if (!data.film) return noFilm(status2, data, filmId, { action: "produce", soundtrack });
|
|
1644
1636
|
const planning = await resumePlanning(data.film, ctx, soundtrack);
|
|
1645
1637
|
if (planning) return planning;
|
|
1646
1638
|
const uncast = await uncastRolesOf(ctx, filmId);
|
|
1647
1639
|
if (uncast.length) return { kind: "text", text: `Nothing was charged. ${castNeededText(filmId, uncast)}` };
|
|
1640
|
+
let propsNote = "";
|
|
1641
|
+
if (!shots(data.film).some((sh) => sh.frameUrl)) {
|
|
1642
|
+
const locked = await filmApi(
|
|
1643
|
+
ctx,
|
|
1644
|
+
"POST",
|
|
1645
|
+
`/api/films/${filmId}/props`,
|
|
1646
|
+
{}
|
|
1647
|
+
).catch(() => null);
|
|
1648
|
+
const made = locked?.status === 200 ? locked.data.created ?? [] : [];
|
|
1649
|
+
if (made.length) {
|
|
1650
|
+
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` : ""}.`;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1648
1653
|
const pending = shots(data.film).filter((sh) => sh.status === "idle" || sh.status === "failed" && !sh.frameUrl);
|
|
1649
1654
|
let fired = 0;
|
|
1650
1655
|
let refused;
|
|
@@ -1658,7 +1663,7 @@ async function produce(filmId, soundtrack, ctx) {
|
|
|
1658
1663
|
}
|
|
1659
1664
|
return {
|
|
1660
1665
|
kind: "text",
|
|
1661
|
-
text: `Production started on "${data.film.title}": ${fired} frame(s) dispatched (each is a normal frame charge; the server's Frame check reviews every one).${refused ? ` Some could not start: ${refused}` : ""} Poll pixeldojo_film with {"film_id": "${filmId}", "action": "status"} every ~30 seconds \u2014 each poll advances the pipeline.`,
|
|
1666
|
+
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.`,
|
|
1662
1667
|
// The caller's soundtrack choice rides along: the follow-up status calls
|
|
1663
1668
|
// are the ones that reach the score.
|
|
1664
1669
|
followUp: keepGoing(filmId, 30, { soundtrack })
|
|
@@ -1667,7 +1672,7 @@ async function produce(filmId, soundtrack, ctx) {
|
|
|
1667
1672
|
function buildScorePlanBody(film) {
|
|
1668
1673
|
const scenes = film.scenes.filter((s) => s.shots.length > 0);
|
|
1669
1674
|
const sections = scenes.flatMap((scene, i) => {
|
|
1670
|
-
const runtime = Math.max(3e3, scene.shots.reduce((n, sh) => n + (sh
|
|
1675
|
+
const runtime = Math.max(3e3, Math.round(scene.shots.reduce((n, sh) => n + cutSec(sh), 0) * 1e3));
|
|
1671
1676
|
const styles = i === 0 ? ARC.opening : i === scenes.length - 1 ? ARC.finale : ARC.middle;
|
|
1672
1677
|
const parts = Math.max(1, Math.ceil(runtime / 12e4));
|
|
1673
1678
|
const partMs = Math.round(runtime / parts);
|
|
@@ -1690,7 +1695,7 @@ function buildScorePlanBody(film) {
|
|
|
1690
1695
|
};
|
|
1691
1696
|
}
|
|
1692
1697
|
function buildFlatMusicBody(film, all) {
|
|
1693
|
-
const runtimeSec = all.reduce((n, sh) => n + (sh
|
|
1698
|
+
const runtimeSec = all.reduce((n, sh) => n + cutSec(sh), 0);
|
|
1694
1699
|
const ms = Math.min(3e5, Math.max(3e4, Math.ceil(runtimeSec * 1e3 / 5e3) * 5e3));
|
|
1695
1700
|
return {
|
|
1696
1701
|
prompt: `${LOOK_MUSIC[film.look] ?? LOOK_MUSIC["live-action"]}. Instrumental only, no vocals. One consistent theme, loopable as a background bed under dialogue.`,
|
|
@@ -1700,18 +1705,19 @@ function buildFlatMusicBody(film, all) {
|
|
|
1700
1705
|
};
|
|
1701
1706
|
}
|
|
1702
1707
|
async function advance(filmId, soundtrack, musicJobId, ctx) {
|
|
1703
|
-
const { data } = await filmApi(ctx, "GET", `/api/films/${filmId}?refresh=1`);
|
|
1708
|
+
const { status: status2, data } = await filmApi(ctx, "GET", `/api/films/${filmId}?refresh=1`);
|
|
1704
1709
|
const film = data.film;
|
|
1705
|
-
if (!film) return {
|
|
1710
|
+
if (!film) return noFilm(status2, data, filmId, { action: "status", soundtrack });
|
|
1706
1711
|
const all = shots(film);
|
|
1707
1712
|
if (film.status === "exported" && film.exportUrl) {
|
|
1708
1713
|
return {
|
|
1709
1714
|
kind: "text",
|
|
1710
1715
|
text: [
|
|
1711
|
-
`Film finished: "${film.title}"
|
|
1716
|
+
`Film finished: "${film.title}" (${runtimeText(film)})
|
|
1712
1717
|
Watch/download: ${film.exportUrl}
|
|
1713
1718
|
Workspace: ${ctx.baseUrl}/film-studio/${film.id}
|
|
1714
1719
|
It is saved in the Library folder "Film: ${film.title}".`,
|
|
1720
|
+
stillFlagged(film),
|
|
1715
1721
|
shotIndex(film)
|
|
1716
1722
|
].filter(Boolean).join("\n")
|
|
1717
1723
|
};
|
|
@@ -1767,7 +1773,45 @@ It is saved in the Library folder "Film: ${film.title}".`,
|
|
|
1767
1773
|
const clipQueue = framesDone ? all.filter((sh) => sh.status === "frame-ready" || sh.status === "failed" && frameDone(sh) && !isRefused(sh)) : [];
|
|
1768
1774
|
for (const sh of clipQueue.slice(0, Math.max(0, 4 - inFlight))) await dispatch(sh, "clip");
|
|
1769
1775
|
const clipsDone = all.length > 0 && all.every((sh) => sh.status === "clip-ready");
|
|
1776
|
+
const toRedo = clipsDone ? all.filter((sh) => flaggedClip(sh) && !(sh.manualClipRedos ?? 0)) : [];
|
|
1777
|
+
if (toRedo.length) {
|
|
1778
|
+
const labels2 = shotLabels(film);
|
|
1779
|
+
const redone = [];
|
|
1780
|
+
let refusedRedo;
|
|
1781
|
+
for (const sh of toRedo) {
|
|
1782
|
+
const r = await filmApi(ctx, "POST", `/api/films/${filmId}/shots/${sh.id}/generate`, { kind: "clip" });
|
|
1783
|
+
if (r.status === 200) redone.push(`${labels2.get(sh.id)} (${sh.clipQa.note || sh.clipQa.issues.join(", ")})`);
|
|
1784
|
+
else if (!benignDispatchRefusal(r.status, r.data.error)) refusedRedo ??= r.data.error ?? `HTTP ${r.status}`;
|
|
1785
|
+
}
|
|
1786
|
+
if (redone.length) {
|
|
1787
|
+
return {
|
|
1788
|
+
kind: "text",
|
|
1789
|
+
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"}.`,
|
|
1790
|
+
followUp: keepGoing(filmId, 30, { soundtrack })
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1770
1794
|
if (clipsDone) {
|
|
1795
|
+
const reviewing = all.filter((sh) => editReviewing(sh, Date.now())).length;
|
|
1796
|
+
if (reviewing) {
|
|
1797
|
+
return {
|
|
1798
|
+
kind: "text",
|
|
1799
|
+
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"}.`,
|
|
1800
|
+
followUp: keepGoing(filmId, 20, { soundtrack })
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
if (editorCutting(film, Date.now())) {
|
|
1804
|
+
return {
|
|
1805
|
+
kind: "text",
|
|
1806
|
+
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"}.`,
|
|
1807
|
+
followUp: keepGoing(filmId, 30, { soundtrack })
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
const openCut = Boolean(film.editorCut?.shots?.length && !film.editorCut.appliedAt && !film.editorCut.skipped);
|
|
1811
|
+
if (openCut || all.some(openEdit)) {
|
|
1812
|
+
const cut = await filmApi(ctx, "POST", `/api/films/${filmId}/edit`, { action: "apply" });
|
|
1813
|
+
if (cut.data.film) film.scenes = cut.data.film.scenes;
|
|
1814
|
+
}
|
|
1771
1815
|
if (all.some((sh) => needsSave(sh.videoUrl))) {
|
|
1772
1816
|
await filmApi(ctx, "POST", `/api/films/${filmId}/save`, { kind: "clip" });
|
|
1773
1817
|
}
|
|
@@ -1796,7 +1840,7 @@ It is saved in the Library folder "Film: ${film.title}".`,
|
|
|
1796
1840
|
followUp: keepGoing(filmId, 20, { soundtrack, music_job_id: musicJobId })
|
|
1797
1841
|
};
|
|
1798
1842
|
}
|
|
1799
|
-
const body = soundtrack === "scene-synced" ? buildScorePlanBody(film) : buildFlatMusicBody(film,
|
|
1843
|
+
const body = soundtrack === "scene-synced" ? buildScorePlanBody(film) : buildFlatMusicBody(film, shots(film));
|
|
1800
1844
|
const dispatch2 = await ctx.client.run("text-to-music", body);
|
|
1801
1845
|
return {
|
|
1802
1846
|
kind: "text",
|
|
@@ -1837,6 +1881,12 @@ It is saved in the Library folder "Film: ${film.title}".`,
|
|
|
1837
1881
|
...moving && !paused ? { followUp: keepGoing(filmId, 30, { soundtrack }) } : {}
|
|
1838
1882
|
};
|
|
1839
1883
|
}
|
|
1884
|
+
function stillFlagged(f) {
|
|
1885
|
+
const labels = shotLabels(f);
|
|
1886
|
+
const flagged = shots(f).filter(flaggedClip);
|
|
1887
|
+
if (!flagged.length) return "";
|
|
1888
|
+
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).`;
|
|
1889
|
+
}
|
|
1840
1890
|
function shotIndex(f) {
|
|
1841
1891
|
const all = shots(f);
|
|
1842
1892
|
if (all.length === 0 || all.length > 24) return "";
|
|
@@ -1845,9 +1895,10 @@ function shotIndex(f) {
|
|
|
1845
1895
|
const clock = (sec) => `${Math.floor(sec / 60)}:${String(Math.round(sec % 60)).padStart(2, "0")}`;
|
|
1846
1896
|
const lines = all.map((sh) => {
|
|
1847
1897
|
const from = at;
|
|
1848
|
-
at += sh
|
|
1898
|
+
at += cutSec(sh);
|
|
1849
1899
|
const what = sh.prompt.replace(/\s+/g, " ").trim();
|
|
1850
|
-
|
|
1900
|
+
const when = sh.leftOut ? "left out of the cut" : `about ${clock(from)}-${clock(at)}`;
|
|
1901
|
+
return ` ${labels.get(sh.id)} (${when}): ${what.length > 70 ? `${what.slice(0, 67)}...` : what}`;
|
|
1851
1902
|
});
|
|
1852
1903
|
return ['Shots (redo any with action "redo" and its label):', ...lines].join("\n");
|
|
1853
1904
|
}
|
|
@@ -1856,9 +1907,9 @@ async function redoShots(input, ctx) {
|
|
|
1856
1907
|
if (!input.shots?.length) {
|
|
1857
1908
|
return { kind: "text", text: 'Pass shots: the labels status reports, like ["2.3"] for scene 2, shot 3.' };
|
|
1858
1909
|
}
|
|
1859
|
-
const { data } = await filmApi(ctx, "GET", `/api/films/${filmId}`);
|
|
1910
|
+
const { status: status2, data } = await filmApi(ctx, "GET", `/api/films/${filmId}`);
|
|
1860
1911
|
const film = data.film;
|
|
1861
|
-
if (!film) return {
|
|
1912
|
+
if (!film) return noFilm(status2, data, filmId, { action: "redo", shots: input.shots, redo: input.redo });
|
|
1862
1913
|
const labels = shotLabels(film);
|
|
1863
1914
|
const byLabel = new Map(shots(film).map((sh) => [labels.get(sh.id), sh]));
|
|
1864
1915
|
const byId = new Map(shots(film).map((sh) => [sh.id, sh]));
|
|
@@ -1933,7 +1984,8 @@ var init_film = __esm({
|
|
|
1933
1984
|
song_duration_sec: zod.z.number().positive().max(7200).optional().describe("Music video mode (optional): the song length in seconds. Omit it and the studio measures the song."),
|
|
1934
1985
|
lyrics: zod.z.string().max(1e4).optional().describe("Music video mode: the song lyrics, so shots land on the right lines. Optional."),
|
|
1935
1986
|
aspect: zod.z.enum(["16:9", "9:16"]).default("16:9"),
|
|
1936
|
-
|
|
1987
|
+
pacing: zod.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."),
|
|
1988
|
+
length: zod.z.enum(["30s", "60s", "2-3min", "5min"]).default("60s").describe("Target length of the finished film."),
|
|
1937
1989
|
film_id: zod.z.string().max(40).optional().describe("Existing film id (from a previous call) for produce/status/cast/redo."),
|
|
1938
1990
|
action: zod.z.enum(["plan", "produce", "status", "cast", "redo"]).default("plan").describe("'plan' creates + storyboards (free). 'cast' casts, replaces or removes characters on an existing film. 'produce' starts charged generation. 'status' advances production and reports progress; poll it every ~30s until it returns the finished film. 'redo' renders a new take of the listed shots (charged), then status re-saves and re-exports."),
|
|
1939
1991
|
shots: zod.z.array(zod.z.string().trim().min(1).max(40)).min(1).max(12).optional().describe('Redo: the shots to render again, by the labels status reports ("2.3" is scene 2, shot 3) or shot id.'),
|
|
@@ -1954,7 +2006,7 @@ var init_film = __esm({
|
|
|
1954
2006
|
InputJsonSchema8 = {
|
|
1955
2007
|
type: "object",
|
|
1956
2008
|
properties: {
|
|
1957
|
-
idea: { type: "string", description:
|
|
2009
|
+
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.` },
|
|
1958
2010
|
script: { type: "string", description: "Plan step: a full screenplay (sluglines, action lines, dialogue) to plan instead of an idea." },
|
|
1959
2011
|
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).` },
|
|
1960
2012
|
song_url: {
|
|
@@ -1971,7 +2023,12 @@ var init_film = __esm({
|
|
|
1971
2023
|
},
|
|
1972
2024
|
lyrics: { type: "string", description: "Plan step, music video mode (optional): the song lyrics, so shots land on the right lines." },
|
|
1973
2025
|
aspect: { type: "string", enum: ["16:9", "9:16"], description: "Plan step: frame aspect ratio. Default 16:9." },
|
|
1974
|
-
length: { type: "string", enum: ["30s", "60s", "2-3min", "5min"], description: "Plan step: target length
|
|
2026
|
+
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." },
|
|
2027
|
+
pacing: {
|
|
2028
|
+
type: "string",
|
|
2029
|
+
enum: ["story", "trailer"],
|
|
2030
|
+
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."
|
|
2031
|
+
},
|
|
1975
2032
|
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".' },
|
|
1976
2033
|
action: {
|
|
1977
2034
|
type: "string",
|
|
@@ -2030,7 +2087,7 @@ var init_film = __esm({
|
|
|
2030
2087
|
benignDispatchRefusal = (status2, error) => status2 === 429 || status2 === 409 && /already generating/i.test(error ?? "");
|
|
2031
2088
|
count = (f, pred) => shots(f).filter(pred).length;
|
|
2032
2089
|
frameDone = (sh) => Boolean(sh.frameUrl) && sh.status !== "idle" && sh.status !== "frame-generating";
|
|
2033
|
-
RETRY_CLEARS = /automatic audio check flagged|output audio\b[\s\S]*?sensitive information|temporarily unavailable/i;
|
|
2090
|
+
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;
|
|
2034
2091
|
REFUSED = /copyright|infringement|content filter|safety filter|moderation|blocked|flagged|sensitive|real (?:person|human)|nsfw|content policy|policy violation|inappropriate/i;
|
|
2035
2092
|
isRefused = (sh) => sh.status === "failed" && Boolean(sh.errorMessage) && !RETRY_CLEARS.test(sh.errorMessage) && REFUSED.test(sh.errorMessage);
|
|
2036
2093
|
PLANNER_POLL_MS = 3e3;
|
|
@@ -3664,7 +3721,7 @@ function makeRedactedLogger(target = console) {
|
|
|
3664
3721
|
}
|
|
3665
3722
|
|
|
3666
3723
|
// src/server.ts
|
|
3667
|
-
var PACKAGE_VERSION = "0.
|
|
3724
|
+
var PACKAGE_VERSION = "0.20.0" ;
|
|
3668
3725
|
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.";
|
|
3669
3726
|
function buildServer(options) {
|
|
3670
3727
|
if (!options.apiKey) {
|