corent-mcp 0.8.1 → 0.10.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 +70 -7
- package/dist/prompts.js +48 -11
- package/dist/server.js +346 -22
- package/dist/widget-html.js +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -18,7 +18,7 @@ import { z } from "zod";
|
|
|
18
18
|
import { MEDIA_WIDGET_HTML } from "./widget-html.js";
|
|
19
19
|
import { registerPrompts } from "./prompts.js";
|
|
20
20
|
export const DEFAULT_API_URL = "https://api.corent.tech";
|
|
21
|
-
export const SERVER_VERSION = "0.
|
|
21
|
+
export const SERVER_VERSION = "0.10.0";
|
|
22
22
|
// Transport resilience, matching the two SDKs: a 429 or 5xx is retried with
|
|
23
23
|
// backoff, but ONLY on calls that carry an Idempotency-Key (or are GETs), so a
|
|
24
24
|
// retry can never buy a second generation.
|
|
@@ -84,6 +84,8 @@ function errorCode(err) {
|
|
|
84
84
|
return "invalid_request";
|
|
85
85
|
if (err.status === 409 || detail.includes("estimate"))
|
|
86
86
|
return "estimate_exceeds_ceiling";
|
|
87
|
+
if (err.status === 400 && detail.includes("max_cost_cents"))
|
|
88
|
+
return "estimate_exceeds_ceiling";
|
|
87
89
|
if (err.status === 429)
|
|
88
90
|
return "rate_limited";
|
|
89
91
|
if (err.status === 503)
|
|
@@ -170,6 +172,20 @@ export function createCorentServer(config = {}) {
|
|
|
170
172
|
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
171
173
|
};
|
|
172
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* POST /v1/intent answers task_type "film" for anything one clip cannot
|
|
177
|
+
* deliver (longer than one take, or several characters telling a story), and
|
|
178
|
+
* /v1/intent/execute will not make a single clip for it. Say so in this
|
|
179
|
+
* server's own tool names, so the assistant goes to plan_film.
|
|
180
|
+
*/
|
|
181
|
+
function pointFilmsToPlanFilm(res) {
|
|
182
|
+
if (res && typeof res === "object" && res.plan?.task_type === "film") {
|
|
183
|
+
res.next_step = "plan_film";
|
|
184
|
+
const hint = "This is a film: call plan_film with the same request, show the user the plan and price, then make_film.";
|
|
185
|
+
res.message = typeof res.message === "string" && res.message ? `${res.message} ${hint}` : hint;
|
|
186
|
+
}
|
|
187
|
+
return res;
|
|
188
|
+
}
|
|
173
189
|
function wrap(fn) {
|
|
174
190
|
return async (args) => {
|
|
175
191
|
try {
|
|
@@ -237,6 +253,12 @@ export function createCorentServer(config = {}) {
|
|
|
237
253
|
// deliverable in it (parity audit 2026-08-30).
|
|
238
254
|
audio: z.array(z.object({ url: z.string() }).passthrough()).optional(),
|
|
239
255
|
progress_percent: z.number().nullable().optional(),
|
|
256
|
+
// Video jobs (2026-09-12): the PNG of the final frame when return_last_frame
|
|
257
|
+
// was asked for (pass it as the next clip's image_url), the task the model
|
|
258
|
+
// ran (auto | reference | edit | extend) and the ACTUAL clip length.
|
|
259
|
+
last_frame_url: z.string().nullable().optional(),
|
|
260
|
+
task: z.string().nullable().optional(),
|
|
261
|
+
duration_s: z.number().nullable().optional(),
|
|
240
262
|
meta: mediaMeta.optional(),
|
|
241
263
|
error: z.string().optional(),
|
|
242
264
|
};
|
|
@@ -332,10 +354,73 @@ export function createCorentServer(config = {}) {
|
|
|
332
354
|
headers: { "Idempotency-Key": idempotencyKey() },
|
|
333
355
|
body: JSON.stringify(args),
|
|
334
356
|
})));
|
|
357
|
+
// --- Video references (Seedance 2.x, 2026-09-12). The prompt points at
|
|
358
|
+
// them as "@Image 1", "@Video 1", "@Audio 1" in the order given. The API
|
|
359
|
+
// enforces three rules; they are mirrored here so a bad combination is
|
|
360
|
+
// refused before it costs a round trip.
|
|
361
|
+
const VIDEO_ASPECTS = ["16:9", "9:16", "1:1", "adaptive"];
|
|
362
|
+
const VIDEO_TASKS = ["auto", "reference", "edit", "extend"];
|
|
363
|
+
const REAL_FACE_RULE = "The supplier rejects photos of REAL people as references; characters generated inside Corent are accepted.";
|
|
364
|
+
const videoReferenceFields = {
|
|
365
|
+
reference_image_urls: z
|
|
366
|
+
.array(z.string().url())
|
|
367
|
+
.min(1)
|
|
368
|
+
.max(30)
|
|
369
|
+
.optional()
|
|
370
|
+
.describe("1 to 30 public https images the clip should keep to (a character, a product, a setting). Refer to " +
|
|
371
|
+
"them in the prompt as @Image 1, @Image 2 in this order. Needs model " +
|
|
372
|
+
"(corent-seedance-2.5); cannot be combined with image_url/end_image_url. " + REAL_FACE_RULE),
|
|
373
|
+
reference_video_urls: z
|
|
374
|
+
.array(z.string().url())
|
|
375
|
+
.min(1)
|
|
376
|
+
.max(10)
|
|
377
|
+
.optional()
|
|
378
|
+
.describe("1 to 10 public https clips, @Video 1, @Video 2 in the prompt: the source for an edit or extend task, " +
|
|
379
|
+
"or motion/style to follow. Cannot be combined with image_url/end_image_url."),
|
|
380
|
+
reference_audio_urls: z
|
|
381
|
+
.array(z.string().url())
|
|
382
|
+
.min(1)
|
|
383
|
+
.max(10)
|
|
384
|
+
.optional()
|
|
385
|
+
.describe("1 to 10 public https audio files, @Audio 1 in the prompt, e.g. a track the clip should cut to."),
|
|
386
|
+
task: z
|
|
387
|
+
.enum(VIDEO_TASKS)
|
|
388
|
+
.optional()
|
|
389
|
+
.describe("What to do with the references (default auto). edit changes something in @Video 1 and keeps its " +
|
|
390
|
+
"length: omit duration_s. extend continues @Video 1. Both need reference_video_urls and " +
|
|
391
|
+
"aspect_ratio omitted or adaptive. The prompt must say the intent (add, remove, replace, change; " +
|
|
392
|
+
"extend, continue)."),
|
|
393
|
+
return_last_frame: z
|
|
394
|
+
.boolean()
|
|
395
|
+
.optional()
|
|
396
|
+
.describe("true also stores a PNG of the final frame and returns it as last_frame_url on the finished job, so the " +
|
|
397
|
+
"next clip can start exactly there (pass it as image_url). Use it to chain shots."),
|
|
398
|
+
output_format: z.enum(["mp4", "mov"]).optional().describe("Delivered file type (default mp4)."),
|
|
399
|
+
};
|
|
400
|
+
function checkVideoRules(args, where = "") {
|
|
401
|
+
const bad = (message) => {
|
|
402
|
+
throw new CorentApiError(422, { message: `${where}${message}` });
|
|
403
|
+
};
|
|
404
|
+
const frames = Boolean(args.image_url || args.end_image_url);
|
|
405
|
+
const refs = ["reference_image_urls", "reference_video_urls", "reference_audio_urls"].some((k) => Array.isArray(args[k]) && args[k].length > 0);
|
|
406
|
+
const task = args.task ?? "auto";
|
|
407
|
+
if (frames && refs) {
|
|
408
|
+
bad("image_url/end_image_url (first/last frame) cannot be combined with reference_*: pick one way to guide the clip.");
|
|
409
|
+
}
|
|
410
|
+
if ((task === "edit" || task === "extend") && !(Array.isArray(args.reference_video_urls) && args.reference_video_urls.length)) {
|
|
411
|
+
bad(`task ${task} needs at least one reference_video_urls entry: the clip to ${task}.`);
|
|
412
|
+
}
|
|
413
|
+
if (task === "edit" && args.duration_s !== undefined) {
|
|
414
|
+
bad("omit duration_s on an edit task: the edited clip keeps the source clip's length.");
|
|
415
|
+
}
|
|
416
|
+
if ((frames || task === "edit" || task === "extend") && args.aspect_ratio !== undefined && args.aspect_ratio !== "adaptive") {
|
|
417
|
+
bad('aspect_ratio must be omitted or "adaptive" on first/last-frame, edit and extend tasks: the clip keeps the source\'s shape.');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
335
420
|
server.registerTool("generate_video", {
|
|
336
421
|
title: "Generate video",
|
|
337
422
|
_meta: toolMeta("Starting video", "Video job started", true),
|
|
338
|
-
description: "Start a video from a
|
|
423
|
+
description: "Start a video from text, a source image, or references (@Image 1, @Video 1, @Audio 1 in the prompt). Asynchronous: returns a job id; poll get_job until completed. Premium ~52c, pro ~84c per clip; failed generations are never billed. References need corent-seedance-2.5; photos of real people are rejected as references.",
|
|
339
424
|
inputSchema: {
|
|
340
425
|
prompt: z.string().describe("What to generate, in plain language"),
|
|
341
426
|
tier: z
|
|
@@ -347,15 +432,19 @@ export function createCorentServer(config = {}) {
|
|
|
347
432
|
.optional()
|
|
348
433
|
.describe("Optional content style hint, same vocabulary as generate_image"),
|
|
349
434
|
aspect_ratio: z
|
|
350
|
-
.enum(
|
|
435
|
+
.enum(VIDEO_ASPECTS)
|
|
351
436
|
.optional()
|
|
352
|
-
.describe("Shape of the clip. OMIT IT when animating a source image: those
|
|
437
|
+
.describe("Shape of the clip. OMIT IT (or pass adaptive) when animating a source image, editing or extending a clip: those keep the source's own shape, so naming another is refused rather than silently ignored. Omitted for text-to-video means the model's own default (landscape)."),
|
|
353
438
|
duration_s: z.number().int().min(1).max(30).optional().describe("Requested duration in seconds"),
|
|
354
439
|
resolution: z
|
|
355
440
|
.enum(["720p", "1080p", "4k"])
|
|
356
441
|
.optional()
|
|
357
442
|
.describe("Pixel resolution (default 720p). Tier-capped: pro allows up to 1080p, max_pro up to 4k; above the tier's cap it is clamped down, never rejected. Higher resolutions cost more (1080p ~2.5x, 4k ~5.5x). GET /v1/tiers lists each tier's menu with prices."),
|
|
358
|
-
image_url: z
|
|
443
|
+
image_url: z
|
|
444
|
+
.string()
|
|
445
|
+
.url()
|
|
446
|
+
.optional()
|
|
447
|
+
.describe("If set, animates this image as the FIRST frame instead of pure text-to-video. Use a previous clip's last_frame_url to continue from it."),
|
|
359
448
|
audio: z
|
|
360
449
|
.boolean()
|
|
361
450
|
.optional()
|
|
@@ -383,15 +472,105 @@ export function createCorentServer(config = {}) {
|
|
|
383
472
|
model: z
|
|
384
473
|
.string()
|
|
385
474
|
.optional()
|
|
386
|
-
.describe('Direct model access: pin an exact model by name, e.g. "corent-seedance-2.
|
|
475
|
+
.describe('Direct model access: pin an exact model by name, e.g. "corent-seedance-2.5" (list_models shows the menu; every name there is spelled corent-*). Bypasses routing -- never substituted; duration and resolution snap to THAT model\'s own menu rather than a tier cap. Mutually exclusive with tier. list_models reports supports_reference_images, supports_video_edit, supports_video_extend and supports_last_frame per video model.'),
|
|
476
|
+
...videoReferenceFields,
|
|
387
477
|
},
|
|
388
478
|
outputSchema: jobOutput,
|
|
389
479
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
390
|
-
}, wrap(async (args) =>
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
480
|
+
}, wrap(async (args) => {
|
|
481
|
+
checkVideoRules(args);
|
|
482
|
+
return corent("/v1/videos/generate", {
|
|
483
|
+
method: "POST",
|
|
484
|
+
headers: { "Idempotency-Key": idempotencyKey() },
|
|
485
|
+
body: JSON.stringify(args),
|
|
486
|
+
});
|
|
487
|
+
}));
|
|
488
|
+
// --- Two conveniences over generate_video for the clip-in, clip-out asks.
|
|
489
|
+
// Same endpoint, same Idempotency-Key, same widget; they only set task and
|
|
490
|
+
// put the source clip in reference_video_urls so the agent cannot get the
|
|
491
|
+
// plumbing wrong. ---
|
|
492
|
+
const editExtendShared = {
|
|
493
|
+
reference_image_urls: z
|
|
494
|
+
.array(z.string().url())
|
|
495
|
+
.min(1)
|
|
496
|
+
.max(30)
|
|
497
|
+
.optional()
|
|
498
|
+
.describe("Optional images the result should match (@Image 1, @Image 2 in the prompt), e.g. a character or product to bring in. " +
|
|
499
|
+
REAL_FACE_RULE),
|
|
500
|
+
reference_audio_urls: z
|
|
501
|
+
.array(z.string().url())
|
|
502
|
+
.min(1)
|
|
503
|
+
.max(10)
|
|
504
|
+
.optional()
|
|
505
|
+
.describe("Optional audio (@Audio 1 in the prompt) for the result to follow."),
|
|
506
|
+
model: z
|
|
507
|
+
.string()
|
|
508
|
+
.optional()
|
|
509
|
+
.describe('Model to run, default "corent-seedance-2.5", the model that can edit or extend a clip today.'),
|
|
510
|
+
resolution: z.enum(["720p", "1080p", "4k"]).optional().describe("Pixel resolution (default 720p); higher costs more."),
|
|
511
|
+
audio: z.boolean().optional().describe("Ask for native sound in the result."),
|
|
512
|
+
seed: z.number().int().min(0).optional().describe("Reproducibility, same contract as generate_video."),
|
|
513
|
+
negative_prompt: z.string().optional().describe("What must NOT appear in the result."),
|
|
514
|
+
return_last_frame: z
|
|
515
|
+
.boolean()
|
|
516
|
+
.optional()
|
|
517
|
+
.describe("true returns last_frame_url on the finished job, a PNG of the final frame to start the next clip from."),
|
|
518
|
+
output_format: z.enum(["mp4", "mov"]).optional().describe("Delivered file type (default mp4)."),
|
|
519
|
+
};
|
|
520
|
+
server.registerTool("edit_video", {
|
|
521
|
+
title: "Edit video",
|
|
522
|
+
_meta: toolMeta("Editing video", "Video edit started", true),
|
|
523
|
+
description: "Edit an existing clip with a prompt: add, remove, replace or change something in @Video 1. Keeps the source clip's length and shape. Asynchronous: returns a job id; poll get_job until completed. Runs on corent-seedance-2.5. Photos of real people are rejected as references; Corent-made characters are fine.",
|
|
524
|
+
inputSchema: {
|
|
525
|
+
prompt: z
|
|
526
|
+
.string()
|
|
527
|
+
.min(1)
|
|
528
|
+
.describe('The change, naming the source as @Video 1 and using an edit verb: "Replace the red car in @Video 1 with a blue bicycle".'),
|
|
529
|
+
video_url: z.string().url().describe("Public https URL of the clip to edit (becomes @Video 1)."),
|
|
530
|
+
...editExtendShared,
|
|
531
|
+
},
|
|
532
|
+
outputSchema: jobOutput,
|
|
533
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
534
|
+
}, wrap(async ({ prompt, video_url, model, ...rest }) => {
|
|
535
|
+
const body = { prompt, task: "edit", reference_video_urls: [video_url], model: model ?? "corent-seedance-2.5", ...rest };
|
|
536
|
+
checkVideoRules(body);
|
|
537
|
+
return corent("/v1/videos/generate", {
|
|
538
|
+
method: "POST",
|
|
539
|
+
headers: { "Idempotency-Key": idempotencyKey() },
|
|
540
|
+
body: JSON.stringify(body),
|
|
541
|
+
});
|
|
542
|
+
}));
|
|
543
|
+
server.registerTool("extend_video", {
|
|
544
|
+
title: "Extend video",
|
|
545
|
+
_meta: toolMeta("Extending video", "Video extension started", true),
|
|
546
|
+
description: "Continue an existing clip (@Video 1) forward or backward, with a prompt for what happens next or before. Keeps the source shape; duration_s is the added length. Asynchronous: returns a job id; poll get_job until completed. Runs on corent-seedance-2.5. Pair with return_last_frame to chain shots.",
|
|
547
|
+
inputSchema: {
|
|
548
|
+
prompt: z.string().min(1).describe("What happens in the new footage. The direction line is added for you."),
|
|
549
|
+
video_url: z.string().url().describe("Public https URL of the clip to continue (becomes @Video 1)."),
|
|
550
|
+
direction: z
|
|
551
|
+
.enum(["forward", "backward"])
|
|
552
|
+
.optional()
|
|
553
|
+
.describe("forward (default) adds footage after the clip; backward adds what happened before it."),
|
|
554
|
+
duration_s: z.number().int().min(1).max(30).optional().describe("Seconds of NEW footage to add."),
|
|
555
|
+
...editExtendShared,
|
|
556
|
+
},
|
|
557
|
+
outputSchema: jobOutput,
|
|
558
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
559
|
+
}, wrap(async ({ prompt, video_url, direction, model, ...rest }) => {
|
|
560
|
+
const body = {
|
|
561
|
+
prompt: `Extend @Video 1 ${direction ?? "forward"}. ${prompt}`,
|
|
562
|
+
task: "extend",
|
|
563
|
+
reference_video_urls: [video_url],
|
|
564
|
+
model: model ?? "corent-seedance-2.5",
|
|
565
|
+
...rest,
|
|
566
|
+
};
|
|
567
|
+
checkVideoRules(body);
|
|
568
|
+
return corent("/v1/videos/generate", {
|
|
569
|
+
method: "POST",
|
|
570
|
+
headers: { "Idempotency-Key": idempotencyKey() },
|
|
571
|
+
body: JSON.stringify(body),
|
|
572
|
+
});
|
|
573
|
+
}));
|
|
395
574
|
server.registerTool("generate_speech", {
|
|
396
575
|
title: "Generate speech (voice)",
|
|
397
576
|
_meta: toolMeta("Generating speech", "Speech ready"),
|
|
@@ -644,22 +823,31 @@ export function createCorentServer(config = {}) {
|
|
|
644
823
|
.array(z.object({
|
|
645
824
|
prompt: z.string(),
|
|
646
825
|
tier: z.enum(TIERS).optional(),
|
|
826
|
+
model: z.string().optional().describe("Pin a model, e.g. corent-seedance-2.5 for reference/edit/extend items."),
|
|
647
827
|
style: z.enum(IMAGE_STYLES).optional(),
|
|
648
|
-
aspect_ratio: z.enum(
|
|
828
|
+
aspect_ratio: z.enum(VIDEO_ASPECTS).optional(),
|
|
649
829
|
duration_s: z.number().int().min(1).max(30).optional(),
|
|
650
830
|
resolution: z.enum(["720p", "1080p", "4k"]).optional(),
|
|
831
|
+
image_url: z.string().url().optional(),
|
|
832
|
+
end_image_url: z.string().url().optional(),
|
|
833
|
+
audio: z.boolean().optional(),
|
|
834
|
+
seed: z.number().int().min(0).optional(),
|
|
835
|
+
...videoReferenceFields,
|
|
651
836
|
}))
|
|
652
837
|
.min(1)
|
|
653
838
|
.max(50)
|
|
654
|
-
.describe("1 to 50 video requests"),
|
|
839
|
+
.describe("1 to 50 video requests; each item takes the same reference fields as generate_video"),
|
|
655
840
|
},
|
|
656
841
|
outputSchema: batchOutput,
|
|
657
842
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
658
|
-
}, wrap(async ({ items }) =>
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
843
|
+
}, wrap(async ({ items }) => {
|
|
844
|
+
items.forEach((item, i) => checkVideoRules(item, `items[${i}]: `));
|
|
845
|
+
return corent("/v1/videos/generate/batch", {
|
|
846
|
+
method: "POST",
|
|
847
|
+
headers: { "Idempotency-Key": idempotencyKey() },
|
|
848
|
+
body: JSON.stringify({ items }),
|
|
849
|
+
});
|
|
850
|
+
}));
|
|
663
851
|
server.registerTool("get_batch", {
|
|
664
852
|
title: "Check batch progress",
|
|
665
853
|
_meta: toolMeta("Checking batch", "Batch checked"),
|
|
@@ -715,7 +903,7 @@ export function createCorentServer(config = {}) {
|
|
|
715
903
|
server.registerTool("plan", {
|
|
716
904
|
title: "Plan (cost preview, no generation)",
|
|
717
905
|
_meta: toolMeta("Planning", "Plan ready"),
|
|
718
|
-
description: "Preview how Corent would handle a plain-language request without generating
|
|
906
|
+
description: "Preview how Corent would handle a plain-language request, without generating: image vs video, tier, shape, duration and an estimated cost. Image and video only; speech is generate_speech. A story with several characters or anything over about 30 seconds is a film: use plan_film. Impossible asks come back with can_fulfill false.",
|
|
719
907
|
inputSchema: {
|
|
720
908
|
intent: z
|
|
721
909
|
.string()
|
|
@@ -724,13 +912,14 @@ export function createCorentServer(config = {}) {
|
|
|
724
912
|
outputSchema: {
|
|
725
913
|
plan: z.object({ can_fulfill: z.boolean().optional() }).passthrough().optional(),
|
|
726
914
|
estimated_cost_cents: z.number().nullable().optional(),
|
|
915
|
+
next_step: z.string().nullable().optional(),
|
|
727
916
|
},
|
|
728
917
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
729
|
-
}, wrap(async ({ intent }) => corent("/v1/intent", { method: "POST", body: JSON.stringify({ intent }) })));
|
|
918
|
+
}, wrap(async ({ intent }) => pointFilmsToPlanFilm(await corent("/v1/intent", { method: "POST", body: JSON.stringify({ intent }) }))));
|
|
730
919
|
server.registerTool("create", {
|
|
731
920
|
title: "Create (plan + generate, budget-capped)",
|
|
732
921
|
_meta: toolMeta("Creating", "Created", true),
|
|
733
|
-
description: "Describe
|
|
922
|
+
description: "Describe one image or one clip in plain language; Corent picks type, tier, shape and duration and generates it. max_cost_cents is a required ceiling: above it nothing is generated. Videos return a job id for get_job. Failed generations are never billed. A story with several characters or anything over about 30 seconds is a film: use plan_film, then make_film.",
|
|
734
923
|
inputSchema: {
|
|
735
924
|
intent: z.string().describe("What you want, in natural language"),
|
|
736
925
|
max_cost_cents: z
|
|
@@ -745,6 +934,7 @@ export function createCorentServer(config = {}) {
|
|
|
745
934
|
actual_cost_cents: z.number().nullable().optional(),
|
|
746
935
|
result: z.object({}).passthrough().nullable().optional(),
|
|
747
936
|
message: z.string().nullable().optional(),
|
|
937
|
+
next_step: z.string().nullable().optional(),
|
|
748
938
|
},
|
|
749
939
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
750
940
|
}, wrap(async ({ intent, max_cost_cents }) => {
|
|
@@ -758,8 +948,142 @@ export function createCorentServer(config = {}) {
|
|
|
758
948
|
if (res && typeof res.message === "string") {
|
|
759
949
|
res.message = res.message.replace("confirm_estimated_cost_cents_up_to", "max_cost_cents");
|
|
760
950
|
}
|
|
761
|
-
return res;
|
|
951
|
+
return pointFilmsToPlanFilm(res);
|
|
952
|
+
}));
|
|
953
|
+
// --- Films (v0.10.0): one sentence in, one finished video with sound out.
|
|
954
|
+
// Corent writes the script, creates the characters once, shoots every scene
|
|
955
|
+
// with them, checks and reshoots, and joins the scenes. plan_film is free and
|
|
956
|
+
// shows the price; make_film spends, capped by max_cost_cents; a failed or
|
|
957
|
+
// cancelled film is not billed. ---
|
|
958
|
+
const FILM_ASPECTS = ["16:9", "9:16", "1:1"];
|
|
959
|
+
const FILM_RESOLUTIONS = ["720p", "1080p"];
|
|
960
|
+
const filmOptions = {
|
|
961
|
+
duration_s: z.number().int().min(10).max(120).optional().describe("Length of the finished film in seconds, 10 to 120 (default 30)."),
|
|
962
|
+
aspect_ratio: z.enum(FILM_ASPECTS).optional().describe("16:9 (default), 9:16 for vertical, or 1:1."),
|
|
963
|
+
resolution: z.enum(FILM_RESOLUTIONS).optional().describe("720p (default) or 1080p; 1080p costs more."),
|
|
964
|
+
language: z.string().optional().describe('Language the characters speak, e.g. "en" or "es". Omit to follow the brief.'),
|
|
965
|
+
customer_id: z.string().min(1).max(128).optional().describe("Your own id for the end customer this film is for."),
|
|
966
|
+
};
|
|
967
|
+
const filmCharacter = z
|
|
968
|
+
.object({
|
|
969
|
+
id: z.string().optional(),
|
|
970
|
+
name: z.string().nullable().optional(),
|
|
971
|
+
description: z.string().nullable().optional(),
|
|
972
|
+
image_url: z.string().nullable().optional(),
|
|
973
|
+
})
|
|
974
|
+
.passthrough();
|
|
975
|
+
const filmScene = z
|
|
976
|
+
.object({
|
|
977
|
+
index: z.number().optional(),
|
|
978
|
+
duration_s: z.number().nullable().optional(),
|
|
979
|
+
setting: z.string().nullable().optional(),
|
|
980
|
+
action: z.string().nullable().optional(),
|
|
981
|
+
camera: z.string().nullable().optional(),
|
|
982
|
+
characters: z.array(z.string()).nullable().optional(),
|
|
983
|
+
dialogue: z.array(z.object({ character_id: z.string().optional(), line: z.string().optional() }).passthrough()).nullable().optional(),
|
|
984
|
+
status: z.string().nullable().optional(),
|
|
985
|
+
video_url: z.string().nullable().optional(),
|
|
986
|
+
last_frame_url: z.string().nullable().optional(),
|
|
987
|
+
reshoots: z.number().nullable().optional(),
|
|
988
|
+
})
|
|
989
|
+
.passthrough();
|
|
990
|
+
const filmOutput = {
|
|
991
|
+
film_id: z.string().optional(),
|
|
992
|
+
status: z.string().optional(),
|
|
993
|
+
progress_percent: z.number().nullable().optional(),
|
|
994
|
+
stage_detail: z.string().nullable().optional(),
|
|
995
|
+
title: z.string().nullable().optional(),
|
|
996
|
+
characters: z.array(filmCharacter).nullable().optional(),
|
|
997
|
+
scenes: z.array(filmScene).nullable().optional(),
|
|
998
|
+
video_url: z.string().nullable().optional(),
|
|
999
|
+
duration_s: z.number().nullable().optional(),
|
|
1000
|
+
estimated_cost_cents: z.number().nullable().optional(),
|
|
1001
|
+
cost_cents: z.number().nullable().optional(),
|
|
1002
|
+
billed: z.boolean().nullable().optional(),
|
|
1003
|
+
quality_notes: z.array(z.string()).nullable().optional(),
|
|
1004
|
+
error: z.string().nullable().optional(),
|
|
1005
|
+
error_message: z.string().nullable().optional(),
|
|
1006
|
+
created_at: z.string().nullable().optional(),
|
|
1007
|
+
completed_at: z.string().nullable().optional(),
|
|
1008
|
+
customer_id: z.string().nullable().optional(),
|
|
1009
|
+
};
|
|
1010
|
+
server.registerTool("plan_film", {
|
|
1011
|
+
title: "Plan a film (script and price, free)",
|
|
1012
|
+
_meta: toolMeta("Writing the film plan", "Film plan ready"),
|
|
1013
|
+
description: "Turn a one-sentence film idea into a plan without spending anything: title, characters, every scene with its dialogue, total length and the price range. The plan lasts 24 hours. Show the user the plan and the price and get a clear yes before make_film. Use it for any story with several characters or anything over about 30 seconds.",
|
|
1014
|
+
inputSchema: {
|
|
1015
|
+
brief: z
|
|
1016
|
+
.string()
|
|
1017
|
+
.min(1)
|
|
1018
|
+
.max(2000)
|
|
1019
|
+
.describe("The film in the user's own words, e.g. 'a 50 second parody where 5 founders start a startup and know nothing'."),
|
|
1020
|
+
...filmOptions,
|
|
1021
|
+
},
|
|
1022
|
+
outputSchema: {
|
|
1023
|
+
plan_id: z.string().optional(),
|
|
1024
|
+
title: z.string().nullable().optional(),
|
|
1025
|
+
logline: z.string().nullable().optional(),
|
|
1026
|
+
style: z.string().nullable().optional(),
|
|
1027
|
+
characters: z.array(filmCharacter).nullable().optional(),
|
|
1028
|
+
scenes: z.array(filmScene).nullable().optional(),
|
|
1029
|
+
total_duration_s: z.number().nullable().optional(),
|
|
1030
|
+
estimated_cost_cents: z.number().nullable().optional(),
|
|
1031
|
+
estimated_cost_range: z.object({ min: z.number().optional(), max: z.number().optional() }).passthrough().nullable().optional(),
|
|
1032
|
+
expires_at: z.string().nullable().optional(),
|
|
1033
|
+
},
|
|
1034
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
1035
|
+
}, wrap(async (args) => corent("/v1/films/plan", { method: "POST", body: JSON.stringify(args) })));
|
|
1036
|
+
server.registerTool("make_film", {
|
|
1037
|
+
title: "Make a film (budget-capped)",
|
|
1038
|
+
_meta: toolMeta("Starting film", "Film started", true),
|
|
1039
|
+
description: "Start the film the user approved: pass plan_id from plan_film (or a brief), and max_cost_cents a little above the estimate. The charge never goes above max_cost_cents, and a failed or cancelled film is not billed. Returns film_id right away; a film takes about 15 to 30 minutes, so check get_film every 60 seconds.",
|
|
1040
|
+
inputSchema: {
|
|
1041
|
+
plan_id: z.string().optional().describe("The plan_id from plan_film. Preferred: it is the plan and price the user saw."),
|
|
1042
|
+
brief: z.string().min(1).max(2000).optional().describe("Only when there is no plan_id: the film idea, planned and made in one go."),
|
|
1043
|
+
...filmOptions,
|
|
1044
|
+
max_cost_cents: z
|
|
1045
|
+
.number()
|
|
1046
|
+
.int()
|
|
1047
|
+
.min(1)
|
|
1048
|
+
.describe("Spend ceiling in cents. Refused up front if the estimate is higher; the charge never goes above it."),
|
|
1049
|
+
webhook_url: z.string().url().optional().describe("Optional https URL that receives the finished film."),
|
|
1050
|
+
webhook_secret: z.string().optional().describe("Optional secret that signs the webhook delivery."),
|
|
1051
|
+
},
|
|
1052
|
+
outputSchema: filmOutput,
|
|
1053
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
1054
|
+
}, wrap(async ({ plan_id, brief, duration_s, aspect_ratio, resolution, language, ...rest }) => {
|
|
1055
|
+
if (!plan_id && !brief) {
|
|
1056
|
+
throw new CorentApiError(422, { message: "Pass plan_id from plan_film, or a brief." });
|
|
1057
|
+
}
|
|
1058
|
+
const options = { duration_s, aspect_ratio, resolution, language };
|
|
1059
|
+
if (plan_id && (brief || Object.values(options).some((v) => v !== undefined))) {
|
|
1060
|
+
throw new CorentApiError(422, {
|
|
1061
|
+
message: "A plan already fixes the story, length, shape and resolution: pass plan_id on its own, or call plan_film again with the new options.",
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
const body = plan_id ? { plan_id, ...rest } : { brief, ...options, ...rest };
|
|
1065
|
+
return corent("/v1/films", {
|
|
1066
|
+
method: "POST",
|
|
1067
|
+
headers: { "Idempotency-Key": idempotencyKey() },
|
|
1068
|
+
body: JSON.stringify(body),
|
|
1069
|
+
});
|
|
762
1070
|
}));
|
|
1071
|
+
server.registerTool("get_film", {
|
|
1072
|
+
title: "Check film progress",
|
|
1073
|
+
_meta: toolMeta("Checking film", "Film checked", true),
|
|
1074
|
+
description: "Progress of a film from make_film: the stage (queued, writing, casting, shooting, checking, editing, completed, failed, cancelled), progress_percent, and each scene. When completed it carries video_url, the finished film with sound, and cost_cents. Read-only and free. Tell the user the stage in one plain sentence.",
|
|
1075
|
+
inputSchema: { film_id: z.string().min(1).describe("The film_id returned by make_film") },
|
|
1076
|
+
outputSchema: filmOutput,
|
|
1077
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
1078
|
+
}, wrap(async ({ film_id }) => corent(`/v1/films/${encodeURIComponent(film_id)}`)));
|
|
1079
|
+
server.registerTool("cancel_film", {
|
|
1080
|
+
title: "Cancel a film",
|
|
1081
|
+
_meta: toolMeta("Cancelling film", "Film cancelled"),
|
|
1082
|
+
description: "Stop a film that has not finished. A cancelled film is not billed. Use it as soon as the user says they did not mean to start it or want to change the story. A film that already finished stays completed.",
|
|
1083
|
+
inputSchema: { film_id: z.string().min(1).describe("The film_id to cancel") },
|
|
1084
|
+
outputSchema: filmOutput,
|
|
1085
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
1086
|
+
}, wrap(async ({ film_id }) => corent(`/v1/films/${encodeURIComponent(film_id)}/cancel`, { method: "POST" })));
|
|
763
1087
|
// --- Image tools (v0.8.0): edit what already exists instead of generating
|
|
764
1088
|
// anew. All three are synchronous, flat-priced, and replay-safe under an
|
|
765
1089
|
// Idempotency-Key exactly like generate_image. ---
|