@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.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,12 +546,6 @@ 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
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(
|
|
@@ -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
|
),
|
|
@@ -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,
|
|
@@ -1453,10 +1403,51 @@ 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
|
+
}
|
|
1456
1415
|
function keepGoing(filmId, afterSec, extra = {}) {
|
|
1457
1416
|
const set = Object.fromEntries(Object.entries(extra).filter(([, v]) => v !== void 0));
|
|
1458
1417
|
return { input: { film_id: filmId, action: "status", ...set }, afterSec };
|
|
1459
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
|
+
}
|
|
1460
1451
|
function shotLabels(f) {
|
|
1461
1452
|
return new Map(f.scenes.flatMap((s, si) => s.shots.map((sh, i) => [sh.id, `${si + 1}.${i + 1}`])));
|
|
1462
1453
|
}
|
|
@@ -1564,6 +1555,7 @@ async function plan(input, ctx) {
|
|
|
1564
1555
|
aspect: input.aspect,
|
|
1565
1556
|
look: input.look ?? (music ? MUSIC_VIDEO_LOOKS[0] : "live-action"),
|
|
1566
1557
|
lengthTarget: input.length,
|
|
1558
|
+
...!music && input.pacing === "trailer" ? { pacing: "trailer" } : {},
|
|
1567
1559
|
assetIds: [],
|
|
1568
1560
|
...music ? {
|
|
1569
1561
|
// No script step names a music video, so it would stay "Untitled film".
|
|
@@ -1637,12 +1629,25 @@ ${err} Call action "status" to retry the cut.` };
|
|
|
1637
1629
|
return { kind: "text", text: plannedText(planned.film, ctx) };
|
|
1638
1630
|
}
|
|
1639
1631
|
async function produce(filmId, soundtrack, ctx) {
|
|
1640
|
-
const { data } = await filmApi(ctx, "GET", `/api/films/${filmId}`);
|
|
1641
|
-
if (!data.film) return {
|
|
1632
|
+
const { status: status2, data } = await filmApi(ctx, "GET", `/api/films/${filmId}`);
|
|
1633
|
+
if (!data.film) return noFilm(status2, data, filmId, { action: "produce", soundtrack });
|
|
1642
1634
|
const planning = await resumePlanning(data.film, ctx, soundtrack);
|
|
1643
1635
|
if (planning) return planning;
|
|
1644
1636
|
const uncast = await uncastRolesOf(ctx, filmId);
|
|
1645
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
|
+
}
|
|
1646
1651
|
const pending = shots(data.film).filter((sh) => sh.status === "idle" || sh.status === "failed" && !sh.frameUrl);
|
|
1647
1652
|
let fired = 0;
|
|
1648
1653
|
let refused;
|
|
@@ -1656,7 +1661,7 @@ async function produce(filmId, soundtrack, ctx) {
|
|
|
1656
1661
|
}
|
|
1657
1662
|
return {
|
|
1658
1663
|
kind: "text",
|
|
1659
|
-
text: `Production started on "${data.film.title}": ${fired} frame(s) dispatched (each is a normal frame charge; the server's Frame check reviews every one).${refused ? ` Some could not start: ${refused}` : ""} Poll pixeldojo_film with {"film_id": "${filmId}", "action": "status"} every ~30 seconds \u2014 each poll advances the pipeline.`,
|
|
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.`,
|
|
1660
1665
|
// The caller's soundtrack choice rides along: the follow-up status calls
|
|
1661
1666
|
// are the ones that reach the score.
|
|
1662
1667
|
followUp: keepGoing(filmId, 30, { soundtrack })
|
|
@@ -1665,7 +1670,7 @@ async function produce(filmId, soundtrack, ctx) {
|
|
|
1665
1670
|
function buildScorePlanBody(film) {
|
|
1666
1671
|
const scenes = film.scenes.filter((s) => s.shots.length > 0);
|
|
1667
1672
|
const sections = scenes.flatMap((scene, i) => {
|
|
1668
|
-
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));
|
|
1669
1674
|
const styles = i === 0 ? ARC.opening : i === scenes.length - 1 ? ARC.finale : ARC.middle;
|
|
1670
1675
|
const parts = Math.max(1, Math.ceil(runtime / 12e4));
|
|
1671
1676
|
const partMs = Math.round(runtime / parts);
|
|
@@ -1688,7 +1693,7 @@ function buildScorePlanBody(film) {
|
|
|
1688
1693
|
};
|
|
1689
1694
|
}
|
|
1690
1695
|
function buildFlatMusicBody(film, all) {
|
|
1691
|
-
const runtimeSec = all.reduce((n, sh) => n + (sh
|
|
1696
|
+
const runtimeSec = all.reduce((n, sh) => n + cutSec(sh), 0);
|
|
1692
1697
|
const ms = Math.min(3e5, Math.max(3e4, Math.ceil(runtimeSec * 1e3 / 5e3) * 5e3));
|
|
1693
1698
|
return {
|
|
1694
1699
|
prompt: `${LOOK_MUSIC[film.look] ?? LOOK_MUSIC["live-action"]}. Instrumental only, no vocals. One consistent theme, loopable as a background bed under dialogue.`,
|
|
@@ -1698,18 +1703,19 @@ function buildFlatMusicBody(film, all) {
|
|
|
1698
1703
|
};
|
|
1699
1704
|
}
|
|
1700
1705
|
async function advance(filmId, soundtrack, musicJobId, ctx) {
|
|
1701
|
-
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`);
|
|
1702
1707
|
const film = data.film;
|
|
1703
|
-
if (!film) return {
|
|
1708
|
+
if (!film) return noFilm(status2, data, filmId, { action: "status", soundtrack });
|
|
1704
1709
|
const all = shots(film);
|
|
1705
1710
|
if (film.status === "exported" && film.exportUrl) {
|
|
1706
1711
|
return {
|
|
1707
1712
|
kind: "text",
|
|
1708
1713
|
text: [
|
|
1709
|
-
`Film finished: "${film.title}"
|
|
1714
|
+
`Film finished: "${film.title}" (${runtimeText(film)})
|
|
1710
1715
|
Watch/download: ${film.exportUrl}
|
|
1711
1716
|
Workspace: ${ctx.baseUrl}/film-studio/${film.id}
|
|
1712
1717
|
It is saved in the Library folder "Film: ${film.title}".`,
|
|
1718
|
+
stillFlagged(film),
|
|
1713
1719
|
shotIndex(film)
|
|
1714
1720
|
].filter(Boolean).join("\n")
|
|
1715
1721
|
};
|
|
@@ -1765,7 +1771,45 @@ It is saved in the Library folder "Film: ${film.title}".`,
|
|
|
1765
1771
|
const clipQueue = framesDone ? all.filter((sh) => sh.status === "frame-ready" || sh.status === "failed" && frameDone(sh) && !isRefused(sh)) : [];
|
|
1766
1772
|
for (const sh of clipQueue.slice(0, Math.max(0, 4 - inFlight))) await dispatch(sh, "clip");
|
|
1767
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
|
+
}
|
|
1768
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
|
+
}
|
|
1769
1813
|
if (all.some((sh) => needsSave(sh.videoUrl))) {
|
|
1770
1814
|
await filmApi(ctx, "POST", `/api/films/${filmId}/save`, { kind: "clip" });
|
|
1771
1815
|
}
|
|
@@ -1794,7 +1838,7 @@ It is saved in the Library folder "Film: ${film.title}".`,
|
|
|
1794
1838
|
followUp: keepGoing(filmId, 20, { soundtrack, music_job_id: musicJobId })
|
|
1795
1839
|
};
|
|
1796
1840
|
}
|
|
1797
|
-
const body = soundtrack === "scene-synced" ? buildScorePlanBody(film) : buildFlatMusicBody(film,
|
|
1841
|
+
const body = soundtrack === "scene-synced" ? buildScorePlanBody(film) : buildFlatMusicBody(film, shots(film));
|
|
1798
1842
|
const dispatch2 = await ctx.client.run("text-to-music", body);
|
|
1799
1843
|
return {
|
|
1800
1844
|
kind: "text",
|
|
@@ -1835,6 +1879,12 @@ It is saved in the Library folder "Film: ${film.title}".`,
|
|
|
1835
1879
|
...moving && !paused ? { followUp: keepGoing(filmId, 30, { soundtrack }) } : {}
|
|
1836
1880
|
};
|
|
1837
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
|
+
}
|
|
1838
1888
|
function shotIndex(f) {
|
|
1839
1889
|
const all = shots(f);
|
|
1840
1890
|
if (all.length === 0 || all.length > 24) return "";
|
|
@@ -1843,9 +1893,10 @@ function shotIndex(f) {
|
|
|
1843
1893
|
const clock = (sec) => `${Math.floor(sec / 60)}:${String(Math.round(sec % 60)).padStart(2, "0")}`;
|
|
1844
1894
|
const lines = all.map((sh) => {
|
|
1845
1895
|
const from = at;
|
|
1846
|
-
at += sh
|
|
1896
|
+
at += cutSec(sh);
|
|
1847
1897
|
const what = sh.prompt.replace(/\s+/g, " ").trim();
|
|
1848
|
-
|
|
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}`;
|
|
1849
1900
|
});
|
|
1850
1901
|
return ['Shots (redo any with action "redo" and its label):', ...lines].join("\n");
|
|
1851
1902
|
}
|
|
@@ -1854,9 +1905,9 @@ async function redoShots(input, ctx) {
|
|
|
1854
1905
|
if (!input.shots?.length) {
|
|
1855
1906
|
return { kind: "text", text: 'Pass shots: the labels status reports, like ["2.3"] for scene 2, shot 3.' };
|
|
1856
1907
|
}
|
|
1857
|
-
const { data } = await filmApi(ctx, "GET", `/api/films/${filmId}`);
|
|
1908
|
+
const { status: status2, data } = await filmApi(ctx, "GET", `/api/films/${filmId}`);
|
|
1858
1909
|
const film = data.film;
|
|
1859
|
-
if (!film) return {
|
|
1910
|
+
if (!film) return noFilm(status2, data, filmId, { action: "redo", shots: input.shots, redo: input.redo });
|
|
1860
1911
|
const labels = shotLabels(film);
|
|
1861
1912
|
const byLabel = new Map(shots(film).map((sh) => [labels.get(sh.id), sh]));
|
|
1862
1913
|
const byId = new Map(shots(film).map((sh) => [sh.id, sh]));
|
|
@@ -1931,7 +1982,8 @@ var init_film = __esm({
|
|
|
1931
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."),
|
|
1932
1983
|
lyrics: z.string().max(1e4).optional().describe("Music video mode: the song lyrics, so shots land on the right lines. Optional."),
|
|
1933
1984
|
aspect: z.enum(["16:9", "9:16"]).default("16:9"),
|
|
1934
|
-
|
|
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."),
|
|
1935
1987
|
film_id: z.string().max(40).optional().describe("Existing film id (from a previous call) for produce/status/cast/redo."),
|
|
1936
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."),
|
|
1937
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.'),
|
|
@@ -1952,7 +2004,7 @@ var init_film = __esm({
|
|
|
1952
2004
|
InputJsonSchema8 = {
|
|
1953
2005
|
type: "object",
|
|
1954
2006
|
properties: {
|
|
1955
|
-
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.` },
|
|
1956
2008
|
script: { type: "string", description: "Plan step: a full screenplay (sluglines, action lines, dialogue) to plan instead of an idea." },
|
|
1957
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).` },
|
|
1958
2010
|
song_url: {
|
|
@@ -1969,7 +2021,12 @@ var init_film = __esm({
|
|
|
1969
2021
|
},
|
|
1970
2022
|
lyrics: { type: "string", description: "Plan step, music video mode (optional): the song lyrics, so shots land on the right lines." },
|
|
1971
2023
|
aspect: { type: "string", enum: ["16:9", "9:16"], description: "Plan step: frame aspect ratio. Default 16:9." },
|
|
1972
|
-
length: { type: "string", enum: ["30s", "60s", "2-3min", "5min"], description: "Plan step: target length
|
|
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
|
+
},
|
|
1973
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".' },
|
|
1974
2031
|
action: {
|
|
1975
2032
|
type: "string",
|
|
@@ -2028,7 +2085,7 @@ var init_film = __esm({
|
|
|
2028
2085
|
benignDispatchRefusal = (status2, error) => status2 === 429 || status2 === 409 && /already generating/i.test(error ?? "");
|
|
2029
2086
|
count = (f, pred) => shots(f).filter(pred).length;
|
|
2030
2087
|
frameDone = (sh) => Boolean(sh.frameUrl) && sh.status !== "idle" && sh.status !== "frame-generating";
|
|
2031
|
-
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;
|
|
2032
2089
|
REFUSED = /copyright|infringement|content filter|safety filter|moderation|blocked|flagged|sensitive|real (?:person|human)|nsfw|content policy|policy violation|inappropriate/i;
|
|
2033
2090
|
isRefused = (sh) => sh.status === "failed" && Boolean(sh.errorMessage) && !RETRY_CLEARS.test(sh.errorMessage) && REFUSED.test(sh.errorMessage);
|
|
2034
2091
|
PLANNER_POLL_MS = 3e3;
|
|
@@ -3662,7 +3719,7 @@ function makeRedactedLogger(target = console) {
|
|
|
3662
3719
|
}
|
|
3663
3720
|
|
|
3664
3721
|
// src/server.ts
|
|
3665
|
-
var PACKAGE_VERSION = "0.
|
|
3722
|
+
var PACKAGE_VERSION = "0.20.0" ;
|
|
3666
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.";
|
|
3667
3724
|
function buildServer(options) {
|
|
3668
3725
|
if (!options.apiKey) {
|