@instavar/mcp-server 0.6.0 → 0.7.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 +42 -23
- package/dist/index.js +2826 -611
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -236,12 +236,18 @@ function prepareAnnouncement(raw) {
|
|
|
236
236
|
instructions: "Save these files in a new directory. Install and setup download pinned dependencies and Instavar fonts; setup embeds fonts locally before rendering. Review preview frames before rendering. Keep announcement.project.json as the copy source of truth. This tool has not written files, saved a hosted project or rendered video. Regenerating source preserves only the three copy fields, not custom source edits."
|
|
237
237
|
};
|
|
238
238
|
}
|
|
239
|
-
function
|
|
239
|
+
function pairedAnnouncementInstructions(pairedCatalogue = false) {
|
|
240
|
+
return pairedCatalogue ? PAIRED_ANNOUNCEMENT_INSTRUCTIONS.replace(
|
|
241
|
+
"The existing VideoSpec catalogue remains Remotion-only.",
|
|
242
|
+
"VideoSpec templates use prepare_video_template in either framework."
|
|
243
|
+
) : PAIRED_ANNOUNCEMENT_INSTRUCTIONS;
|
|
244
|
+
}
|
|
245
|
+
function registerPairedAnnouncement(server, options = {}) {
|
|
240
246
|
server.registerTool(
|
|
241
247
|
"prepare_announcement",
|
|
242
248
|
{
|
|
243
249
|
title: "Prepare an announcement in Remotion or HyperFrames",
|
|
244
|
-
description: "Prepare one paired announcement-card-v1 template, continue its caller-held project, edit copy or switch frameworks. Returns source files for local rendering. " +
|
|
250
|
+
description: "Prepare one paired announcement-card-v1 template, continue its caller-held project, edit copy or switch frameworks. Returns source files for local rendering. " + pairedAnnouncementInstructions(options.pairedCatalogue),
|
|
245
251
|
inputSchema: pairedAnnouncementInput,
|
|
246
252
|
annotations: {
|
|
247
253
|
readOnlyHint: true,
|
|
@@ -252,10 +258,10 @@ function registerPairedAnnouncement(server) {
|
|
|
252
258
|
},
|
|
253
259
|
async (args) => {
|
|
254
260
|
try {
|
|
255
|
-
const
|
|
261
|
+
const result3 = prepareAnnouncement(args);
|
|
256
262
|
return {
|
|
257
|
-
structuredContent:
|
|
258
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
263
|
+
structuredContent: result3,
|
|
264
|
+
content: [{ type: "text", text: JSON.stringify(result3) }]
|
|
259
265
|
};
|
|
260
266
|
} catch (error) {
|
|
261
267
|
return {
|
|
@@ -272,12 +278,1168 @@ function registerPairedAnnouncement(server) {
|
|
|
272
278
|
);
|
|
273
279
|
}
|
|
274
280
|
|
|
281
|
+
// src/paired-templates/prepare-video-template.ts
|
|
282
|
+
import { z as z3 } from "zod";
|
|
283
|
+
|
|
284
|
+
// src/paired-templates/media-preflight.ts
|
|
285
|
+
function mediaRequirements(spec, timeline) {
|
|
286
|
+
const requirements = [];
|
|
287
|
+
for (const scene of timeline.scenes) {
|
|
288
|
+
const source = spec.scenes[scene.index];
|
|
289
|
+
if (source.kind !== "video-window") continue;
|
|
290
|
+
requirements.push({
|
|
291
|
+
path: String(source.content.src),
|
|
292
|
+
role: `scene ${scene.index + 1} video`,
|
|
293
|
+
kind: "video",
|
|
294
|
+
minSeconds: scene.visibleFrames / timeline.fps
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
if (spec.audio.narrationSrc)
|
|
298
|
+
requirements.push({
|
|
299
|
+
path: spec.audio.narrationSrc,
|
|
300
|
+
role: "narration",
|
|
301
|
+
kind: "audio"
|
|
302
|
+
});
|
|
303
|
+
if (spec.audio.musicSrc)
|
|
304
|
+
requirements.push({
|
|
305
|
+
path: spec.audio.musicSrc,
|
|
306
|
+
role: "music",
|
|
307
|
+
kind: "audio",
|
|
308
|
+
minSeconds: timeline.durationInFrames / timeline.fps
|
|
309
|
+
});
|
|
310
|
+
return requirements;
|
|
311
|
+
}
|
|
312
|
+
function preflightScript(requirements, fps) {
|
|
313
|
+
return `// Paired template media preflight. Run before rendering in either framework.
|
|
314
|
+
import { access } from "node:fs/promises";
|
|
315
|
+
import { execFileSync } from "node:child_process";
|
|
316
|
+
|
|
317
|
+
const requirements = ${JSON.stringify(requirements, null, 2)};
|
|
318
|
+
const tolerance = ${0.5 / fps};
|
|
319
|
+
function probe(file) {
|
|
320
|
+
try {
|
|
321
|
+
return JSON.parse(
|
|
322
|
+
execFileSync("ffprobe", ["-v", "error", "-show_streams", "-show_format", "-of", "json", file], { encoding: "utf8" })
|
|
323
|
+
);
|
|
324
|
+
} catch {
|
|
325
|
+
throw new Error("ffprobe could not read " + file + ". Install FFmpeg and check the file.");
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
for (const item of requirements) {
|
|
329
|
+
try {
|
|
330
|
+
await access(item.path);
|
|
331
|
+
} catch {
|
|
332
|
+
throw new Error("Missing " + item.role + " file " + item.path + ". Copy it to that path first.");
|
|
333
|
+
}
|
|
334
|
+
const info = probe(item.path);
|
|
335
|
+
const duration = Number(info.format?.duration ?? 0);
|
|
336
|
+
const stream = info.streams.find((entry) => entry.codec_type === item.kind);
|
|
337
|
+
if (!stream) throw new Error(item.role + " file " + item.path + " has no " + item.kind + " stream.");
|
|
338
|
+
if (item.kind === "audio" && stream.channels !== 2)
|
|
339
|
+
throw new Error(item.role + " file " + item.path + " must be stereo. Convert it first, for example: ffmpeg -i input -ac 2 output.wav");
|
|
340
|
+
// Untagged video is read as BT.709 by Remotion and BT.601 by HyperFrames,
|
|
341
|
+
// so saturated colours differ. Only tagged BT.709 SDR video renders alike.
|
|
342
|
+
if (item.kind === "video" && [stream.color_space, stream.color_primaries, stream.color_transfer].some((tag) => tag !== "bt709"))
|
|
343
|
+
throw new Error(item.role + " file " + item.path + " must be tagged BT.709 SDR (colour space " + stream.color_space + ", primaries " + stream.color_primaries + ", transfer " + stream.color_transfer + "). Convert it first, for example: ffmpeg -i input -vf scale=out_color_matrix=bt709:out_range=tv,format=yuv420p,setparams=range=tv:color_primaries=bt709:color_trc=bt709:colorspace=bt709 -c:v libx264 -crf 16 output.mp4");
|
|
344
|
+
if (item.minSeconds !== undefined && duration + tolerance < item.minSeconds)
|
|
345
|
+
throw new Error(item.role + " file " + item.path + " lasts " + duration + " s but must cover " + item.minSeconds + " s.");
|
|
346
|
+
}
|
|
347
|
+
console.log("Media preflight passed for " + requirements.length + " file(s).");
|
|
348
|
+
`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/paired-templates/spring.ts
|
|
352
|
+
var CONFIG = { damping: 200, mass: 1, stiffness: 100 };
|
|
353
|
+
var REST_THRESHOLD = 5e-3;
|
|
354
|
+
function advance(animation, now) {
|
|
355
|
+
const { toValue, lastTimestamp, current, velocity } = animation;
|
|
356
|
+
const deltaTime = Math.min(now - lastTimestamp, 64);
|
|
357
|
+
const c = CONFIG.damping;
|
|
358
|
+
const m = CONFIG.mass;
|
|
359
|
+
const k = CONFIG.stiffness;
|
|
360
|
+
const v0 = -velocity;
|
|
361
|
+
const x0 = toValue - current;
|
|
362
|
+
const zeta = c / (2 * Math.sqrt(k * m));
|
|
363
|
+
const omega0 = Math.sqrt(k / m);
|
|
364
|
+
const omega1 = omega0 * Math.sqrt(1 - zeta ** 2);
|
|
365
|
+
const t = deltaTime / 1e3;
|
|
366
|
+
const sin1 = Math.sin(omega1 * t);
|
|
367
|
+
const cos1 = Math.cos(omega1 * t);
|
|
368
|
+
const underDampedEnvelope = Math.exp(-zeta * omega0 * t);
|
|
369
|
+
const underDampedFrag1 = underDampedEnvelope * (sin1 * ((v0 + zeta * omega0 * x0) / omega1) + x0 * cos1);
|
|
370
|
+
const underDampedPosition = toValue - underDampedFrag1;
|
|
371
|
+
const underDampedVelocity = zeta * omega0 * underDampedFrag1 - underDampedEnvelope * (cos1 * (v0 + zeta * omega0 * x0) - omega1 * x0 * sin1);
|
|
372
|
+
const criticallyDampedEnvelope = Math.exp(-omega0 * t);
|
|
373
|
+
const criticallyDampedPosition = toValue - criticallyDampedEnvelope * (x0 + (v0 + omega0 * x0) * t);
|
|
374
|
+
const criticallyDampedVelocity = criticallyDampedEnvelope * (v0 * (t * omega0 - 1) + t * x0 * omega0 * omega0);
|
|
375
|
+
return {
|
|
376
|
+
toValue,
|
|
377
|
+
lastTimestamp: now,
|
|
378
|
+
current: zeta < 1 ? underDampedPosition : criticallyDampedPosition,
|
|
379
|
+
velocity: zeta < 1 ? underDampedVelocity : criticallyDampedVelocity
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
function springCalculation(frame2, fps) {
|
|
383
|
+
let animation = {
|
|
384
|
+
lastTimestamp: 0,
|
|
385
|
+
current: 0,
|
|
386
|
+
toValue: 1,
|
|
387
|
+
velocity: 0
|
|
388
|
+
};
|
|
389
|
+
const frameClamped = Math.max(0, frame2);
|
|
390
|
+
const unevenRest = frameClamped % 1;
|
|
391
|
+
for (let f = 0; f <= Math.floor(frameClamped); f++) {
|
|
392
|
+
if (f === Math.floor(frameClamped)) f += unevenRest;
|
|
393
|
+
animation = advance(animation, f / fps * 1e3);
|
|
394
|
+
}
|
|
395
|
+
return animation;
|
|
396
|
+
}
|
|
397
|
+
function measureSpring(fps) {
|
|
398
|
+
let frame2 = 0;
|
|
399
|
+
let animation = springCalculation(frame2, fps);
|
|
400
|
+
let difference = Math.abs(animation.current - animation.toValue);
|
|
401
|
+
while (difference >= REST_THRESHOLD) {
|
|
402
|
+
frame2++;
|
|
403
|
+
animation = springCalculation(frame2, fps);
|
|
404
|
+
difference = Math.abs(animation.current - animation.toValue);
|
|
405
|
+
}
|
|
406
|
+
let finishedFrame = frame2;
|
|
407
|
+
for (let i = 0; i < 20; i++) {
|
|
408
|
+
frame2++;
|
|
409
|
+
animation = springCalculation(frame2, fps);
|
|
410
|
+
difference = Math.abs(animation.current - animation.toValue);
|
|
411
|
+
if (difference >= REST_THRESHOLD) {
|
|
412
|
+
i = 0;
|
|
413
|
+
finishedFrame = frame2 + 1;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return finishedFrame;
|
|
417
|
+
}
|
|
418
|
+
function springSamples(fps) {
|
|
419
|
+
const durationInFrames = Math.round(fps * 0.7);
|
|
420
|
+
const naturalDuration = measureSpring(fps);
|
|
421
|
+
return Array.from(
|
|
422
|
+
{ length: durationInFrames + 2 },
|
|
423
|
+
(_, frame2) => frame2 > durationInFrames ? 1 : springCalculation(frame2 / (durationInFrames / naturalDuration), fps).current
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/paired-templates/hyperframes-videospec.ts
|
|
428
|
+
var HYPERFRAMES_RUNTIME = { hyperframes: "0.8.143", gsap: "3.14.2" };
|
|
429
|
+
var LATIN_RANGE = "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD";
|
|
430
|
+
var PAIRED_FONTS = [
|
|
431
|
+
{
|
|
432
|
+
family: "Fraunces",
|
|
433
|
+
file: "Fraunces.woff2",
|
|
434
|
+
url: "https://fonts.gstatic.com/s/fraunces/v38/6NUu8FyLNQOQZAnv9bYEvDiIdE9Ea92uemAk_WBq8U_9v0c2Wa0K7iN7hzFUPJH58nib14c7qv8.woff2",
|
|
435
|
+
sha256: "7f9d191d999336d3b9790afa72e1358e50a13b06d4f289341e92a311967a80f9"
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
family: "Inter",
|
|
439
|
+
file: "Inter.woff2",
|
|
440
|
+
url: "https://fonts.gstatic.com/s/inter/v20/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2",
|
|
441
|
+
sha256: "3100e775e8616cd2611beecfa23a4263d7037586789b43f035236a2e6fbd4c62"
|
|
442
|
+
}
|
|
443
|
+
];
|
|
444
|
+
var palettes = {
|
|
445
|
+
"editorial-light": {
|
|
446
|
+
background: "#f4efe5",
|
|
447
|
+
surface: "#fffaf0",
|
|
448
|
+
ink: "#18241f",
|
|
449
|
+
muted: "#667169",
|
|
450
|
+
accent: "#2f6d5a",
|
|
451
|
+
accentSoft: "#cfe0d5",
|
|
452
|
+
signal: "#c96f58"
|
|
453
|
+
},
|
|
454
|
+
"editorial-dark": {
|
|
455
|
+
background: "#171c1a",
|
|
456
|
+
surface: "#222a27",
|
|
457
|
+
ink: "#f5efe3",
|
|
458
|
+
muted: "#abb7ae",
|
|
459
|
+
accent: "#a8cfb8",
|
|
460
|
+
accentSoft: "#30483e",
|
|
461
|
+
signal: "#ec967d"
|
|
462
|
+
},
|
|
463
|
+
clinical: {
|
|
464
|
+
background: "#f2f1e9",
|
|
465
|
+
surface: "#fbfaf5",
|
|
466
|
+
ink: "#14283a",
|
|
467
|
+
muted: "#60727c",
|
|
468
|
+
accent: "#3d796d",
|
|
469
|
+
accentSoft: "#d6e5df",
|
|
470
|
+
signal: "#d2705d"
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
function paletteFor(spec) {
|
|
474
|
+
if (spec.style.theme !== "default") return palettes[spec.style.theme];
|
|
475
|
+
return spec.templateFamily === "announcement-brief" ? palettes["editorial-light"] : palettes["editorial-dark"];
|
|
476
|
+
}
|
|
477
|
+
var escapeHtml = (value) => value.replace(
|
|
478
|
+
/[&<>"']/g,
|
|
479
|
+
(character) => ({
|
|
480
|
+
"&": "&",
|
|
481
|
+
"<": "<",
|
|
482
|
+
">": ">",
|
|
483
|
+
'"': """,
|
|
484
|
+
"'": "'"
|
|
485
|
+
})[character]
|
|
486
|
+
);
|
|
487
|
+
var str = (value) => typeof value === "string" ? value : "";
|
|
488
|
+
var div = (className, value, attributes = "") => `<div class="${className}"${attributes}>${escapeHtml(value)}</div>`;
|
|
489
|
+
function sceneCss(palette) {
|
|
490
|
+
const p = palette;
|
|
491
|
+
const fonts = PAIRED_FONTS.map(
|
|
492
|
+
(font) => `@font-face{font-family:${font.family};src:url(${font.file}) format('woff2');font-weight:100 900;font-style:normal;unicode-range:${LATIN_RANGE}}`
|
|
493
|
+
).join("");
|
|
494
|
+
return `${fonts}
|
|
495
|
+
*{box-sizing:border-box}
|
|
496
|
+
.fill{position:absolute;top:0;left:0;right:0;bottom:0;width:100%;height:100%;display:flex;flex-direction:column}
|
|
497
|
+
.background{background:${p.background}}
|
|
498
|
+
.orb{position:absolute;width:760px;height:760px;border-radius:50%;background:${p.accentSoft};opacity:.42;right:-280px;top:-300px}
|
|
499
|
+
.ring{position:absolute;width:420px;height:420px;border-radius:50%;border:2px solid ${p.accent};opacity:.16;left:-220px;bottom:80px}
|
|
500
|
+
.layout{justify-content:center;padding:11% 9%}
|
|
501
|
+
.layout.hero{justify-content:space-between}
|
|
502
|
+
.layout.mechanism,.layout.points,.layout.equation{padding:10% 8.5%}
|
|
503
|
+
.layout.video-window{padding:8%}
|
|
504
|
+
.title{color:${p.ink};font-family:Fraunces;font-weight:600}
|
|
505
|
+
.subtitle{color:${p.muted};font-family:Inter}
|
|
506
|
+
.label{color:${p.muted};font-family:Inter;font-size:26px;font-weight:600;letter-spacing:.2px;margin-bottom:32px}
|
|
507
|
+
.accent-bar{width:116px;height:12px;border-radius:12px;background:${p.accent};transform-origin:left}
|
|
508
|
+
.hero-title{line-height:.98;letter-spacing:-2.6px;max-width:94%}
|
|
509
|
+
.hero-sub{font-size:38px;line-height:1.35;margin-top:44px;max-width:82%}
|
|
510
|
+
.footer{color:${p.accent};font-family:Inter;font-size:25px;font-weight:600}
|
|
511
|
+
.statement-card{width:88%;padding:70px 64px;border-radius:44px;background:${p.surface};box-shadow:0 32px 90px rgba(20,40,58,.10)}
|
|
512
|
+
.statement-title{font-size:88px;line-height:1.02}
|
|
513
|
+
.statement-sub{font-size:34px;line-height:1.4;margin-top:34px}
|
|
514
|
+
.mechanism-title{font-size:88px;line-height:1;letter-spacing:-1.5px}
|
|
515
|
+
.mechanism-sub{font-size:32px;line-height:1.35;margin-top:24px}
|
|
516
|
+
.states{display:grid;grid-template-columns:1fr 112px 1fr;align-items:center;gap:20px;margin-top:84px}
|
|
517
|
+
.state-card{height:520px;border-radius:220px;background:${p.surface};box-shadow:0 28px 70px rgba(20,40,58,.10);display:flex;flex-direction:column;align-items:center;justify-content:center;padding:28px}
|
|
518
|
+
.capsule{width:180px;height:290px;border-radius:90px;border:18px solid ${p.accent};opacity:.9}
|
|
519
|
+
.capsule.before{border-color:${p.signal}}
|
|
520
|
+
.state-label{color:${p.ink};font-family:Inter;font-size:29px;font-weight:600;margin-top:34px;text-align:center}
|
|
521
|
+
.arrow{color:${p.accent};font-family:Inter;font-size:64px;text-align:center}
|
|
522
|
+
.points-head{max-width:860px}
|
|
523
|
+
.points-title{line-height:1.02;letter-spacing:-1.7px}
|
|
524
|
+
.points-sub{font-size:32px;line-height:1.4;margin-top:24px}
|
|
525
|
+
.point-list{display:grid;gap:22px;margin-top:70px}
|
|
526
|
+
.point{display:grid;grid-template-columns:70px 1fr;align-items:center;gap:28px;background:${p.surface};border-radius:28px;padding:30px 34px;box-shadow:0 18px 52px rgba(20,40,58,.07)}
|
|
527
|
+
.number{width:58px;height:58px;border-radius:50%;background:${p.accentSoft};color:${p.accent};display:flex;align-items:center;justify-content:center;font-family:Inter;font-size:27px;font-weight:700}
|
|
528
|
+
.point-text{color:${p.ink};font-family:Inter;font-size:35px;font-weight:500;line-height:1.25}
|
|
529
|
+
.check{width:124px;height:124px;border-radius:50%;background:${p.accent};color:${p.background};font-family:Inter;font-size:58px;display:flex;align-items:center;justify-content:center;margin-bottom:58px}
|
|
530
|
+
.cta-title{font-size:104px;line-height:.98;letter-spacing:-2px}
|
|
531
|
+
.cta-sub{font-size:34px;line-height:1.4;margin-top:38px}
|
|
532
|
+
.equation-title{font-size:84px;line-height:1.02}
|
|
533
|
+
.equations{display:grid;gap:24px;margin-top:58px}
|
|
534
|
+
.equation-block{color:${p.ink};background:${p.surface};font-size:40px;padding:30px;border-radius:24px}
|
|
535
|
+
.window-title{font-size:72px;line-height:1.04;margin-bottom:42px}
|
|
536
|
+
.window{position:relative;height:66%;overflow:hidden;border-radius:36px;background:${p.surface};box-shadow:0 28px 80px rgba(20,40,58,.14)}
|
|
537
|
+
.window video{position:absolute;top:0;left:0;width:100%;height:100%;object-fit:cover}
|
|
538
|
+
.scene-label{position:absolute;left:54px;bottom:40px;color:${p.muted};font-family:Inter;font-size:18px}`;
|
|
539
|
+
}
|
|
540
|
+
function title(content, className, fit = "") {
|
|
541
|
+
return div(
|
|
542
|
+
`title ${className}`,
|
|
543
|
+
str(content.title) || str(content.line1),
|
|
544
|
+
fit
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
function subtitle(content, className) {
|
|
548
|
+
const value = str(content.subtitle) || str(content.line2);
|
|
549
|
+
return value ? div(`subtitle ${className}`, value) : "";
|
|
550
|
+
}
|
|
551
|
+
function pointsSubtitle(content) {
|
|
552
|
+
const value = str(content.subtitle);
|
|
553
|
+
return value ? div("subtitle points-sub", value) : "";
|
|
554
|
+
}
|
|
555
|
+
function label(content) {
|
|
556
|
+
const value = str(content.label);
|
|
557
|
+
return value ? div("label", value) : "";
|
|
558
|
+
}
|
|
559
|
+
function sceneBody(scene, id2, seconds) {
|
|
560
|
+
const c = scene.content;
|
|
561
|
+
switch (scene.kind) {
|
|
562
|
+
case "hero":
|
|
563
|
+
return `<div class="accent-bar" data-motion="bar"></div><div data-motion="rise" data-rise="42">${title(
|
|
564
|
+
c,
|
|
565
|
+
"hero-title",
|
|
566
|
+
' data-fit="885.6,126"'
|
|
567
|
+
)}${subtitle(c, "hero-sub")}</div>${div("footer", str(c.footer))}`;
|
|
568
|
+
case "statement":
|
|
569
|
+
case "custom":
|
|
570
|
+
return `<div class="statement-card" data-motion="rise" data-rise="36">${label(
|
|
571
|
+
c
|
|
572
|
+
)}${title(c, "statement-title")}${subtitle(c, "statement-sub")}</div>`;
|
|
573
|
+
case "mechanism": {
|
|
574
|
+
const state = (value, side) => `<div class="state-card"><div class="capsule ${side}" data-motion="${side}"></div>${div(
|
|
575
|
+
"state-label",
|
|
576
|
+
value
|
|
577
|
+
)}</div>`;
|
|
578
|
+
return `<div data-motion="fade">${label(c)}${title(
|
|
579
|
+
c,
|
|
580
|
+
"mechanism-title"
|
|
581
|
+
)}${str(c.subtitle) ? div("subtitle mechanism-sub", str(c.subtitle)) : ""}</div><div class="states">${state(
|
|
582
|
+
str(c.before),
|
|
583
|
+
"before"
|
|
584
|
+
)}<div class="arrow" data-motion="arrow">\u2192</div>${state(
|
|
585
|
+
str(c.after),
|
|
586
|
+
"after"
|
|
587
|
+
)}</div>`;
|
|
588
|
+
}
|
|
589
|
+
case "points": {
|
|
590
|
+
const points = c.points ?? [];
|
|
591
|
+
return `<div class="points-head">${label(c)}${title(
|
|
592
|
+
c,
|
|
593
|
+
"points-title",
|
|
594
|
+
' data-fit="850,98"'
|
|
595
|
+
)}${pointsSubtitle(c)}</div><div class="point-list">${points.map(
|
|
596
|
+
(point2, index) => `<div class="point" data-motion="rise" data-rise="28" data-delay="${index * 6}">${div("number", String(index + 1))}${div(
|
|
597
|
+
"point-text",
|
|
598
|
+
point2
|
|
599
|
+
)}</div>`
|
|
600
|
+
).join("")}</div>`;
|
|
601
|
+
}
|
|
602
|
+
case "equation":
|
|
603
|
+
return `${div(
|
|
604
|
+
"title equation-title",
|
|
605
|
+
str(c.title)
|
|
606
|
+
)}<div class="equations">${c.latexBlocks.map(
|
|
607
|
+
(block) => `<code class="equation-block">${escapeHtml(block)}</code>`
|
|
608
|
+
).join("")}</div>`;
|
|
609
|
+
case "video-window":
|
|
610
|
+
return `${div(
|
|
611
|
+
"title window-title",
|
|
612
|
+
str(c.title)
|
|
613
|
+
)}<div class="window"><video id="${id2}-video" class="clip" src="${escapeHtml(
|
|
614
|
+
str(c.src)
|
|
615
|
+
)}" data-start="0" data-duration="${seconds}" data-track-index="1" muted playsinline></video></div>`;
|
|
616
|
+
case "cta":
|
|
617
|
+
return `<div class="check" data-motion="pop">\u2713</div>${title(
|
|
618
|
+
c,
|
|
619
|
+
"cta-title"
|
|
620
|
+
)}${subtitle(c, "cta-sub")}`;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function sceneScript(id2, seconds, fps) {
|
|
624
|
+
const samples = springSamples(fps);
|
|
625
|
+
const span = samples.length - 1;
|
|
626
|
+
return `(async()=>{
|
|
627
|
+
const root=document.getElementById(${JSON.stringify(id2)});
|
|
628
|
+
await Promise.all(['600 100px Fraunces','400 38px Inter','500 35px Inter','600 29px Inter','700 27px Inter'].map(f=>document.fonts.load(f)));
|
|
629
|
+
root.querySelectorAll('[data-fit]').forEach(e=>{const [width,cap]=e.dataset.fit.split(',').map(Number);const span=document.createElement('span');Object.assign(span.style,{fontFamily:'Fraunces',fontWeight:'600',fontSize:'100px',display:'inline-block',position:'absolute',top:'-10000px',whiteSpace:'pre',textRendering:'auto'});span.innerText=e.textContent;document.body.append(span);const size=Math.max(58,Math.min(cap,width/span.getBoundingClientRect().width*100*2.25));span.remove();e.style.fontSize=size+'px';});
|
|
630
|
+
const samples=${JSON.stringify(samples)};
|
|
631
|
+
const enter=f=>{if(f<=0)return samples[0];if(f>=${span})return 1;const i=Math.floor(f);return samples[i]+(samples[i+1]-samples[i])*(f-i);};
|
|
632
|
+
const tl=gsap.timeline({paused:true});
|
|
633
|
+
tl.to({},{duration:${seconds}},0);
|
|
634
|
+
const spring={duration:${span / fps},ease:t=>enter(t*${span}),force3D:false};
|
|
635
|
+
root.querySelectorAll('[data-motion="rise"]').forEach(e=>tl.fromTo(e,{opacity:0,y:Number(e.dataset.rise)},{opacity:1,y:0,...spring},Number(e.dataset.delay||0)/${fps}));
|
|
636
|
+
root.querySelectorAll('[data-motion="fade"]').forEach(e=>tl.fromTo(e,{opacity:0},{opacity:1,...spring},0));
|
|
637
|
+
root.querySelectorAll('[data-motion="bar"]').forEach(e=>tl.fromTo(e,{scaleX:0},{scaleX:1,...spring},0));
|
|
638
|
+
root.querySelectorAll('[data-motion="pop"]').forEach(e=>tl.fromTo(e,{scale:0},{scale:1,...spring},0));
|
|
639
|
+
const calm={duration:1.95,ease:'power1.inOut',force3D:false};
|
|
640
|
+
root.querySelectorAll('[data-motion="before"]').forEach(e=>tl.fromTo(e,{scaleX:1},{scaleX:0.66,...calm},0.45));
|
|
641
|
+
root.querySelectorAll('[data-motion="after"]').forEach(e=>tl.fromTo(e,{scaleX:0.45},{scaleX:0.25,...calm},0.45));
|
|
642
|
+
root.querySelectorAll('[data-motion="arrow"]').forEach(e=>tl.fromTo(e,{x:-14},{x:14,...calm},0.45));
|
|
643
|
+
window.__timelines=window.__timelines||{};window.__timelines[${JSON.stringify(
|
|
644
|
+
id2
|
|
645
|
+
)}]=tl;window.__hfForceTimelineRebind?.();
|
|
646
|
+
})();`;
|
|
647
|
+
}
|
|
648
|
+
function hyperframesVideoSpecFiles(spec, timeline) {
|
|
649
|
+
const palette = paletteFor(spec);
|
|
650
|
+
const css = sceneCss(palette);
|
|
651
|
+
const { fps, width, height } = timeline;
|
|
652
|
+
const total = timeline.durationInFrames / fps;
|
|
653
|
+
const files = {};
|
|
654
|
+
const hosts = [];
|
|
655
|
+
for (const entry of timeline.scenes) {
|
|
656
|
+
if (entry.visibleFrames === 0) continue;
|
|
657
|
+
const scene = spec.scenes[entry.index];
|
|
658
|
+
const id2 = `scene-${entry.index + 1}`;
|
|
659
|
+
const seconds = entry.visibleFrames / fps;
|
|
660
|
+
const body = `<div class="fill background"><div class="orb" data-layout-allow-overflow></div><div class="ring" data-layout-allow-overflow></div></div><div class="fill layout ${scene.kind}">${sceneBody(scene, id2, seconds)}</div>${spec.style.showSceneLabels ? div("scene-label", scene.kind) : ""}`;
|
|
661
|
+
files[`compositions/${id2}.html`] = `<template><style>${css}
|
|
662
|
+
#${id2}{position:absolute;inset:0;width:100%;height:100%;overflow:hidden}
|
|
663
|
+
#${id2},#${id2} *{text-rendering:auto}</style><div id="${id2}" data-composition-id="${id2}" data-scene-id="${escapeHtml(
|
|
664
|
+
scene.id
|
|
665
|
+
)}" data-start="0" data-duration="${seconds}" data-width="${width}" data-height="${height}">${body}</div><script>${sceneScript(
|
|
666
|
+
id2,
|
|
667
|
+
seconds,
|
|
668
|
+
fps
|
|
669
|
+
)}</script></template>
|
|
670
|
+
`;
|
|
671
|
+
hosts.push(
|
|
672
|
+
`<div id="${id2}" class="clip" data-composition-id="${id2}" data-composition-src="compositions/${id2}.html" data-start="${entry.startFrame / fps}" data-duration="${seconds}" data-track-index="0"></div>`
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
const audio = spec.audio;
|
|
676
|
+
if (audio.musicSrc)
|
|
677
|
+
hosts.push(
|
|
678
|
+
`<audio id="music" src="${escapeHtml(
|
|
679
|
+
audio.musicSrc
|
|
680
|
+
)}" data-start="0" data-duration="${total}" data-track-index="10" data-volume="${audio.ducking && audio.narrationSrc ? audio.musicVolume * 0.32 : audio.musicVolume}"></audio>`
|
|
681
|
+
);
|
|
682
|
+
if (audio.narrationSrc)
|
|
683
|
+
hosts.push(
|
|
684
|
+
`<audio id="narration" src="${escapeHtml(
|
|
685
|
+
audio.narrationSrc
|
|
686
|
+
)}" data-start="0" data-duration="${total}" data-track-index="11" data-volume="${audio.narrationVolume}"></audio>`
|
|
687
|
+
);
|
|
688
|
+
files["index.html"] = `<!doctype html>
|
|
689
|
+
<html lang="en"><head><meta charset="utf-8"><title>${escapeHtml(
|
|
690
|
+
spec.meta.title
|
|
691
|
+
)}</title><style>html,body{margin:0;width:100%;height:100%;background:${palette.background}}#video{position:absolute;inset:0;overflow:hidden;background:${palette.background}}.clip{position:absolute;inset:0}</style><script src="gsap.min.js"></script></head><body>
|
|
692
|
+
<div id="video" data-composition-id="video" data-start="0" data-width="${width}" data-height="${height}" data-duration="${total}">
|
|
693
|
+
${hosts.join("\n")}
|
|
694
|
+
</div>
|
|
695
|
+
<script>const tl=gsap.timeline({paused:true});tl.to({},{duration:${total}});window.__timelines=window.__timelines||{};window.__timelines.video=tl;window.__hfForceTimelineRebind?.();</script>
|
|
696
|
+
</body></html>
|
|
697
|
+
`;
|
|
698
|
+
files["video.json"] = JSON.stringify(spec, null, 2) + "\n";
|
|
699
|
+
files["hyperframes.json"] = JSON.stringify({ authoringSkill: "general-video" }, null, 2) + "\n";
|
|
700
|
+
files["package.json"] = JSON.stringify(
|
|
701
|
+
{
|
|
702
|
+
private: true,
|
|
703
|
+
type: "module",
|
|
704
|
+
scripts: {
|
|
705
|
+
setup: "node setup.mjs",
|
|
706
|
+
check: "hyperframes check",
|
|
707
|
+
preview: "hyperframes preview",
|
|
708
|
+
// Extract source video frames as PNG. The default JPEG extraction
|
|
709
|
+
// re-subsamples chroma and leaves 1:1 windows of saturated footage
|
|
710
|
+
// just under the similarity floor; under HyperFrames 0.8.143 PNG
|
|
711
|
+
// extraction no longer darkens BT.709 footage.
|
|
712
|
+
render: `hyperframes render --fps ${fps} --video-frame-format png --output video.mp4`
|
|
713
|
+
},
|
|
714
|
+
dependencies: HYPERFRAMES_RUNTIME
|
|
715
|
+
},
|
|
716
|
+
null,
|
|
717
|
+
2
|
|
718
|
+
) + "\n";
|
|
719
|
+
files["preflight.mjs"] = preflightScript(
|
|
720
|
+
mediaRequirements(spec, timeline),
|
|
721
|
+
fps
|
|
722
|
+
);
|
|
723
|
+
files["setup.mjs"] = `// Fetch the pinned fonts, copy the local GSAP runtime, then check media.
|
|
724
|
+
import { createHash } from "node:crypto";
|
|
725
|
+
import { copyFile, writeFile } from "node:fs/promises";
|
|
726
|
+
|
|
727
|
+
const fonts = ${JSON.stringify(
|
|
728
|
+
PAIRED_FONTS.map(({ file, url, sha256 }) => ({ file, url, sha256 })),
|
|
729
|
+
null,
|
|
730
|
+
2
|
|
731
|
+
)};
|
|
732
|
+
for (const font of fonts) {
|
|
733
|
+
const response = await fetch(font.url);
|
|
734
|
+
if (!response.ok) throw new Error("Font download failed: " + font.url);
|
|
735
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
736
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
737
|
+
if (digest !== font.sha256)
|
|
738
|
+
throw new Error("Font " + font.file + " changed upstream; expected sha-256 " + font.sha256);
|
|
739
|
+
await writeFile(font.file, bytes);
|
|
740
|
+
}
|
|
741
|
+
await copyFile("node_modules/gsap/dist/gsap.min.js", "gsap.min.js");
|
|
742
|
+
await import("./preflight.mjs");
|
|
743
|
+
`;
|
|
744
|
+
return files;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// src/paired-templates/remotion-videospec.ts
|
|
748
|
+
var REMOTION_TEMPLATES = {
|
|
749
|
+
repository: "https://github.com/instavar/remotion-templates",
|
|
750
|
+
commit: "a9092254937d28a05a277a9b6ecfd4ebea864245",
|
|
751
|
+
remotion: "4.0.502"
|
|
752
|
+
};
|
|
753
|
+
function remotionVideoSpecFiles(spec, timeline) {
|
|
754
|
+
return {
|
|
755
|
+
"video.json": JSON.stringify(spec, null, 2) + "\n",
|
|
756
|
+
"preflight.mjs": preflightScript(
|
|
757
|
+
mediaRequirements(spec, timeline),
|
|
758
|
+
timeline.fps
|
|
759
|
+
)
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
function remotionVideoSpecCommands(spec) {
|
|
763
|
+
return [
|
|
764
|
+
`git clone ${REMOTION_TEMPLATES.repository}.git`,
|
|
765
|
+
"cd remotion-templates",
|
|
766
|
+
`git checkout ${REMOTION_TEMPLATES.commit}`,
|
|
767
|
+
"npm ci",
|
|
768
|
+
"# Save video.json and preflight.mjs here and place each referenced media file at its public/ path.",
|
|
769
|
+
"node preflight.mjs",
|
|
770
|
+
"npm run video:validate -- video.json",
|
|
771
|
+
"npm run video:preview -- video.json",
|
|
772
|
+
`npm run video:render -- video.json out/${spec.id}.mp4`
|
|
773
|
+
];
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// src/paired-templates/videospec-contract.ts
|
|
777
|
+
import { z as z2 } from "zod";
|
|
778
|
+
var PAIRED_VIDEOSPEC_CONTRACT = "paired-videospec/v1";
|
|
779
|
+
var PAIRED_TEMPLATE_FAMILIES = [
|
|
780
|
+
"qa-ad",
|
|
781
|
+
"proof-walkthrough",
|
|
782
|
+
"announcement-brief",
|
|
783
|
+
"social-remix",
|
|
784
|
+
"finance-brief"
|
|
785
|
+
];
|
|
786
|
+
var PAIRED_SCENE_KINDS = [
|
|
787
|
+
"hero",
|
|
788
|
+
"statement",
|
|
789
|
+
// The public renderer draws custom as a statement card; so does HyperFrames.
|
|
790
|
+
"custom",
|
|
791
|
+
"mechanism",
|
|
792
|
+
"points",
|
|
793
|
+
"equation",
|
|
794
|
+
"video-window",
|
|
795
|
+
"cta"
|
|
796
|
+
];
|
|
797
|
+
var PAIRED_FPS = { min: 1, max: 240 };
|
|
798
|
+
var PAIRED_LIMITS = {
|
|
799
|
+
scenes: 24,
|
|
800
|
+
sceneDurationMs: 3e5,
|
|
801
|
+
totalDurationMs: 20 * 60 * 1e3
|
|
802
|
+
};
|
|
803
|
+
var text2 = (max) => z2.string().max(max).refine((value) => value.trim().length > 0, "Must not be blank.");
|
|
804
|
+
var mediaPath = (extensions) => z2.string().max(200).refine(
|
|
805
|
+
(value) => /^public\/(?:[A-Za-z0-9_-][A-Za-z0-9._-]*\/)*[A-Za-z0-9_-][A-Za-z0-9._-]*$/.test(
|
|
806
|
+
value
|
|
807
|
+
) && !value.split("/").includes("..") && extensions.some((extension) => value.toLowerCase().endsWith(extension)),
|
|
808
|
+
`Use a local file under public/ ending in ${extensions.join(
|
|
809
|
+
", "
|
|
810
|
+
)}. Remote URLs and other paths are not supported by the paired templates.`
|
|
811
|
+
);
|
|
812
|
+
var videoPath = mediaPath([".mp4"]);
|
|
813
|
+
var audioPath = mediaPath([".wav", ".mp3", ".m4a"]);
|
|
814
|
+
var contentSchemas = {
|
|
815
|
+
hero: z2.object({
|
|
816
|
+
title: text2(160).optional(),
|
|
817
|
+
line1: text2(160).optional(),
|
|
818
|
+
subtitle: text2(400).optional(),
|
|
819
|
+
line2: text2(400).optional(),
|
|
820
|
+
footer: text2(120).optional()
|
|
821
|
+
}).strict(),
|
|
822
|
+
statement: z2.object({
|
|
823
|
+
title: text2(160).optional(),
|
|
824
|
+
line1: text2(160).optional(),
|
|
825
|
+
subtitle: text2(400).optional(),
|
|
826
|
+
line2: text2(400).optional(),
|
|
827
|
+
label: text2(60).optional()
|
|
828
|
+
}).strict(),
|
|
829
|
+
custom: z2.object({
|
|
830
|
+
title: text2(160).optional(),
|
|
831
|
+
line1: text2(160).optional(),
|
|
832
|
+
subtitle: text2(400).optional(),
|
|
833
|
+
line2: text2(400).optional(),
|
|
834
|
+
label: text2(60).optional()
|
|
835
|
+
}).strict(),
|
|
836
|
+
// The public renderer reads only title here and invents copy for missing
|
|
837
|
+
// before/after states, so all three are required.
|
|
838
|
+
mechanism: z2.object({
|
|
839
|
+
title: text2(160),
|
|
840
|
+
subtitle: text2(400).optional(),
|
|
841
|
+
label: text2(60).optional(),
|
|
842
|
+
before: text2(80),
|
|
843
|
+
after: text2(80)
|
|
844
|
+
}).strict(),
|
|
845
|
+
points: z2.object({
|
|
846
|
+
title: text2(160).optional(),
|
|
847
|
+
line1: text2(160).optional(),
|
|
848
|
+
subtitle: text2(400).optional(),
|
|
849
|
+
label: text2(60).optional(),
|
|
850
|
+
points: z2.array(text2(160)).max(8).default([])
|
|
851
|
+
}).strict(),
|
|
852
|
+
equation: z2.object({
|
|
853
|
+
title: text2(160),
|
|
854
|
+
latexBlocks: z2.array(text2(400)).min(1).max(6)
|
|
855
|
+
}).strict(),
|
|
856
|
+
"video-window": z2.object({
|
|
857
|
+
title: text2(160),
|
|
858
|
+
src: videoPath
|
|
859
|
+
}).strict(),
|
|
860
|
+
cta: z2.object({
|
|
861
|
+
title: text2(160).optional(),
|
|
862
|
+
line1: text2(160).optional(),
|
|
863
|
+
subtitle: text2(400).optional(),
|
|
864
|
+
line2: text2(400).optional()
|
|
865
|
+
}).strict()
|
|
866
|
+
};
|
|
867
|
+
var narrationTimingSchema = z2.object({
|
|
868
|
+
schemaVersion: z2.literal("1.0"),
|
|
869
|
+
source: z2.literal("segmented-synthesis"),
|
|
870
|
+
durationMs: z2.number().int().positive().max(PAIRED_LIMITS.totalDurationMs),
|
|
871
|
+
segments: z2.array(
|
|
872
|
+
z2.object({
|
|
873
|
+
sceneId: z2.string().min(1).max(80),
|
|
874
|
+
startMs: z2.number().int().nonnegative(),
|
|
875
|
+
endMs: z2.number().int().positive(),
|
|
876
|
+
durationMs: z2.number().int().positive()
|
|
877
|
+
}).strict()
|
|
878
|
+
).min(1).max(PAIRED_LIMITS.scenes)
|
|
879
|
+
}).strict();
|
|
880
|
+
var sceneSchema = z2.object({
|
|
881
|
+
id: z2.string().min(1).max(80),
|
|
882
|
+
kind: z2.string(),
|
|
883
|
+
content: z2.record(z2.unknown()),
|
|
884
|
+
timing: z2.object({
|
|
885
|
+
durationMs: z2.number().int().positive().max(PAIRED_LIMITS.sceneDurationMs).optional()
|
|
886
|
+
}).strict().optional()
|
|
887
|
+
}).strict();
|
|
888
|
+
var specShape = z2.object({
|
|
889
|
+
schemaVersion: z2.literal("1.0"),
|
|
890
|
+
id: z2.string().regex(/^[a-z0-9][a-z0-9-]{2,80}$/),
|
|
891
|
+
templateFamily: z2.enum(PAIRED_TEMPLATE_FAMILIES),
|
|
892
|
+
meta: z2.object({
|
|
893
|
+
title: z2.string().min(1).max(160),
|
|
894
|
+
description: z2.string().max(500).optional(),
|
|
895
|
+
tags: z2.array(z2.string().max(40)).max(20).default([])
|
|
896
|
+
}).strict(),
|
|
897
|
+
target: z2.object({
|
|
898
|
+
aspect: z2.enum(["9:16", "4:5", "1:1"]),
|
|
899
|
+
fps: z2.number().int().positive().default(30),
|
|
900
|
+
durationMode: z2.enum(["auto", "fixed"]).default("auto"),
|
|
901
|
+
fixedDurationSec: z2.number().positive().max(PAIRED_LIMITS.totalDurationMs / 1e3).optional()
|
|
902
|
+
}).strict(),
|
|
903
|
+
style: z2.object({
|
|
904
|
+
theme: z2.enum(["default", "editorial-light", "editorial-dark", "clinical"]).default("default"),
|
|
905
|
+
variant: z2.string().default("default"),
|
|
906
|
+
safeAreaProfile: z2.enum(["baseline", "metaSafe"]).default("baseline"),
|
|
907
|
+
showSceneLabels: z2.boolean().default(false)
|
|
908
|
+
}).strict().default({}),
|
|
909
|
+
audio: z2.object({
|
|
910
|
+
mode: z2.enum(["auto", "narration", "music", "silent"]).default("auto"),
|
|
911
|
+
narrationText: z2.string().max(1e4).optional(),
|
|
912
|
+
narrationSrc: audioPath.optional(),
|
|
913
|
+
narrationTiming: narrationTimingSchema.optional(),
|
|
914
|
+
narrationVolume: z2.number().min(0).max(1).default(1),
|
|
915
|
+
musicSrc: audioPath.optional(),
|
|
916
|
+
musicVolume: z2.number().min(0).max(1).default(0.16),
|
|
917
|
+
ducking: z2.boolean().default(true)
|
|
918
|
+
}).strict().default({}),
|
|
919
|
+
assets: z2.object({
|
|
920
|
+
logoSrc: z2.string().optional(),
|
|
921
|
+
posterSrc: z2.string().optional()
|
|
922
|
+
}).strict().default({}),
|
|
923
|
+
scenes: z2.array(sceneSchema).min(1).max(PAIRED_LIMITS.scenes)
|
|
924
|
+
}).strict();
|
|
925
|
+
var DIMENSIONS = {
|
|
926
|
+
"9:16": { width: 1080, height: 1920 },
|
|
927
|
+
"4:5": { width: 1080, height: 1350 },
|
|
928
|
+
"1:1": { width: 1080, height: 1080 }
|
|
929
|
+
};
|
|
930
|
+
function pairedTimeline(spec) {
|
|
931
|
+
const fps = spec.target.fps;
|
|
932
|
+
const sceneMs = (scene) => spec.audio.narrationTiming?.segments.find(
|
|
933
|
+
(segment) => segment.sceneId === scene.id
|
|
934
|
+
)?.durationMs ?? scene.timing?.durationMs ?? 3e3;
|
|
935
|
+
const totalMs = spec.scenes.reduce((sum, scene) => sum + sceneMs(scene), 0);
|
|
936
|
+
const durationInFrames = spec.target.durationMode === "fixed" && spec.target.fixedDurationSec ? Math.max(1, Math.round(spec.target.fixedDurationSec * fps)) : Math.max(1, Math.round(totalMs / 1e3 * fps));
|
|
937
|
+
let cursor = 0;
|
|
938
|
+
const scenes = spec.scenes.map((scene, index) => {
|
|
939
|
+
const frames = Math.max(1, Math.round(sceneMs(scene) / 1e3 * fps));
|
|
940
|
+
const startFrame = cursor;
|
|
941
|
+
cursor += frames;
|
|
942
|
+
return {
|
|
943
|
+
index,
|
|
944
|
+
id: scene.id,
|
|
945
|
+
kind: scene.kind,
|
|
946
|
+
startFrame,
|
|
947
|
+
durationInFrames: frames,
|
|
948
|
+
visibleFrames: Math.max(
|
|
949
|
+
0,
|
|
950
|
+
Math.min(frames, durationInFrames - startFrame)
|
|
951
|
+
)
|
|
952
|
+
};
|
|
953
|
+
});
|
|
954
|
+
return { fps, ...DIMENSIONS[spec.target.aspect], durationInFrames, scenes };
|
|
955
|
+
}
|
|
956
|
+
function stringValue(value) {
|
|
957
|
+
return typeof value === "string" ? value.trim() : "";
|
|
958
|
+
}
|
|
959
|
+
function qualityReview(spec) {
|
|
960
|
+
const errors = [];
|
|
961
|
+
const notes = [];
|
|
962
|
+
spec.scenes.forEach((scene, index) => {
|
|
963
|
+
if (scene.kind === "custom")
|
|
964
|
+
notes.push({
|
|
965
|
+
level: "warning",
|
|
966
|
+
code: "custom-renders-as-statement",
|
|
967
|
+
message: `Scene ${index + 1} is custom. Both frameworks draw it as a statement card; no custom code runs.`
|
|
968
|
+
});
|
|
969
|
+
});
|
|
970
|
+
const audio = spec.audio;
|
|
971
|
+
const audible = audio.mode === "narration" || audio.mode === "music" || audio.mode === "auto" && Boolean(audio.narrationSrc || audio.musicSrc);
|
|
972
|
+
if (audio.mode === "narration" && !audio.narrationSrc)
|
|
973
|
+
errors.push({
|
|
974
|
+
path: "audio.narrationSrc",
|
|
975
|
+
code: "narration-source-missing",
|
|
976
|
+
message: "audio.mode is narration, but audio.narrationSrc is missing. Add an existing audio file."
|
|
977
|
+
});
|
|
978
|
+
if (audio.mode === "music" && !audio.musicSrc)
|
|
979
|
+
errors.push({
|
|
980
|
+
path: "audio.musicSrc",
|
|
981
|
+
code: "music-source-missing",
|
|
982
|
+
message: "audio.mode is music, but audio.musicSrc is missing."
|
|
983
|
+
});
|
|
984
|
+
if (audio.narrationText && !audio.narrationSrc) {
|
|
985
|
+
if (audio.mode === "silent")
|
|
986
|
+
notes.push({
|
|
987
|
+
level: "warning",
|
|
988
|
+
code: "narration-text-not-rendered",
|
|
989
|
+
message: "Narration text exists without narrationSrc. Written narration is not audible until audio is generated or attached."
|
|
990
|
+
});
|
|
991
|
+
else
|
|
992
|
+
errors.push({
|
|
993
|
+
path: "audio.narrationText",
|
|
994
|
+
code: "narration-text-not-rendered",
|
|
995
|
+
message: "Narration text exists without narrationSrc. Written narration is not audible until audio is generated or attached."
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
if (audio.narrationTiming) {
|
|
999
|
+
const ids = spec.scenes.map((scene) => scene.id);
|
|
1000
|
+
const timed = audio.narrationTiming.segments.map((item) => item.sceneId);
|
|
1001
|
+
if (ids.length !== timed.length || ids.some((id2, index) => timed[index] !== id2))
|
|
1002
|
+
errors.push({
|
|
1003
|
+
path: "audio.narrationTiming.segments",
|
|
1004
|
+
code: "narration-timing-scene-mismatch",
|
|
1005
|
+
message: "Narration timing must contain exactly one ordered segment for every scene."
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
if (!audible)
|
|
1009
|
+
notes.push({
|
|
1010
|
+
level: "info",
|
|
1011
|
+
code: "intentional-silence",
|
|
1012
|
+
message: "No audible audio is configured. Set audio.mode to silent only when this is intentional."
|
|
1013
|
+
});
|
|
1014
|
+
if (spec.style.showSceneLabels)
|
|
1015
|
+
notes.push({
|
|
1016
|
+
level: "warning",
|
|
1017
|
+
code: "debug-labels-visible",
|
|
1018
|
+
message: "Scene-kind labels are visible. Keep showSceneLabels false for audience-facing renders."
|
|
1019
|
+
});
|
|
1020
|
+
spec.scenes.forEach((scene, index) => {
|
|
1021
|
+
const title2 = stringValue(scene.content.title) || stringValue(scene.content.line1);
|
|
1022
|
+
const label2 = stringValue(scene.content.label);
|
|
1023
|
+
const at = `scenes.${index}`;
|
|
1024
|
+
if (!title2)
|
|
1025
|
+
errors.push({
|
|
1026
|
+
path: `${at}.content.title`,
|
|
1027
|
+
code: "scene-title-missing",
|
|
1028
|
+
message: `Scene ${index + 1} (${scene.id}) has no title or line1.`
|
|
1029
|
+
});
|
|
1030
|
+
if (title2.length > 82)
|
|
1031
|
+
notes.push({
|
|
1032
|
+
level: "warning",
|
|
1033
|
+
code: "headline-too-long",
|
|
1034
|
+
message: `Scene ${index + 1} headline has ${title2.length} characters. Prefer 82 or fewer.`
|
|
1035
|
+
});
|
|
1036
|
+
if (title2.length >= 12 && title2 === title2.toUpperCase())
|
|
1037
|
+
notes.push({
|
|
1038
|
+
level: "warning",
|
|
1039
|
+
code: "all-caps-headline",
|
|
1040
|
+
message: `Scene ${index + 1} uses an all-caps headline. Use sentence case for editorial hierarchy.`
|
|
1041
|
+
});
|
|
1042
|
+
if (label2 && label2 === label2.toUpperCase())
|
|
1043
|
+
notes.push({
|
|
1044
|
+
level: "warning",
|
|
1045
|
+
code: "all-caps-label",
|
|
1046
|
+
message: `Scene ${index + 1} uses an all-caps label. Remove it or use sentence case only when it adds meaning.`
|
|
1047
|
+
});
|
|
1048
|
+
if (label2 && ["hero", "custom", "points", "cta"].includes(label2.toLowerCase()))
|
|
1049
|
+
errors.push({
|
|
1050
|
+
path: `${at}.content.label`,
|
|
1051
|
+
code: "implementation-label",
|
|
1052
|
+
message: `Scene ${index + 1} exposes the implementation label "${label2}". Remove it from audience-facing copy.`
|
|
1053
|
+
});
|
|
1054
|
+
const points = Array.isArray(scene.content.points) ? scene.content.points : [];
|
|
1055
|
+
if (points.length > 4)
|
|
1056
|
+
notes.push({
|
|
1057
|
+
level: "warning",
|
|
1058
|
+
code: "too-many-points",
|
|
1059
|
+
message: `Scene ${index + 1} has ${points.length} points. Prefer four or fewer.`
|
|
1060
|
+
});
|
|
1061
|
+
if (points.some((point2) => point2.length > 58))
|
|
1062
|
+
notes.push({
|
|
1063
|
+
level: "warning",
|
|
1064
|
+
code: "point-too-long",
|
|
1065
|
+
message: `Scene ${index + 1} contains a point longer than 58 characters.`
|
|
1066
|
+
});
|
|
1067
|
+
});
|
|
1068
|
+
const kinds = new Set(spec.scenes.map((scene) => scene.kind));
|
|
1069
|
+
if (spec.scenes.length >= 4 && kinds.size < 3)
|
|
1070
|
+
notes.push({
|
|
1071
|
+
level: "warning",
|
|
1072
|
+
code: "weak-scene-differentiation",
|
|
1073
|
+
message: "Four or more scenes use fewer than three layout kinds. Add visual structure instead of repeating text slides."
|
|
1074
|
+
});
|
|
1075
|
+
return { errors, notes };
|
|
1076
|
+
}
|
|
1077
|
+
function zodIssues(error, prefix = "") {
|
|
1078
|
+
return error.issues.map((issue) => ({
|
|
1079
|
+
path: [prefix, ...issue.path.map(String)].filter(Boolean).join("."),
|
|
1080
|
+
code: issue.code === "unrecognized_keys" ? "unsupported-field" : "invalid-videospec",
|
|
1081
|
+
message: issue.code === "unrecognized_keys" ? `${issue.message}. Neither paired renderer reads these fields; remove them rather than expecting them to render.` : issue.message
|
|
1082
|
+
}));
|
|
1083
|
+
}
|
|
1084
|
+
function validatePairedVideoSpec(raw) {
|
|
1085
|
+
const parsed = specShape.safeParse(raw);
|
|
1086
|
+
if (!parsed.success) return { ok: false, issues: zodIssues(parsed.error) };
|
|
1087
|
+
const shape2 = parsed.data;
|
|
1088
|
+
const issues = [];
|
|
1089
|
+
const reject = (path, code, message) => issues.push({ path, code, message });
|
|
1090
|
+
if (shape2.target.fps > PAIRED_FPS.max)
|
|
1091
|
+
reject(
|
|
1092
|
+
"target.fps",
|
|
1093
|
+
"unsupported-fps",
|
|
1094
|
+
`Paired templates render at whole frame rates from ${PAIRED_FPS.min} to ${PAIRED_FPS.max} fps, the range the HyperFrames renderer accepts.`
|
|
1095
|
+
);
|
|
1096
|
+
if (shape2.target.durationMode === "fixed" && !shape2.target.fixedDurationSec)
|
|
1097
|
+
reject(
|
|
1098
|
+
"target.fixedDurationSec",
|
|
1099
|
+
"fixed-duration-missing",
|
|
1100
|
+
"durationMode fixed needs fixedDurationSec; the public renderer would otherwise fall back to automatic duration."
|
|
1101
|
+
);
|
|
1102
|
+
if (shape2.target.durationMode === "auto" && shape2.target.fixedDurationSec)
|
|
1103
|
+
reject(
|
|
1104
|
+
"target.fixedDurationSec",
|
|
1105
|
+
"fixed-duration-unused",
|
|
1106
|
+
"fixedDurationSec is ignored unless durationMode is fixed. Remove it or set durationMode to fixed."
|
|
1107
|
+
);
|
|
1108
|
+
if (shape2.style.variant !== "default")
|
|
1109
|
+
reject(
|
|
1110
|
+
"style.variant",
|
|
1111
|
+
"unsupported-style-variant",
|
|
1112
|
+
"Only the default variant exists. The public renderer ignores other variant values."
|
|
1113
|
+
);
|
|
1114
|
+
if (shape2.style.safeAreaProfile !== "baseline")
|
|
1115
|
+
reject(
|
|
1116
|
+
"style.safeAreaProfile",
|
|
1117
|
+
"unsupported-safe-area",
|
|
1118
|
+
"Neither renderer applies a metaSafe layout; it renders exactly like baseline. Set baseline so the document does not promise safe-area handling."
|
|
1119
|
+
);
|
|
1120
|
+
for (const key of ["logoSrc", "posterSrc"])
|
|
1121
|
+
if (shape2.assets[key] !== void 0)
|
|
1122
|
+
reject(
|
|
1123
|
+
`assets.${key}`,
|
|
1124
|
+
"unsupported-asset",
|
|
1125
|
+
`assets.${key} is not rendered by either framework. Remove it.`
|
|
1126
|
+
);
|
|
1127
|
+
const narrationTiming = shape2.audio.narrationTiming;
|
|
1128
|
+
if (narrationTiming) {
|
|
1129
|
+
let previousEnd = 0;
|
|
1130
|
+
const timedIds = /* @__PURE__ */ new Set();
|
|
1131
|
+
narrationTiming.segments.forEach((segment, index) => {
|
|
1132
|
+
const at = `audio.narrationTiming.segments.${index}`;
|
|
1133
|
+
if (timedIds.has(segment.sceneId))
|
|
1134
|
+
reject(
|
|
1135
|
+
`${at}.sceneId`,
|
|
1136
|
+
"invalid-videospec",
|
|
1137
|
+
"Narration timing scene IDs must be unique."
|
|
1138
|
+
);
|
|
1139
|
+
timedIds.add(segment.sceneId);
|
|
1140
|
+
if (segment.startMs !== previousEnd)
|
|
1141
|
+
reject(
|
|
1142
|
+
`${at}.startMs`,
|
|
1143
|
+
"invalid-videospec",
|
|
1144
|
+
"Narration timing segments must be contiguous."
|
|
1145
|
+
);
|
|
1146
|
+
if (segment.durationMs !== segment.endMs - segment.startMs)
|
|
1147
|
+
reject(
|
|
1148
|
+
`${at}.durationMs`,
|
|
1149
|
+
"invalid-videospec",
|
|
1150
|
+
"Narration timing duration must equal endMs minus startMs."
|
|
1151
|
+
);
|
|
1152
|
+
previousEnd = segment.endMs;
|
|
1153
|
+
});
|
|
1154
|
+
if (previousEnd !== narrationTiming.durationMs)
|
|
1155
|
+
reject(
|
|
1156
|
+
"audio.narrationTiming.durationMs",
|
|
1157
|
+
"invalid-videospec",
|
|
1158
|
+
"Narration timing duration must end with the final segment."
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
if (shape2.audio.mode === "silent" && (shape2.audio.narrationSrc || shape2.audio.musicSrc))
|
|
1162
|
+
reject(
|
|
1163
|
+
"audio.mode",
|
|
1164
|
+
"audio-mode-conflict",
|
|
1165
|
+
"audio.mode is silent, but an audio source is set and would still play. Remove the source or change the mode."
|
|
1166
|
+
);
|
|
1167
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1168
|
+
const scenes = [];
|
|
1169
|
+
shape2.scenes.forEach((scene, index) => {
|
|
1170
|
+
const at = `scenes.${index}`;
|
|
1171
|
+
if (ids.has(scene.id))
|
|
1172
|
+
reject(`${at}.id`, "duplicate-scene-id", "Scene IDs must be unique.");
|
|
1173
|
+
ids.add(scene.id);
|
|
1174
|
+
if (!PAIRED_SCENE_KINDS.includes(scene.kind)) {
|
|
1175
|
+
reject(
|
|
1176
|
+
`${at}.kind`,
|
|
1177
|
+
"invalid-videospec",
|
|
1178
|
+
`Unknown scene kind ${JSON.stringify(scene.kind)}.`
|
|
1179
|
+
);
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
const kind = scene.kind;
|
|
1183
|
+
const content = contentSchemas[kind].safeParse(scene.content);
|
|
1184
|
+
if (!content.success) {
|
|
1185
|
+
issues.push(...zodIssues(content.error, `${at}.content`));
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
const value = content.data;
|
|
1189
|
+
for (const [first, second] of [
|
|
1190
|
+
["title", "line1"],
|
|
1191
|
+
["subtitle", "line2"]
|
|
1192
|
+
])
|
|
1193
|
+
if (value[first] !== void 0 && value[second] !== void 0)
|
|
1194
|
+
reject(
|
|
1195
|
+
`${at}.content.${second}`,
|
|
1196
|
+
"ambiguous-copy-field",
|
|
1197
|
+
`Supply ${first} or ${second}, not both; the renderer shows only ${first}.`
|
|
1198
|
+
);
|
|
1199
|
+
if (kind === "equation") {
|
|
1200
|
+
const blocks = value.latexBlocks;
|
|
1201
|
+
if (new Set(blocks).size !== blocks.length)
|
|
1202
|
+
reject(
|
|
1203
|
+
`${at}.content.latexBlocks`,
|
|
1204
|
+
"duplicate-equation",
|
|
1205
|
+
"Equation blocks must be unique."
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
scenes.push({ ...scene, kind, content: value });
|
|
1209
|
+
});
|
|
1210
|
+
if (issues.length) return { ok: false, issues };
|
|
1211
|
+
const spec = { ...shape2, scenes };
|
|
1212
|
+
const quality = qualityReview(spec);
|
|
1213
|
+
issues.push(...quality.errors);
|
|
1214
|
+
const timeline = pairedTimeline(spec);
|
|
1215
|
+
const sceneFrames = timeline.scenes.reduce(
|
|
1216
|
+
(sum, scene) => sum + scene.durationInFrames,
|
|
1217
|
+
0
|
|
1218
|
+
);
|
|
1219
|
+
if (timeline.durationInFrames / timeline.fps > PAIRED_LIMITS.totalDurationMs / 1e3)
|
|
1220
|
+
reject(
|
|
1221
|
+
"scenes",
|
|
1222
|
+
"duration-too-long",
|
|
1223
|
+
"Paired templates are limited to 20 minutes."
|
|
1224
|
+
);
|
|
1225
|
+
if (spec.target.durationMode === "fixed" && timeline.durationInFrames < sceneFrames)
|
|
1226
|
+
reject(
|
|
1227
|
+
"target.fixedDurationSec",
|
|
1228
|
+
"fixed-duration-truncates",
|
|
1229
|
+
`fixedDurationSec gives ${timeline.durationInFrames} frames but the scenes need ${sceneFrames}; later scenes would be cut off.`
|
|
1230
|
+
);
|
|
1231
|
+
const hidden = timeline.scenes.find((scene) => scene.visibleFrames === 0);
|
|
1232
|
+
if (hidden)
|
|
1233
|
+
reject(
|
|
1234
|
+
`scenes.${hidden.index}.timing`,
|
|
1235
|
+
"scene-not-visible",
|
|
1236
|
+
"Frame rounding leaves this scene no visible frames. Lengthen the scene durations."
|
|
1237
|
+
);
|
|
1238
|
+
if (issues.length) return { ok: false, issues };
|
|
1239
|
+
return { ok: true, spec, timeline, qualityNotes: quality.notes };
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// src/paired-templates/catalogue.ts
|
|
1243
|
+
var FRAMEWORKS = ["remotion", "hyperframes"];
|
|
1244
|
+
var videoSpecContract = {
|
|
1245
|
+
id: PAIRED_VIDEOSPEC_CONTRACT,
|
|
1246
|
+
validate(raw) {
|
|
1247
|
+
const result3 = validatePairedVideoSpec(raw);
|
|
1248
|
+
if (!result3.ok) return result3;
|
|
1249
|
+
const { spec, timeline, qualityNotes } = result3;
|
|
1250
|
+
return {
|
|
1251
|
+
ok: true,
|
|
1252
|
+
document: spec,
|
|
1253
|
+
timeline,
|
|
1254
|
+
qualityNotes,
|
|
1255
|
+
realise: (framework3) => framework3 === "remotion" ? {
|
|
1256
|
+
files: remotionVideoSpecFiles(spec, timeline),
|
|
1257
|
+
commands: remotionVideoSpecCommands(spec),
|
|
1258
|
+
runtime: { ...REMOTION_TEMPLATES }
|
|
1259
|
+
} : {
|
|
1260
|
+
files: hyperframesVideoSpecFiles(spec, timeline),
|
|
1261
|
+
commands: [
|
|
1262
|
+
"npm install",
|
|
1263
|
+
"npm run setup",
|
|
1264
|
+
"npm run check",
|
|
1265
|
+
"npm run preview",
|
|
1266
|
+
"npm run render"
|
|
1267
|
+
],
|
|
1268
|
+
runtime: { ...HYPERFRAMES_RUNTIME }
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
};
|
|
1273
|
+
var familyNames = {
|
|
1274
|
+
"qa-ad": "Question and answer ad",
|
|
1275
|
+
"proof-walkthrough": "Proof walkthrough",
|
|
1276
|
+
"announcement-brief": "Announcement brief",
|
|
1277
|
+
"social-remix": "Social remix",
|
|
1278
|
+
"finance-brief": "Data and finance brief"
|
|
1279
|
+
};
|
|
1280
|
+
var PAIRED_CONTRACTS = {
|
|
1281
|
+
[videoSpecContract.id]: videoSpecContract
|
|
1282
|
+
};
|
|
1283
|
+
var PAIRED_FAMILIES = PAIRED_TEMPLATE_FAMILIES.map((id2) => ({
|
|
1284
|
+
id: id2,
|
|
1285
|
+
name: familyNames[id2],
|
|
1286
|
+
contract: PAIRED_VIDEOSPEC_CONTRACT,
|
|
1287
|
+
frameworks: FRAMEWORKS
|
|
1288
|
+
}));
|
|
1289
|
+
function contractForFamily(templateFamily) {
|
|
1290
|
+
const family = PAIRED_FAMILIES.find((item) => item.id === templateFamily);
|
|
1291
|
+
return family ? PAIRED_CONTRACTS[family.contract] : void 0;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// src/paired-templates/prepare-video-template.ts
|
|
1295
|
+
function pairedTemplatesEnabled(env = process.env) {
|
|
1296
|
+
return env.INSTAVAR_PAIRED_TEMPLATES !== "0";
|
|
1297
|
+
}
|
|
1298
|
+
var PAIRED_TEMPLATE_CAPABILITY = {
|
|
1299
|
+
tool: "prepare_video_template",
|
|
1300
|
+
contract: PAIRED_VIDEOSPEC_CONTRACT,
|
|
1301
|
+
frameworks: FRAMEWORKS,
|
|
1302
|
+
families: PAIRED_FAMILIES.map((family) => family.id),
|
|
1303
|
+
fps: PAIRED_FPS,
|
|
1304
|
+
aspects: ["9:16", "4:5", "1:1"],
|
|
1305
|
+
realisations: {
|
|
1306
|
+
remotion: `${REMOTION_TEMPLATES.repository} at ${REMOTION_TEMPLATES.commit}`,
|
|
1307
|
+
hyperframes: `hyperframes ${HYPERFRAMES_RUNTIME.hyperframes} source generated from the same VideoSpec`
|
|
1308
|
+
},
|
|
1309
|
+
scope: "One VideoSpec document, realised in either framework. Returns local source and commands; does not save, upload or render. Fields neither renderer honours are rejected, not dropped."
|
|
1310
|
+
};
|
|
1311
|
+
var PAIRED_TEMPLATE_INSTRUCTIONS = "For a VideoSpec template in Remotion or HyperFrames, use prepare_video_template with the VideoSpec. Never infer a framework from creative wording; omit framework unless the user chose one or said no preference, and ask the user on needs-selection. Continue with the returned project: edit project.spec, then call again to regenerate or to switch frameworks. A rejected field is unsupported in both frameworks; fix it rather than dropping it. The tool returns local source and commands; it does not render, save or upload.";
|
|
1312
|
+
var framework2 = z3.enum(["remotion", "hyperframes"]);
|
|
1313
|
+
var prepareVideoTemplateInput = {
|
|
1314
|
+
framework: z3.enum(["remotion", "hyperframes", "no-preference"]).optional().describe(
|
|
1315
|
+
"Only supply an explicit user choice. Omit for an ambiguous request or to inherit the existing project framework."
|
|
1316
|
+
),
|
|
1317
|
+
spec: z3.record(z3.unknown()).optional().describe("A VideoSpec 1.0 document for a new project."),
|
|
1318
|
+
project: z3.object({
|
|
1319
|
+
contract: z3.literal(PAIRED_VIDEOSPEC_CONTRACT),
|
|
1320
|
+
framework: framework2,
|
|
1321
|
+
spec: z3.record(z3.unknown())
|
|
1322
|
+
}).strict().optional().describe(
|
|
1323
|
+
"The latest project returned by this tool, with any edits made to project.spec. Caller-held, not a saved hosted project."
|
|
1324
|
+
)
|
|
1325
|
+
};
|
|
1326
|
+
var inputSchema2 = z3.object(prepareVideoTemplateInput).strict();
|
|
1327
|
+
function prepareVideoTemplate(raw) {
|
|
1328
|
+
const input = inputSchema2.parse(raw);
|
|
1329
|
+
if (input.spec && input.project)
|
|
1330
|
+
throw new Error(
|
|
1331
|
+
"Supply spec for a new project or project to continue one, not both."
|
|
1332
|
+
);
|
|
1333
|
+
const spec = input.project?.spec ?? input.spec;
|
|
1334
|
+
if (!spec)
|
|
1335
|
+
return {
|
|
1336
|
+
isError: false,
|
|
1337
|
+
result: {
|
|
1338
|
+
status: "needs-spec",
|
|
1339
|
+
message: "Supply a VideoSpec 1.0 document. Use scaffold_video_spec for a starting point."
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
const contract = contractForFamily(spec.templateFamily);
|
|
1343
|
+
if (!contract)
|
|
1344
|
+
return {
|
|
1345
|
+
isError: true,
|
|
1346
|
+
result: {
|
|
1347
|
+
status: "rejected",
|
|
1348
|
+
issues: [
|
|
1349
|
+
{
|
|
1350
|
+
path: "templateFamily",
|
|
1351
|
+
code: "unsupported-family",
|
|
1352
|
+
message: `Paired templates cover ${PAIRED_FAMILIES.map(
|
|
1353
|
+
(family) => family.id
|
|
1354
|
+
).join(", ")}.`
|
|
1355
|
+
}
|
|
1356
|
+
]
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
const validated = contract.validate(spec);
|
|
1360
|
+
if (!validated.ok)
|
|
1361
|
+
return {
|
|
1362
|
+
isError: true,
|
|
1363
|
+
result: {
|
|
1364
|
+
status: "rejected",
|
|
1365
|
+
contract: contract.id,
|
|
1366
|
+
issues: validated.issues,
|
|
1367
|
+
message: "No source was generated. These fields are unsupported in both frameworks; change them and call again."
|
|
1368
|
+
}
|
|
1369
|
+
};
|
|
1370
|
+
const selected = input.framework === "remotion" || input.framework === "hyperframes" ? input.framework : input.project?.framework ?? (input.framework === "no-preference" ? "remotion" : void 0);
|
|
1371
|
+
if (!selected)
|
|
1372
|
+
return {
|
|
1373
|
+
isError: false,
|
|
1374
|
+
result: {
|
|
1375
|
+
status: "needs-selection",
|
|
1376
|
+
options: ["remotion", "hyperframes", "no-preference"],
|
|
1377
|
+
message: "Which framework would you like: Remotion, HyperFrames, or no preference? No source was generated."
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
const realisation = validated.realise(selected);
|
|
1381
|
+
return {
|
|
1382
|
+
isError: false,
|
|
1383
|
+
result: {
|
|
1384
|
+
status: "ready",
|
|
1385
|
+
reason: input.framework && input.framework !== "no-preference" ? "explicit" : input.project ? "existing-project" : "visible-default",
|
|
1386
|
+
project: {
|
|
1387
|
+
contract: contract.id,
|
|
1388
|
+
framework: selected,
|
|
1389
|
+
spec: validated.document
|
|
1390
|
+
},
|
|
1391
|
+
timeline: validated.timeline,
|
|
1392
|
+
qualityNotes: validated.qualityNotes,
|
|
1393
|
+
runtime: realisation.runtime,
|
|
1394
|
+
files: realisation.files,
|
|
1395
|
+
commands: realisation.commands,
|
|
1396
|
+
instructions: "Save these files in a new directory and place each media file at the public/ path the spec names. The same preflight checks media for both frameworks. Equation blocks are shown as source text, not typeset. Keep project.spec as the content source of truth: regenerating or switching frameworks keeps only what the spec holds, not edits made to generated files. This tool has not written files, saved a hosted project or rendered video."
|
|
1397
|
+
}
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
function registerPairedTemplates(server) {
|
|
1401
|
+
server.registerTool(
|
|
1402
|
+
"prepare_video_template",
|
|
1403
|
+
{
|
|
1404
|
+
title: "Prepare a template video in Remotion or HyperFrames",
|
|
1405
|
+
description: "Validate one VideoSpec against the paired template contract and return local source for the chosen framework, or switch an existing project's framework. " + PAIRED_TEMPLATE_INSTRUCTIONS,
|
|
1406
|
+
inputSchema: prepareVideoTemplateInput,
|
|
1407
|
+
annotations: {
|
|
1408
|
+
readOnlyHint: true,
|
|
1409
|
+
destructiveHint: false,
|
|
1410
|
+
openWorldHint: false
|
|
1411
|
+
},
|
|
1412
|
+
_meta: { securitySchemes: [{ type: "noauth" }] }
|
|
1413
|
+
},
|
|
1414
|
+
async (args) => {
|
|
1415
|
+
try {
|
|
1416
|
+
const { isError, result: result3 } = prepareVideoTemplate(args);
|
|
1417
|
+
return {
|
|
1418
|
+
...isError ? { isError: true } : {},
|
|
1419
|
+
structuredContent: result3,
|
|
1420
|
+
content: [{ type: "text", text: JSON.stringify(result3) }]
|
|
1421
|
+
};
|
|
1422
|
+
} catch (error) {
|
|
1423
|
+
return {
|
|
1424
|
+
isError: true,
|
|
1425
|
+
content: [
|
|
1426
|
+
{
|
|
1427
|
+
type: "text",
|
|
1428
|
+
text: error instanceof Error ? error.message : "Invalid template input"
|
|
1429
|
+
}
|
|
1430
|
+
]
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
275
1437
|
// src/index.ts
|
|
276
1438
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
277
1439
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
278
1440
|
|
|
279
1441
|
// src/creative-guides.ts
|
|
280
|
-
import { z as
|
|
1442
|
+
import { z as z4 } from "zod";
|
|
281
1443
|
|
|
282
1444
|
// src/creative-guides.generated.ts
|
|
283
1445
|
var CREATIVE_GUIDES = [
|
|
@@ -308,16 +1470,16 @@ var CREATIVE_GUIDES = [
|
|
|
308
1470
|
];
|
|
309
1471
|
|
|
310
1472
|
// src/creative-guides.ts
|
|
311
|
-
var creativeGuideIdSchema =
|
|
1473
|
+
var creativeGuideIdSchema = z4.enum([
|
|
312
1474
|
"visual-copy",
|
|
313
1475
|
"carousel-stories",
|
|
314
1476
|
"visual-design",
|
|
315
1477
|
"human-review"
|
|
316
1478
|
]);
|
|
317
1479
|
var CREATIVE_GUIDE_INDEX = CREATIVE_GUIDES.map(
|
|
318
|
-
({ id: id2, title, uri }) => ({
|
|
1480
|
+
({ id: id2, title: title2, uri }) => ({
|
|
319
1481
|
id: id2,
|
|
320
|
-
title,
|
|
1482
|
+
title: title2,
|
|
321
1483
|
uri
|
|
322
1484
|
})
|
|
323
1485
|
);
|
|
@@ -338,83 +1500,513 @@ function registerCreativeGuides(server) {
|
|
|
338
1500
|
})
|
|
339
1501
|
);
|
|
340
1502
|
}
|
|
341
|
-
server.registerTool(
|
|
342
|
-
"get_creative_guide",
|
|
343
|
-
{
|
|
344
|
-
title: "Get a visual communication guide",
|
|
345
|
-
description: "Read practical guidance before drafting concise plain or poetic copy, choosing carousel stories, designing visuals or recording human feedback. Returns guidance for the calling assistant to apply, not generated copy or saved feedback. Omit guideId to list guides.",
|
|
346
|
-
inputSchema: {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
1503
|
+
server.registerTool(
|
|
1504
|
+
"get_creative_guide",
|
|
1505
|
+
{
|
|
1506
|
+
title: "Get a visual communication guide",
|
|
1507
|
+
description: "Read practical guidance before drafting concise plain or poetic copy, choosing carousel stories, designing visuals or recording human feedback. Returns guidance for the calling assistant to apply, not generated copy or saved feedback. Omit guideId to list guides.",
|
|
1508
|
+
inputSchema: {
|
|
1509
|
+
guideId: creativeGuideIdSchema.optional().describe("Guide to read. Omit to list the guides.")
|
|
1510
|
+
},
|
|
1511
|
+
annotations: {
|
|
1512
|
+
readOnlyHint: true,
|
|
1513
|
+
destructiveHint: false,
|
|
1514
|
+
openWorldHint: false
|
|
1515
|
+
},
|
|
1516
|
+
_meta: { securitySchemes: [{ type: "noauth" }] }
|
|
1517
|
+
},
|
|
1518
|
+
async ({ guideId }) => {
|
|
1519
|
+
const guide = CREATIVE_GUIDES.find((entry) => entry.id === guideId);
|
|
1520
|
+
return {
|
|
1521
|
+
structuredContent: guide ? { guide } : { guides: CREATIVE_GUIDE_INDEX },
|
|
1522
|
+
content: [
|
|
1523
|
+
{
|
|
1524
|
+
type: "text",
|
|
1525
|
+
text: guide ? guide.text : CREATIVE_GUIDE_INDEX.map(
|
|
1526
|
+
(entry) => `${entry.id}: ${entry.title} (${entry.uri})`
|
|
1527
|
+
).join("\n")
|
|
1528
|
+
}
|
|
1529
|
+
]
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// src/public-catalogue.ts
|
|
1536
|
+
import { z as z5 } from "zod";
|
|
1537
|
+
|
|
1538
|
+
// src/carousel/archetypes.ts
|
|
1539
|
+
function frame(slides, content, visualStyle = "editorial") {
|
|
1540
|
+
return {
|
|
1541
|
+
slides: slides.map((slide, index) => ({
|
|
1542
|
+
...slide,
|
|
1543
|
+
image: content.images?.[index],
|
|
1544
|
+
visualRequest: content.visualRequests?.[index]
|
|
1545
|
+
})),
|
|
1546
|
+
handle: content.handle,
|
|
1547
|
+
showPageNumber: true,
|
|
1548
|
+
visualStyle
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
var listicle = {
|
|
1552
|
+
id: "listicle",
|
|
1553
|
+
label: "Listicle",
|
|
1554
|
+
blurb: "Hook, a numbered rundown on one slide, then the payoff.",
|
|
1555
|
+
build: (c) => frame(
|
|
1556
|
+
[
|
|
1557
|
+
{
|
|
1558
|
+
kind: "cover",
|
|
1559
|
+
title: c.hook,
|
|
1560
|
+
accent: c.accent,
|
|
1561
|
+
subtitle: c.subtitle
|
|
1562
|
+
},
|
|
1563
|
+
{
|
|
1564
|
+
kind: "list",
|
|
1565
|
+
title: c.listTitle ?? "The rundown",
|
|
1566
|
+
items: c.points
|
|
1567
|
+
},
|
|
1568
|
+
{ kind: "takeaway", title: c.payoff, accent: c.accent, cta: c.cta }
|
|
1569
|
+
],
|
|
1570
|
+
c
|
|
1571
|
+
)
|
|
1572
|
+
};
|
|
1573
|
+
var manifesto = {
|
|
1574
|
+
id: "manifesto",
|
|
1575
|
+
label: "Manifesto",
|
|
1576
|
+
blurb: "Hook, then one punchy idea per slide, building to the payoff.",
|
|
1577
|
+
build: (c) => frame(
|
|
1578
|
+
[
|
|
1579
|
+
{
|
|
1580
|
+
kind: "cover",
|
|
1581
|
+
title: c.hook,
|
|
1582
|
+
accent: c.accent,
|
|
1583
|
+
subtitle: c.subtitle
|
|
1584
|
+
},
|
|
1585
|
+
...c.points.map(
|
|
1586
|
+
(p) => ({
|
|
1587
|
+
kind: "statement",
|
|
1588
|
+
statement: p
|
|
1589
|
+
})
|
|
1590
|
+
),
|
|
1591
|
+
{ kind: "takeaway", title: c.payoff, cta: c.cta }
|
|
1592
|
+
],
|
|
1593
|
+
c
|
|
1594
|
+
)
|
|
1595
|
+
};
|
|
1596
|
+
var beforeAfter = {
|
|
1597
|
+
id: "before-after",
|
|
1598
|
+
label: "Before / After",
|
|
1599
|
+
blurb: "Hook, the problem, the contrast, then the payoff.",
|
|
1600
|
+
build: (c) => {
|
|
1601
|
+
const problem = c.points[0] ?? "";
|
|
1602
|
+
const contrast = c.points[c.points.length - 1] ?? problem;
|
|
1603
|
+
return frame(
|
|
1604
|
+
[
|
|
1605
|
+
{
|
|
1606
|
+
kind: "cover",
|
|
1607
|
+
title: c.hook,
|
|
1608
|
+
accent: c.accent,
|
|
1609
|
+
subtitle: c.subtitle
|
|
1610
|
+
},
|
|
1611
|
+
{ kind: "statement", statement: problem, footnote: "The status quo" },
|
|
1612
|
+
{ kind: "statement", statement: contrast, footnote: "The shift" },
|
|
1613
|
+
{ kind: "takeaway", title: c.payoff, accent: c.accent, cta: c.cta }
|
|
1614
|
+
],
|
|
1615
|
+
c
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1619
|
+
var valueFirstReveal = {
|
|
1620
|
+
id: "value-first-reveal",
|
|
1621
|
+
label: "Value-First Reveal",
|
|
1622
|
+
blurb: "Three useful beats first, then proof and a restrained product reveal.",
|
|
1623
|
+
build: (c) => {
|
|
1624
|
+
const beats = [0, 1, 2].map(
|
|
1625
|
+
(index) => c.points[index] ?? c.points.at(-1) ?? c.payoff
|
|
1626
|
+
);
|
|
1627
|
+
const bridge = c.bridge ?? "Here is what makes this repeatable.";
|
|
1628
|
+
const proof = c.productProof ?? c.payoff;
|
|
1629
|
+
return frame(
|
|
1630
|
+
[
|
|
1631
|
+
{
|
|
1632
|
+
kind: "cover",
|
|
1633
|
+
title: c.hook,
|
|
1634
|
+
accent: c.accent,
|
|
1635
|
+
subtitle: c.subtitle
|
|
1636
|
+
},
|
|
1637
|
+
{ kind: "statement", statement: beats[0], footnote: "01 \xB7 The value" },
|
|
1638
|
+
{ kind: "statement", statement: beats[1], footnote: "02 \xB7 The value" },
|
|
1639
|
+
{ kind: "statement", statement: beats[2], footnote: "03 \xB7 The value" },
|
|
1640
|
+
{
|
|
1641
|
+
kind: "statement",
|
|
1642
|
+
statement: bridge,
|
|
1643
|
+
footnote: `Proof \xB7 ${proof}`
|
|
1644
|
+
},
|
|
1645
|
+
{
|
|
1646
|
+
kind: "takeaway",
|
|
1647
|
+
title: c.productReveal ?? c.payoff,
|
|
1648
|
+
cta: [c.cta, c.disclosure].filter(Boolean).join(" \xB7 ") || void 0
|
|
1649
|
+
}
|
|
1650
|
+
],
|
|
1651
|
+
c
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
};
|
|
1655
|
+
var CAROUSEL_ARCHETYPES = [
|
|
1656
|
+
listicle,
|
|
1657
|
+
manifesto,
|
|
1658
|
+
beforeAfter,
|
|
1659
|
+
valueFirstReveal
|
|
1660
|
+
];
|
|
1661
|
+
function getArchetype(id2) {
|
|
1662
|
+
return CAROUSEL_ARCHETYPES.find((a) => a.id === id2);
|
|
1663
|
+
}
|
|
1664
|
+
var DEFAULT_CAROUSEL_CONTENT = {
|
|
1665
|
+
hook: "Just say the word.",
|
|
1666
|
+
accent: "the word",
|
|
1667
|
+
subtitle: "How one sentence becomes a finished video.",
|
|
1668
|
+
listTitle: "What you skip",
|
|
1669
|
+
points: [
|
|
1670
|
+
"Storyboarding every shot by hand",
|
|
1671
|
+
"Re-recording voiceover for one wording change",
|
|
1672
|
+
"Fighting a timeline editor for an hour"
|
|
1673
|
+
],
|
|
1674
|
+
payoff: "Describe it. We render it.",
|
|
1675
|
+
bridge: "The workflow gets easier when one brief drives every production step.",
|
|
1676
|
+
productProof: "One approved brief, one repeatable render pipeline.",
|
|
1677
|
+
productReveal: "Instavar turns the brief into the first draft.",
|
|
1678
|
+
images: [
|
|
1679
|
+
"2025_video_hooks_hero.v20250811.avif",
|
|
1680
|
+
"ai_content_ops_system.v20250811.avif",
|
|
1681
|
+
"short_form_funnel_blueprint.v20250811.avif",
|
|
1682
|
+
"server_side_tracking_playbook.v20250811.avif",
|
|
1683
|
+
"retention_loops_post_purchase_funnel.v20250811.avif",
|
|
1684
|
+
"true_roas_incrementality_first.v20250811.avif"
|
|
1685
|
+
].map((file, index) => ({
|
|
1686
|
+
src: `https://thumbnails.instavar.com/${file}`,
|
|
1687
|
+
alt: `Instavar AI-generated carousel sample image ${index + 1}`,
|
|
1688
|
+
provenance: "ai-generated"
|
|
1689
|
+
})),
|
|
1690
|
+
cta: "Start at instavar.com. Your first draft is one sentence away.",
|
|
1691
|
+
handle: "@instavar"
|
|
1692
|
+
};
|
|
1693
|
+
|
|
1694
|
+
// src/carousel/aspect-contract.ts
|
|
1695
|
+
var CAROUSEL_ASPECTS = ["4:5", "3:4", "1:1", "9:16"];
|
|
1696
|
+
function isCarouselAspect(value) {
|
|
1697
|
+
return CAROUSEL_ASPECTS.some((aspect) => aspect === value);
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// src/carousel/deck-contract.ts
|
|
1701
|
+
var IMAGE_PROVENANCE = /* @__PURE__ */ new Set([
|
|
1702
|
+
"user-owned",
|
|
1703
|
+
"licensed-stock",
|
|
1704
|
+
"ai-generated"
|
|
1705
|
+
]);
|
|
1706
|
+
var VISUAL_KINDS = /* @__PURE__ */ new Set([
|
|
1707
|
+
"photography",
|
|
1708
|
+
"illustration",
|
|
1709
|
+
"diagram",
|
|
1710
|
+
"background",
|
|
1711
|
+
"visual-metaphor"
|
|
1712
|
+
]);
|
|
1713
|
+
var EVIDENCE_LABELS = /* @__PURE__ */ new Set([
|
|
1714
|
+
"AI-generated visual",
|
|
1715
|
+
"Concept illustration"
|
|
1716
|
+
]);
|
|
1717
|
+
var INPUT_IMAGE_PROVENANCE = /* @__PURE__ */ new Set(["user-owned", "licensed-stock"]);
|
|
1718
|
+
var CAROUSEL_DECK_LIMITS = {
|
|
1719
|
+
slides: 12,
|
|
1720
|
+
handle: 40,
|
|
1721
|
+
imageSrc: 2048,
|
|
1722
|
+
imageAlt: 240,
|
|
1723
|
+
visualRequest: { id: 120, prompt: 1200, direction: 500, inputs: 4 },
|
|
1724
|
+
cover: { title: 100, accent: 100, subtitle: 160 },
|
|
1725
|
+
statement: { statement: 220, accent: 220, footnote: 100 },
|
|
1726
|
+
list: { title: 80, accent: 80, items: 5, item: 120 },
|
|
1727
|
+
takeaway: { title: 120, accent: 120, cta: 140 }
|
|
1728
|
+
};
|
|
1729
|
+
function isNonEmptyString(value) {
|
|
1730
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
1731
|
+
}
|
|
1732
|
+
function textLength(value) {
|
|
1733
|
+
return Array.from(value.trim()).length;
|
|
1734
|
+
}
|
|
1735
|
+
function validateText(value, path, maxLength, issues, options = {}) {
|
|
1736
|
+
if (value === void 0 && options.optional) return;
|
|
1737
|
+
if (!isNonEmptyString(value)) {
|
|
1738
|
+
issues.push(`${path} is required`);
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
if (textLength(value) > maxLength) {
|
|
1742
|
+
issues.push(`${path} must be at most ${maxLength} characters`);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
function validateOptionalImage(value, path, issues) {
|
|
1746
|
+
if (value === void 0) return;
|
|
1747
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1748
|
+
issues.push(`${path} must be an object`);
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
const image = value;
|
|
1752
|
+
validateText(image.src, `${path}.src`, CAROUSEL_DECK_LIMITS.imageSrc, issues);
|
|
1753
|
+
validateText(image.alt, `${path}.alt`, CAROUSEL_DECK_LIMITS.imageAlt, issues);
|
|
1754
|
+
if (!isNonEmptyString(image.provenance) || !IMAGE_PROVENANCE.has(image.provenance)) {
|
|
1755
|
+
issues.push(`${path}.provenance is invalid`);
|
|
1756
|
+
}
|
|
1757
|
+
if (image.generation !== void 0) {
|
|
1758
|
+
const generation = image.generation;
|
|
1759
|
+
if (!generation || typeof generation !== "object" || Array.isArray(generation)) {
|
|
1760
|
+
issues.push(`${path}.generation must be an object`);
|
|
1761
|
+
} else {
|
|
1762
|
+
const record2 = generation;
|
|
1763
|
+
for (const field of ["provider", "model", "generatedAt", "promptHash"])
|
|
1764
|
+
validateText(record2[field], `${path}.generation.${field}`, 200, issues);
|
|
1765
|
+
if (isNonEmptyString(record2.generatedAt) && Number.isNaN(Date.parse(record2.generatedAt))) {
|
|
1766
|
+
issues.push(`${path}.generation.generatedAt must be an ISO timestamp`);
|
|
1767
|
+
}
|
|
1768
|
+
validateText(
|
|
1769
|
+
record2.outputHash,
|
|
1770
|
+
`${path}.generation.outputHash`,
|
|
1771
|
+
200,
|
|
1772
|
+
issues,
|
|
1773
|
+
{
|
|
1774
|
+
optional: true
|
|
1775
|
+
}
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
function validateOptionalVisualRequest(value, path, issues) {
|
|
1781
|
+
if (value === void 0) return;
|
|
1782
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1783
|
+
issues.push(`${path} must be an object`);
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
const request = value;
|
|
1787
|
+
validateText(
|
|
1788
|
+
request.id,
|
|
1789
|
+
`${path}.id`,
|
|
1790
|
+
CAROUSEL_DECK_LIMITS.visualRequest.id,
|
|
1791
|
+
issues
|
|
1792
|
+
);
|
|
1793
|
+
validateText(
|
|
1794
|
+
request.prompt,
|
|
1795
|
+
`${path}.prompt`,
|
|
1796
|
+
CAROUSEL_DECK_LIMITS.visualRequest.prompt,
|
|
1797
|
+
issues
|
|
1798
|
+
);
|
|
1799
|
+
validateText(
|
|
1800
|
+
request.direction,
|
|
1801
|
+
`${path}.direction`,
|
|
1802
|
+
CAROUSEL_DECK_LIMITS.visualRequest.direction,
|
|
1803
|
+
issues
|
|
1804
|
+
);
|
|
1805
|
+
validateText(
|
|
1806
|
+
request.alt,
|
|
1807
|
+
`${path}.alt`,
|
|
1808
|
+
CAROUSEL_DECK_LIMITS.imageAlt,
|
|
1809
|
+
issues
|
|
1810
|
+
);
|
|
1811
|
+
if (!isNonEmptyString(request.kind) || !VISUAL_KINDS.has(request.kind)) {
|
|
1812
|
+
issues.push(`${path}.kind is invalid`);
|
|
1813
|
+
}
|
|
1814
|
+
if (!isNonEmptyString(request.evidenceLabel) || !EVIDENCE_LABELS.has(request.evidenceLabel)) {
|
|
1815
|
+
issues.push(`${path}.evidenceLabel is invalid`);
|
|
1816
|
+
}
|
|
1817
|
+
if (request.inputImages !== void 0) {
|
|
1818
|
+
if (!Array.isArray(request.inputImages)) {
|
|
1819
|
+
issues.push(`${path}.inputImages must be an array`);
|
|
1820
|
+
} else {
|
|
1821
|
+
if (request.inputImages.length > CAROUSEL_DECK_LIMITS.visualRequest.inputs) {
|
|
1822
|
+
issues.push(
|
|
1823
|
+
`${path}.inputImages must contain at most ${CAROUSEL_DECK_LIMITS.visualRequest.inputs} items`
|
|
1824
|
+
);
|
|
1825
|
+
}
|
|
1826
|
+
request.inputImages.forEach((candidate, index) => {
|
|
1827
|
+
const inputPath = `${path}.inputImages[${index}]`;
|
|
1828
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
1829
|
+
issues.push(`${inputPath} must be an object`);
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
const input = candidate;
|
|
1833
|
+
validateText(
|
|
1834
|
+
input.src,
|
|
1835
|
+
`${inputPath}.src`,
|
|
1836
|
+
CAROUSEL_DECK_LIMITS.imageSrc,
|
|
1837
|
+
issues
|
|
1838
|
+
);
|
|
1839
|
+
if (!isNonEmptyString(input.provenance) || !INPUT_IMAGE_PROVENANCE.has(input.provenance)) {
|
|
1840
|
+
issues.push(`${inputPath}.provenance is invalid`);
|
|
1841
|
+
}
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
function validateCarouselDeck(value) {
|
|
1847
|
+
const issues = [];
|
|
1848
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1849
|
+
return { ok: false, issues: ["deck must be an object"] };
|
|
1850
|
+
}
|
|
1851
|
+
const deck = value;
|
|
1852
|
+
if (deck.aspect !== void 0 && !isCarouselAspect(deck.aspect)) {
|
|
1853
|
+
issues.push("deck.aspect is invalid");
|
|
1854
|
+
}
|
|
1855
|
+
if (!Array.isArray(deck.slides) || deck.slides.length === 0) {
|
|
1856
|
+
issues.push("deck.slides must contain at least one slide");
|
|
1857
|
+
return { ok: false, issues };
|
|
1858
|
+
}
|
|
1859
|
+
if (deck.slides.length > CAROUSEL_DECK_LIMITS.slides) {
|
|
1860
|
+
issues.push(
|
|
1861
|
+
`deck.slides must contain at most ${CAROUSEL_DECK_LIMITS.slides} slides`
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1864
|
+
validateText(
|
|
1865
|
+
deck.handle,
|
|
1866
|
+
"deck.handle",
|
|
1867
|
+
CAROUSEL_DECK_LIMITS.handle,
|
|
1868
|
+
issues,
|
|
1869
|
+
{ optional: true }
|
|
1870
|
+
);
|
|
1871
|
+
if (deck.showPageNumber !== void 0 && typeof deck.showPageNumber !== "boolean") {
|
|
1872
|
+
issues.push("deck.showPageNumber must be a boolean");
|
|
1873
|
+
}
|
|
1874
|
+
if (deck.visualStyle !== void 0 && deck.visualStyle !== "editorial" && deck.visualStyle !== "native-photo") {
|
|
1875
|
+
issues.push("deck.visualStyle is invalid");
|
|
1876
|
+
}
|
|
1877
|
+
const visualRequestIds = /* @__PURE__ */ new Set();
|
|
1878
|
+
deck.slides.forEach((value2, index) => {
|
|
1879
|
+
const path = `deck.slides[${index}]`;
|
|
1880
|
+
if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) {
|
|
1881
|
+
issues.push(`${path} must be an object`);
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
const slide = value2;
|
|
1885
|
+
switch (slide.kind) {
|
|
1886
|
+
case "cover":
|
|
1887
|
+
validateText(
|
|
1888
|
+
slide.title,
|
|
1889
|
+
`${path}.title`,
|
|
1890
|
+
CAROUSEL_DECK_LIMITS.cover.title,
|
|
1891
|
+
issues
|
|
1892
|
+
);
|
|
1893
|
+
validateText(
|
|
1894
|
+
slide.accent,
|
|
1895
|
+
`${path}.accent`,
|
|
1896
|
+
CAROUSEL_DECK_LIMITS.cover.accent,
|
|
1897
|
+
issues,
|
|
1898
|
+
{ optional: true }
|
|
1899
|
+
);
|
|
1900
|
+
validateText(
|
|
1901
|
+
slide.subtitle,
|
|
1902
|
+
`${path}.subtitle`,
|
|
1903
|
+
CAROUSEL_DECK_LIMITS.cover.subtitle,
|
|
1904
|
+
issues,
|
|
1905
|
+
{ optional: true }
|
|
1906
|
+
);
|
|
1907
|
+
break;
|
|
1908
|
+
case "statement":
|
|
1909
|
+
validateText(
|
|
1910
|
+
slide.statement,
|
|
1911
|
+
`${path}.statement`,
|
|
1912
|
+
CAROUSEL_DECK_LIMITS.statement.statement,
|
|
1913
|
+
issues
|
|
1914
|
+
);
|
|
1915
|
+
validateText(
|
|
1916
|
+
slide.accent,
|
|
1917
|
+
`${path}.accent`,
|
|
1918
|
+
CAROUSEL_DECK_LIMITS.statement.accent,
|
|
1919
|
+
issues,
|
|
1920
|
+
{ optional: true }
|
|
1921
|
+
);
|
|
1922
|
+
validateText(
|
|
1923
|
+
slide.footnote,
|
|
1924
|
+
`${path}.footnote`,
|
|
1925
|
+
CAROUSEL_DECK_LIMITS.statement.footnote,
|
|
1926
|
+
issues,
|
|
1927
|
+
{ optional: true }
|
|
1928
|
+
);
|
|
1929
|
+
break;
|
|
1930
|
+
case "list":
|
|
1931
|
+
validateText(
|
|
1932
|
+
slide.title,
|
|
1933
|
+
`${path}.title`,
|
|
1934
|
+
CAROUSEL_DECK_LIMITS.list.title,
|
|
1935
|
+
issues
|
|
1936
|
+
);
|
|
1937
|
+
validateText(
|
|
1938
|
+
slide.accent,
|
|
1939
|
+
`${path}.accent`,
|
|
1940
|
+
CAROUSEL_DECK_LIMITS.list.accent,
|
|
1941
|
+
issues,
|
|
1942
|
+
{ optional: true }
|
|
1943
|
+
);
|
|
1944
|
+
if (!Array.isArray(slide.items) || slide.items.length === 0 || slide.items.some((item) => !isNonEmptyString(item))) {
|
|
1945
|
+
issues.push(`${path}.items must contain non-empty strings`);
|
|
1946
|
+
} else {
|
|
1947
|
+
if (slide.items.length > CAROUSEL_DECK_LIMITS.list.items) {
|
|
1948
|
+
issues.push(
|
|
1949
|
+
`${path}.items must contain at most ${CAROUSEL_DECK_LIMITS.list.items} items`
|
|
1950
|
+
);
|
|
364
1951
|
}
|
|
365
|
-
|
|
366
|
-
|
|
1952
|
+
slide.items.forEach(
|
|
1953
|
+
(item, itemIndex) => validateText(
|
|
1954
|
+
item,
|
|
1955
|
+
`${path}.items[${itemIndex}]`,
|
|
1956
|
+
CAROUSEL_DECK_LIMITS.list.item,
|
|
1957
|
+
issues
|
|
1958
|
+
)
|
|
1959
|
+
);
|
|
1960
|
+
}
|
|
1961
|
+
break;
|
|
1962
|
+
case "takeaway":
|
|
1963
|
+
validateText(
|
|
1964
|
+
slide.title,
|
|
1965
|
+
`${path}.title`,
|
|
1966
|
+
CAROUSEL_DECK_LIMITS.takeaway.title,
|
|
1967
|
+
issues
|
|
1968
|
+
);
|
|
1969
|
+
validateText(
|
|
1970
|
+
slide.accent,
|
|
1971
|
+
`${path}.accent`,
|
|
1972
|
+
CAROUSEL_DECK_LIMITS.takeaway.accent,
|
|
1973
|
+
issues,
|
|
1974
|
+
{ optional: true }
|
|
1975
|
+
);
|
|
1976
|
+
validateText(
|
|
1977
|
+
slide.cta,
|
|
1978
|
+
`${path}.cta`,
|
|
1979
|
+
CAROUSEL_DECK_LIMITS.takeaway.cta,
|
|
1980
|
+
issues,
|
|
1981
|
+
{ optional: true }
|
|
1982
|
+
);
|
|
1983
|
+
break;
|
|
1984
|
+
default:
|
|
1985
|
+
issues.push(`${path}.kind is invalid`);
|
|
367
1986
|
}
|
|
368
|
-
|
|
1987
|
+
validateOptionalImage(slide.image, `${path}.image`, issues);
|
|
1988
|
+
validateOptionalVisualRequest(
|
|
1989
|
+
slide.visualRequest,
|
|
1990
|
+
`${path}.visualRequest`,
|
|
1991
|
+
issues
|
|
1992
|
+
);
|
|
1993
|
+
if (slide.visualRequest && typeof slide.visualRequest === "object" && !Array.isArray(slide.visualRequest)) {
|
|
1994
|
+
const id2 = slide.visualRequest.id;
|
|
1995
|
+
if (isNonEmptyString(id2)) {
|
|
1996
|
+
if (visualRequestIds.has(id2)) {
|
|
1997
|
+
issues.push(
|
|
1998
|
+
`${path}.visualRequest.id must be unique within the deck`
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
visualRequestIds.add(id2);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
});
|
|
2005
|
+
return issues.length === 0 ? { ok: true, slideCount: deck.slides.length } : { ok: false, issues };
|
|
369
2006
|
}
|
|
370
2007
|
|
|
371
|
-
// src/
|
|
372
|
-
|
|
373
|
-
var TEMPLATE_REPOSITORY = "https://github.com/instavar/remotion-templates";
|
|
374
|
-
var TEMPLATE_ENDPOINT = "https://instavar.com/api/mcp/templates";
|
|
375
|
-
var VOICE_STUDIO_URL = "https://instavar.com/voice";
|
|
376
|
-
var templateIds = [
|
|
377
|
-
"qa-ad",
|
|
378
|
-
"proof-walkthrough",
|
|
379
|
-
"announcement-brief",
|
|
380
|
-
"social-remix",
|
|
381
|
-
"finance-brief"
|
|
382
|
-
];
|
|
383
|
-
var templateIdSchema = z3.enum(templateIds);
|
|
384
|
-
var aspectSchema = z3.enum(["9:16", "4:5", "1:1"]);
|
|
385
|
-
var templates = [
|
|
386
|
-
{
|
|
387
|
-
id: "qa-ad",
|
|
388
|
-
name: "Question and answer ad",
|
|
389
|
-
description: "A short video with a hook, source-video window, explanation points, and call to action.",
|
|
390
|
-
aspects: ["9:16", "4:5", "1:1"]
|
|
391
|
-
},
|
|
392
|
-
{
|
|
393
|
-
id: "proof-walkthrough",
|
|
394
|
-
name: "Proof walkthrough",
|
|
395
|
-
description: "A paced explanation for equations, worked reasoning, and a final takeaway.",
|
|
396
|
-
aspects: ["9:16", "4:5", "1:1"]
|
|
397
|
-
},
|
|
398
|
-
{
|
|
399
|
-
id: "announcement-brief",
|
|
400
|
-
name: "Announcement brief",
|
|
401
|
-
description: "A concise announcement with a headline, supporting points, optional media, and next action.",
|
|
402
|
-
aspects: ["9:16", "4:5", "1:1"]
|
|
403
|
-
},
|
|
404
|
-
{
|
|
405
|
-
id: "social-remix",
|
|
406
|
-
name: "Social remix",
|
|
407
|
-
description: "A caption-forward treatment that frames an existing clip with a hook, context, and call to action.",
|
|
408
|
-
aspects: ["9:16", "4:5", "1:1"]
|
|
409
|
-
},
|
|
410
|
-
{
|
|
411
|
-
id: "finance-brief",
|
|
412
|
-
name: "Data and finance brief",
|
|
413
|
-
description: "An editorial brief for comparisons, numeric highlights, evidence, caveats, and a recommendation.",
|
|
414
|
-
aspects: ["9:16", "4:5", "1:1"]
|
|
415
|
-
}
|
|
416
|
-
];
|
|
417
|
-
var videoSpecSchema = {
|
|
2008
|
+
// src/videospec-json-schema.ts
|
|
2009
|
+
var VIDEO_SPEC_JSON_SCHEMA = {
|
|
418
2010
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
419
2011
|
$id: "https://instavar.com/schemas/video-spec-1.0.json",
|
|
420
2012
|
title: "Instavar VideoSpec",
|
|
@@ -431,52 +2023,94 @@ var videoSpecSchema = {
|
|
|
431
2023
|
properties: {
|
|
432
2024
|
schemaVersion: { const: "1.0" },
|
|
433
2025
|
id: { type: "string", pattern: "^[a-z0-9][a-z0-9-]{2,80}$" },
|
|
434
|
-
templateFamily: {
|
|
2026
|
+
templateFamily: {
|
|
2027
|
+
enum: [
|
|
2028
|
+
"qa-ad",
|
|
2029
|
+
"proof-walkthrough",
|
|
2030
|
+
"announcement-brief",
|
|
2031
|
+
"social-remix",
|
|
2032
|
+
"finance-brief"
|
|
2033
|
+
]
|
|
2034
|
+
},
|
|
435
2035
|
meta: {
|
|
436
2036
|
type: "object",
|
|
2037
|
+
additionalProperties: false,
|
|
437
2038
|
required: ["title"],
|
|
438
2039
|
properties: {
|
|
439
2040
|
title: { type: "string", minLength: 1 },
|
|
440
2041
|
description: { type: "string" },
|
|
441
|
-
tags: { type: "array", items: { type: "string" } }
|
|
2042
|
+
tags: { type: "array", items: { type: "string" }, default: [] }
|
|
442
2043
|
}
|
|
443
2044
|
},
|
|
444
2045
|
target: {
|
|
445
2046
|
type: "object",
|
|
446
|
-
|
|
2047
|
+
additionalProperties: false,
|
|
2048
|
+
required: ["aspect"],
|
|
447
2049
|
properties: {
|
|
448
2050
|
aspect: { enum: ["9:16", "4:5", "1:1"] },
|
|
449
|
-
fps: { type: "integer", minimum: 1 },
|
|
450
|
-
durationMode: { enum: ["auto", "fixed"] },
|
|
2051
|
+
fps: { type: "integer", minimum: 1, default: 30 },
|
|
2052
|
+
durationMode: { enum: ["auto", "fixed"], default: "auto" },
|
|
451
2053
|
fixedDurationSec: { type: "number", exclusiveMinimum: 0 }
|
|
452
2054
|
}
|
|
453
2055
|
},
|
|
2056
|
+
style: {
|
|
2057
|
+
type: "object",
|
|
2058
|
+
additionalProperties: false,
|
|
2059
|
+
properties: {
|
|
2060
|
+
theme: {
|
|
2061
|
+
enum: ["default", "editorial-light", "editorial-dark", "clinical"],
|
|
2062
|
+
default: "default"
|
|
2063
|
+
},
|
|
2064
|
+
variant: { type: "string", default: "default" },
|
|
2065
|
+
safeAreaProfile: {
|
|
2066
|
+
enum: ["baseline", "metaSafe"],
|
|
2067
|
+
default: "baseline"
|
|
2068
|
+
},
|
|
2069
|
+
showSceneLabels: { type: "boolean", default: false }
|
|
2070
|
+
}
|
|
2071
|
+
},
|
|
454
2072
|
audio: {
|
|
455
2073
|
type: "object",
|
|
2074
|
+
additionalProperties: false,
|
|
456
2075
|
properties: {
|
|
457
|
-
mode: {
|
|
2076
|
+
mode: {
|
|
2077
|
+
enum: ["auto", "narration", "music", "silent"],
|
|
2078
|
+
default: "auto"
|
|
2079
|
+
},
|
|
458
2080
|
narrationText: { type: "string" },
|
|
459
2081
|
narrationSrc: { type: "string" },
|
|
460
|
-
narrationVolume: {
|
|
2082
|
+
narrationVolume: {
|
|
2083
|
+
type: "number",
|
|
2084
|
+
minimum: 0,
|
|
2085
|
+
maximum: 1,
|
|
2086
|
+
default: 1
|
|
2087
|
+
},
|
|
461
2088
|
musicSrc: { type: "string" },
|
|
462
|
-
musicVolume: {
|
|
463
|
-
|
|
2089
|
+
musicVolume: {
|
|
2090
|
+
type: "number",
|
|
2091
|
+
minimum: 0,
|
|
2092
|
+
maximum: 1,
|
|
2093
|
+
default: 0.16
|
|
2094
|
+
},
|
|
2095
|
+
ducking: { type: "boolean", default: true },
|
|
464
2096
|
narrationTiming: {
|
|
465
2097
|
type: "object",
|
|
2098
|
+
additionalProperties: false,
|
|
466
2099
|
required: ["schemaVersion", "source", "durationMs", "segments"],
|
|
467
2100
|
properties: {
|
|
468
2101
|
schemaVersion: { const: "1.0" },
|
|
469
2102
|
source: { const: "segmented-synthesis" },
|
|
470
|
-
durationMs: { type: "integer", minimum: 1 },
|
|
2103
|
+
durationMs: { type: "integer", minimum: 1, maximum: 12e5 },
|
|
471
2104
|
segments: {
|
|
472
2105
|
type: "array",
|
|
473
2106
|
minItems: 1,
|
|
474
2107
|
maxItems: 24,
|
|
475
2108
|
items: {
|
|
476
2109
|
type: "object",
|
|
2110
|
+
additionalProperties: false,
|
|
477
2111
|
required: ["sceneId", "startMs", "endMs", "durationMs"],
|
|
478
2112
|
properties: {
|
|
479
|
-
sceneId: { type: "string", minLength: 1 },
|
|
2113
|
+
sceneId: { type: "string", minLength: 1, maxLength: 80 },
|
|
480
2114
|
startMs: { type: "integer", minimum: 0 },
|
|
481
2115
|
endMs: { type: "integer", minimum: 1 },
|
|
482
2116
|
durationMs: { type: "integer", minimum: 1 }
|
|
@@ -487,11 +2121,20 @@ var videoSpecSchema = {
|
|
|
487
2121
|
}
|
|
488
2122
|
}
|
|
489
2123
|
},
|
|
2124
|
+
assets: {
|
|
2125
|
+
type: "object",
|
|
2126
|
+
additionalProperties: false,
|
|
2127
|
+
properties: {
|
|
2128
|
+
logoSrc: { type: "string" },
|
|
2129
|
+
posterSrc: { type: "string" }
|
|
2130
|
+
}
|
|
2131
|
+
},
|
|
490
2132
|
scenes: {
|
|
491
2133
|
type: "array",
|
|
492
2134
|
minItems: 1,
|
|
493
2135
|
items: {
|
|
494
2136
|
type: "object",
|
|
2137
|
+
additionalProperties: false,
|
|
495
2138
|
required: ["id", "kind", "content"],
|
|
496
2139
|
properties: {
|
|
497
2140
|
id: { type: "string", minLength: 1 },
|
|
@@ -508,27 +2151,136 @@ var videoSpecSchema = {
|
|
|
508
2151
|
]
|
|
509
2152
|
},
|
|
510
2153
|
content: { type: "object" },
|
|
511
|
-
timing: {
|
|
2154
|
+
timing: {
|
|
2155
|
+
type: "object",
|
|
2156
|
+
additionalProperties: false,
|
|
2157
|
+
properties: {
|
|
2158
|
+
durationMs: { type: "integer", exclusiveMinimum: 0 }
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
512
2161
|
}
|
|
513
2162
|
}
|
|
514
|
-
},
|
|
515
|
-
style: {
|
|
516
|
-
type: "object",
|
|
517
|
-
properties: {
|
|
518
|
-
theme: {
|
|
519
|
-
enum: ["default", "editorial-light", "editorial-dark", "clinical"]
|
|
520
|
-
},
|
|
521
|
-
variant: { type: "string" },
|
|
522
|
-
safeAreaProfile: { enum: ["baseline", "metaSafe"] },
|
|
523
|
-
showSceneLabels: { type: "boolean", default: false }
|
|
524
|
-
}
|
|
525
2163
|
}
|
|
526
2164
|
}
|
|
527
2165
|
};
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
2166
|
+
|
|
2167
|
+
// src/public-catalogue.ts
|
|
2168
|
+
var TEMPLATE_REPOSITORY = "https://github.com/instavar/remotion-templates";
|
|
2169
|
+
var VOICE_STUDIO_URL = "https://instavar.com/voice";
|
|
2170
|
+
var PUBLIC_VIDEO_TEMPLATES = [
|
|
2171
|
+
{
|
|
2172
|
+
id: "qa-ad",
|
|
2173
|
+
name: "Question and answer ad",
|
|
2174
|
+
description: "Start with a question, then answer it with a clip and clear points.",
|
|
2175
|
+
aspects: ["9:16", "4:5", "1:1"],
|
|
2176
|
+
requiredSceneKinds: ["hero", "video-window", "cta"],
|
|
2177
|
+
optionalSceneKinds: ["points"],
|
|
2178
|
+
exampleId: "qa-ad-starter"
|
|
2179
|
+
},
|
|
2180
|
+
{
|
|
2181
|
+
id: "proof-walkthrough",
|
|
2182
|
+
name: "Proof walkthrough",
|
|
2183
|
+
description: "Explain a calculation or claim step by step, then end with the takeaway.",
|
|
2184
|
+
aspects: ["9:16", "4:5", "1:1"],
|
|
2185
|
+
requiredSceneKinds: ["hero", "equation"],
|
|
2186
|
+
optionalSceneKinds: ["points", "cta"],
|
|
2187
|
+
exampleId: "proof-walkthrough-starter"
|
|
2188
|
+
},
|
|
2189
|
+
{
|
|
2190
|
+
id: "announcement-brief",
|
|
2191
|
+
name: "Announcement brief",
|
|
2192
|
+
description: "Announce something new with a headline, proof, and next step.",
|
|
2193
|
+
aspects: ["9:16", "4:5", "1:1"],
|
|
2194
|
+
requiredSceneKinds: ["hero", "points"],
|
|
2195
|
+
optionalSceneKinds: ["statement", "mechanism", "video-window", "cta"],
|
|
2196
|
+
exampleId: "announcement-brief-starter"
|
|
2197
|
+
},
|
|
2198
|
+
{
|
|
2199
|
+
id: "social-remix",
|
|
2200
|
+
name: "Social remix",
|
|
2201
|
+
description: "Turn an existing clip into a caption-led social post.",
|
|
2202
|
+
aspects: ["9:16", "4:5", "1:1"],
|
|
2203
|
+
requiredSceneKinds: ["hero", "video-window", "cta"],
|
|
2204
|
+
optionalSceneKinds: ["points"],
|
|
2205
|
+
exampleId: "social-remix-starter"
|
|
2206
|
+
},
|
|
2207
|
+
{
|
|
2208
|
+
id: "finance-brief",
|
|
2209
|
+
name: "Data and finance brief",
|
|
2210
|
+
description: "Compare key numbers, show the evidence, and give a recommendation.",
|
|
2211
|
+
aspects: ["9:16", "4:5", "1:1"],
|
|
2212
|
+
requiredSceneKinds: ["hero", "points"],
|
|
2213
|
+
optionalSceneKinds: ["equation", "cta"],
|
|
2214
|
+
exampleId: "finance-brief-starter"
|
|
2215
|
+
}
|
|
2216
|
+
];
|
|
2217
|
+
function findPublicTemplate(id2) {
|
|
2218
|
+
return PUBLIC_VIDEO_TEMPLATES.find((template) => template.id === id2);
|
|
2219
|
+
}
|
|
2220
|
+
var aspectSchema = z5.enum(["9:16", "4:5", "1:1"]);
|
|
2221
|
+
var storyboardBriefSchema = z5.string().min(8).max(600);
|
|
2222
|
+
var templateIdSchema = z5.enum([
|
|
2223
|
+
"qa-ad",
|
|
2224
|
+
"proof-walkthrough",
|
|
2225
|
+
"announcement-brief",
|
|
2226
|
+
"social-remix",
|
|
2227
|
+
"finance-brief"
|
|
2228
|
+
]);
|
|
2229
|
+
var acceptedDesignSchema = z5.object({
|
|
2230
|
+
style: z5.string().min(1).max(600),
|
|
2231
|
+
durationSeconds: z5.number().positive().max(300),
|
|
2232
|
+
audio: z5.enum(["silent", "narration", "music"]),
|
|
2233
|
+
illustration: z5.string().min(1).max(600),
|
|
2234
|
+
exclusions: z5.array(z5.string().min(1).max(200)).max(20),
|
|
2235
|
+
preview: z5.literal("dark-cafe").optional()
|
|
2236
|
+
});
|
|
2237
|
+
var carouselTemplateIdSchema = z5.enum([
|
|
2238
|
+
"listicle",
|
|
2239
|
+
"manifesto",
|
|
2240
|
+
"before-after",
|
|
2241
|
+
"value-first-reveal"
|
|
2242
|
+
]);
|
|
2243
|
+
var PUBLIC_CAROUSEL_DECK_JSON_SCHEMA = {
|
|
2244
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
2245
|
+
title: "Instavar CarouselDeck",
|
|
2246
|
+
type: "object",
|
|
2247
|
+
required: ["slides"],
|
|
2248
|
+
properties: {
|
|
2249
|
+
aspect: { enum: ["4:5", "1:1", "9:16"] },
|
|
2250
|
+
visualStyle: { enum: ["editorial", "native-photo"] },
|
|
2251
|
+
handle: { type: "string", maxLength: 40 },
|
|
2252
|
+
showPageNumber: { type: "boolean" },
|
|
2253
|
+
slides: {
|
|
2254
|
+
type: "array",
|
|
2255
|
+
minItems: 1,
|
|
2256
|
+
maxItems: 12,
|
|
2257
|
+
items: {
|
|
2258
|
+
type: "object",
|
|
2259
|
+
required: ["kind"],
|
|
2260
|
+
properties: {
|
|
2261
|
+
kind: { enum: ["cover", "statement", "list", "takeaway"] },
|
|
2262
|
+
title: { type: "string" },
|
|
2263
|
+
statement: { type: "string" },
|
|
2264
|
+
subtitle: { type: "string" },
|
|
2265
|
+
accent: { type: "string" },
|
|
2266
|
+
footnote: { type: "string" },
|
|
2267
|
+
cta: { type: "string" },
|
|
2268
|
+
items: { type: "array", items: { type: "string" }, maxItems: 5 },
|
|
2269
|
+
image: {
|
|
2270
|
+
type: "object",
|
|
2271
|
+
required: ["src", "alt", "provenance"],
|
|
2272
|
+
properties: {
|
|
2273
|
+
src: { type: "string" },
|
|
2274
|
+
alt: { type: "string" },
|
|
2275
|
+
provenance: {
|
|
2276
|
+
enum: ["user-owned", "licensed-stock", "ai-generated"]
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
532
2284
|
};
|
|
533
2285
|
function result(value, summary) {
|
|
534
2286
|
return {
|
|
@@ -540,14 +2292,18 @@ function slugify(value) {
|
|
|
540
2292
|
const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 72);
|
|
541
2293
|
return slug.length >= 3 ? slug : "new-video";
|
|
542
2294
|
}
|
|
543
|
-
function
|
|
2295
|
+
function withFrameworks(template, paired) {
|
|
2296
|
+
return paired ? { ...template, frameworks: ["remotion", "hyperframes"] } : template;
|
|
2297
|
+
}
|
|
2298
|
+
function scaffoldVideoSpec(args, options = {}) {
|
|
2299
|
+
const announcement = args.templateId === "announcement-brief";
|
|
544
2300
|
const scenes = [
|
|
545
2301
|
{
|
|
546
2302
|
id: "hero",
|
|
547
2303
|
kind: "hero",
|
|
548
2304
|
content: {
|
|
549
2305
|
title: args.title,
|
|
550
|
-
subtitle: "Replace with a concise promise"
|
|
2306
|
+
...announcement && args.body ? {} : { subtitle: args.body ?? "Replace with a concise promise" }
|
|
551
2307
|
},
|
|
552
2308
|
timing: { durationMs: 4e3 }
|
|
553
2309
|
}
|
|
@@ -563,7 +2319,14 @@ function scaffoldVideoSpec(args) {
|
|
|
563
2319
|
timing: { durationMs: 7e3 }
|
|
564
2320
|
});
|
|
565
2321
|
}
|
|
566
|
-
if (args.
|
|
2322
|
+
if (announcement && args.body) {
|
|
2323
|
+
scenes.push({
|
|
2324
|
+
id: "key-points",
|
|
2325
|
+
kind: "points",
|
|
2326
|
+
content: { title: args.body, points: [] },
|
|
2327
|
+
timing: { durationMs: 7e3 }
|
|
2328
|
+
});
|
|
2329
|
+
} else if (announcement) {
|
|
567
2330
|
scenes.push(
|
|
568
2331
|
{
|
|
569
2332
|
id: "context",
|
|
@@ -594,14 +2357,21 @@ function scaffoldVideoSpec(args) {
|
|
|
594
2357
|
timing: { durationMs: 7e3 }
|
|
595
2358
|
}
|
|
596
2359
|
);
|
|
597
|
-
} else {
|
|
2360
|
+
} else if (args.templateId === "proof-walkthrough") {
|
|
598
2361
|
scenes.push({
|
|
599
|
-
id:
|
|
600
|
-
kind:
|
|
601
|
-
content:
|
|
2362
|
+
id: "working",
|
|
2363
|
+
kind: "equation",
|
|
2364
|
+
content: {
|
|
602
2365
|
title: "Work through the idea",
|
|
603
2366
|
latexBlocks: ["x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}"]
|
|
604
|
-
}
|
|
2367
|
+
},
|
|
2368
|
+
timing: { durationMs: 8e3 }
|
|
2369
|
+
});
|
|
2370
|
+
} else {
|
|
2371
|
+
scenes.push({
|
|
2372
|
+
id: "key-points",
|
|
2373
|
+
kind: "points",
|
|
2374
|
+
content: args.body ? { title: args.body, points: [args.body] } : {
|
|
605
2375
|
title: "Key points",
|
|
606
2376
|
points: [
|
|
607
2377
|
"Replace with evidence",
|
|
@@ -615,217 +2385,655 @@ function scaffoldVideoSpec(args) {
|
|
|
615
2385
|
scenes.push({
|
|
616
2386
|
id: "cta",
|
|
617
2387
|
kind: "cta",
|
|
618
|
-
content: { line1: "Replace with the next action" },
|
|
619
|
-
timing: {
|
|
620
|
-
|
|
2388
|
+
content: { line1: args.cta ?? "Replace with the next action" },
|
|
2389
|
+
timing: { durationMs: announcement ? 6e3 : 5e3 }
|
|
2390
|
+
});
|
|
2391
|
+
return {
|
|
2392
|
+
schemaVersion: "1.0",
|
|
2393
|
+
id: slugify(args.title),
|
|
2394
|
+
templateFamily: args.templateId,
|
|
2395
|
+
meta: { title: args.title, tags: [] },
|
|
2396
|
+
target: { aspect: args.aspect, fps: 30, durationMode: "auto" },
|
|
2397
|
+
style: {
|
|
2398
|
+
theme: announcement ? "editorial-light" : "default",
|
|
2399
|
+
variant: "default",
|
|
2400
|
+
// Neither renderer applies metaSafe; the paired contract rejects it.
|
|
2401
|
+
safeAreaProfile: options.pairedTemplates ? "baseline" : "metaSafe",
|
|
2402
|
+
showSceneLabels: false
|
|
2403
|
+
},
|
|
2404
|
+
audio: {
|
|
2405
|
+
mode: "silent",
|
|
2406
|
+
narrationText: "",
|
|
2407
|
+
narrationVolume: 1,
|
|
2408
|
+
musicVolume: 0.16,
|
|
2409
|
+
ducking: true
|
|
2410
|
+
},
|
|
2411
|
+
assets: {},
|
|
2412
|
+
scenes
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
var HEALTH_OR_SAFETY_PATTERN = /\b(cpr|first aid|medical|medicine|medication|symptom|emergency|safety)\b/i;
|
|
2416
|
+
function titleFromBrief(brief) {
|
|
2417
|
+
const concise = brief.trim().replace(
|
|
2418
|
+
/^(?:please\s+)?(?:make|create|show|teach|explain)\s+(?:me\s+)?(?:a\s+)?(?:video\s+)?(?:about\s+)?/i,
|
|
2419
|
+
""
|
|
2420
|
+
).replace(/^tell me\s+/i, "").replace(/\s+/g, " ").replace(/[.?!]+$/, "");
|
|
2421
|
+
if (!concise) return "A clear video";
|
|
2422
|
+
return concise.charAt(0).toUpperCase() + concise.slice(1);
|
|
2423
|
+
}
|
|
2424
|
+
function recommendStoryboardStructure(brief) {
|
|
2425
|
+
const mediaIntent = brief.replace(
|
|
2426
|
+
/\b(?:no|without|avoid)\s+(?:(?:any|source|stock|automatic|supplied)\s+)?(?:clips?|footage|b-?roll)\b/gi,
|
|
2427
|
+
""
|
|
2428
|
+
);
|
|
2429
|
+
if (/\b(clip|footage|reel|remix|repurpose)\b/i.test(mediaIntent)) {
|
|
2430
|
+
return { templateId: "social-remix", kind: "source-remix" };
|
|
2431
|
+
}
|
|
2432
|
+
if (/\b(data|numbers?|metric|finance|financial|compare|comparison)\b/i.test(
|
|
2433
|
+
brief
|
|
2434
|
+
)) {
|
|
2435
|
+
return { templateId: "finance-brief", kind: "data-brief" };
|
|
2436
|
+
}
|
|
2437
|
+
if (/\b(announce(?:ment)?|launch|new|opening|release)\b/i.test(brief)) {
|
|
2438
|
+
return { templateId: "announcement-brief", kind: "announcement" };
|
|
2439
|
+
}
|
|
2440
|
+
if (/\b(how\s+to|how\s+do\s+(?:i|you|we)|steps?|step-by-step|recipe|cook|slice|chop|dice|peel|prepare|assemble|install|set\s+up|fold|tie)\b/i.test(
|
|
2441
|
+
brief
|
|
2442
|
+
)) {
|
|
2443
|
+
return { templateId: "qa-ad", kind: "procedure" };
|
|
2444
|
+
}
|
|
2445
|
+
return { templateId: "qa-ad", kind: "question-answer" };
|
|
2446
|
+
}
|
|
2447
|
+
function storyboardFrames(args) {
|
|
2448
|
+
if (args.kind === "procedure" && args.templateId === "qa-ad") {
|
|
2449
|
+
return [
|
|
2450
|
+
[
|
|
2451
|
+
"Prepare the setup",
|
|
2452
|
+
"Show the tools, ingredients, and safe starting position."
|
|
2453
|
+
],
|
|
2454
|
+
[
|
|
2455
|
+
"Show the key action",
|
|
2456
|
+
"Use a close, readable view of the movement the viewer must copy."
|
|
2457
|
+
],
|
|
2458
|
+
[
|
|
2459
|
+
"Show the result",
|
|
2460
|
+
"End on the finished outcome and the one safety point to remember."
|
|
2461
|
+
]
|
|
2462
|
+
].map(([title2, purpose], index) => ({
|
|
2463
|
+
id: `beat-${index + 1}`,
|
|
2464
|
+
title: title2,
|
|
2465
|
+
purpose,
|
|
2466
|
+
visualBrief: `${title2} for ${args.title}. Editorial composition, one clear subject, generous negative space, no words, numbers, logos, or interface elements.`,
|
|
2467
|
+
alt: `${title2}: a proposed visual beat for ${args.title}.`,
|
|
2468
|
+
asset: { kind: "image", state: "proposed" }
|
|
2469
|
+
}));
|
|
2470
|
+
}
|
|
2471
|
+
const frameCopy = {
|
|
2472
|
+
"qa-ad": [
|
|
2473
|
+
[
|
|
2474
|
+
"Open with the question",
|
|
2475
|
+
"Make the viewer recognise the problem or curiosity."
|
|
2476
|
+
],
|
|
2477
|
+
["Show the answer", "Show the one idea that resolves it."],
|
|
2478
|
+
["Leave one next step", "End with the takeaway the viewer can use."]
|
|
2479
|
+
],
|
|
2480
|
+
"proof-walkthrough": [
|
|
2481
|
+
[
|
|
2482
|
+
"Set the scene",
|
|
2483
|
+
"Establish the ingredients, tools, or starting condition."
|
|
2484
|
+
],
|
|
2485
|
+
[
|
|
2486
|
+
"Show the essential move",
|
|
2487
|
+
"Show the central action in a close, readable moment."
|
|
2488
|
+
],
|
|
2489
|
+
[
|
|
2490
|
+
"Land the outcome",
|
|
2491
|
+
"Show the finished result and the one idea to remember."
|
|
2492
|
+
]
|
|
2493
|
+
],
|
|
2494
|
+
"announcement-brief": [
|
|
2495
|
+
[
|
|
2496
|
+
"Lead with the news",
|
|
2497
|
+
"Make the new thing unmistakable at first glance."
|
|
2498
|
+
],
|
|
2499
|
+
["Show the proof", "Show the detail that makes the announcement matter."],
|
|
2500
|
+
["Give the next step", "End with a simple, concrete invitation."]
|
|
2501
|
+
],
|
|
2502
|
+
"social-remix": [
|
|
2503
|
+
[
|
|
2504
|
+
"Find the strongest moment",
|
|
2505
|
+
"Open on the most recognisable moment in the supplied clip."
|
|
2506
|
+
],
|
|
2507
|
+
[
|
|
2508
|
+
"Add the context",
|
|
2509
|
+
"Use a second visual beat to make the point clear without stopping the clip."
|
|
2510
|
+
],
|
|
2511
|
+
[
|
|
2512
|
+
"End before it drags",
|
|
2513
|
+
"Leave one memorable line or image as the viewer scrolls on."
|
|
2514
|
+
]
|
|
2515
|
+
],
|
|
2516
|
+
"finance-brief": [
|
|
2517
|
+
["Name the decision", "Establish the question behind the numbers."],
|
|
2518
|
+
[
|
|
2519
|
+
"Make the evidence visible",
|
|
2520
|
+
"Show the comparison or trend as a simple visual fact."
|
|
2521
|
+
],
|
|
2522
|
+
[
|
|
2523
|
+
"State the recommendation",
|
|
2524
|
+
"End with the decision the evidence supports."
|
|
2525
|
+
]
|
|
2526
|
+
]
|
|
2527
|
+
};
|
|
2528
|
+
return frameCopy[args.templateId].map(([title2, purpose], index) => ({
|
|
2529
|
+
id: `beat-${index + 1}`,
|
|
2530
|
+
title: title2,
|
|
2531
|
+
purpose,
|
|
2532
|
+
visualBrief: `${title2} for ${args.title}. Editorial composition, one clear subject, generous negative space, no words, numbers, logos, or interface elements.`,
|
|
2533
|
+
alt: `${title2}: a proposed visual beat for ${args.title}.`,
|
|
2534
|
+
asset: { kind: "image", state: "proposed" }
|
|
2535
|
+
}));
|
|
2536
|
+
}
|
|
2537
|
+
function proposeVideoStoryboard(args, acceptedPreview) {
|
|
2538
|
+
const title2 = args.title ?? titleFromBrief(args.brief);
|
|
2539
|
+
const recommendation = recommendStoryboardStructure(args.brief);
|
|
2540
|
+
const templateId = args.templateId ?? recommendation.templateId;
|
|
2541
|
+
const kind = recommendation.kind;
|
|
2542
|
+
const template = findPublicTemplate(templateId);
|
|
2543
|
+
const reviewRequired = HEALTH_OR_SAFETY_PATTERN.test(args.brief);
|
|
2544
|
+
const isProcedure = kind === "procedure" && templateId === "qa-ad";
|
|
2545
|
+
return {
|
|
2546
|
+
version: "1.0",
|
|
2547
|
+
brief: args.brief.trim(),
|
|
2548
|
+
title: title2,
|
|
2549
|
+
...args.body ? { body: args.body } : {},
|
|
2550
|
+
...args.cta ? { cta: args.cta } : {},
|
|
2551
|
+
aspect: args.aspect,
|
|
2552
|
+
...args.acceptedDesign ? {
|
|
2553
|
+
acceptedDesign: args.acceptedDesign,
|
|
2554
|
+
acceptedPreview: acceptedPreview?.({ ...args, title: title2 })
|
|
2555
|
+
} : {},
|
|
2556
|
+
template: {
|
|
2557
|
+
id: template.id,
|
|
2558
|
+
name: isProcedure ? "Visual how-to" : template.name,
|
|
2559
|
+
reason: isProcedure ? "Show a physical process as clear visual steps, then the usable outcome." : template.description,
|
|
2560
|
+
variant: isProcedure ? "procedural" : "default"
|
|
2561
|
+
},
|
|
2562
|
+
visualDirection: {
|
|
2563
|
+
communicationJob: "Make the subject and sequence clear at a glance.",
|
|
2564
|
+
style: args.acceptedDesign?.style ?? "Editorial, minimal, and scene-led rather than text-led.",
|
|
2565
|
+
factualReviewRequired: reviewRequired
|
|
2566
|
+
},
|
|
2567
|
+
frames: storyboardFrames({ title: title2, templateId, kind }),
|
|
2568
|
+
imageGeneration: {
|
|
2569
|
+
state: "not-generated",
|
|
2570
|
+
notice: "These are visual briefs, not generated images. GPT-Image-2 drafts require an explicit, signed-in Studio generation step with cost and provenance controls."
|
|
621
2571
|
}
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
function runProposeVideoStoryboardTool(args, acceptedPreview) {
|
|
2575
|
+
const storyboard = proposeVideoStoryboard(args, acceptedPreview);
|
|
2576
|
+
return result(
|
|
2577
|
+
{ storyboard },
|
|
2578
|
+
"A three-beat visual storyboard is ready. It is a proposal only: no images were generated, no project was saved, and no render was started."
|
|
2579
|
+
);
|
|
2580
|
+
}
|
|
2581
|
+
function runListCarouselTemplatesTool() {
|
|
2582
|
+
const templates = CAROUSEL_ARCHETYPES.map(({ id: id2, label: label2, blurb }) => ({
|
|
2583
|
+
id: id2,
|
|
2584
|
+
name: label2,
|
|
2585
|
+
description: blurb,
|
|
2586
|
+
defaultAspect: "4:5",
|
|
2587
|
+
visualStyle: "editorial"
|
|
2588
|
+
}));
|
|
2589
|
+
return result(
|
|
2590
|
+
{ templates, creativeGuides: CREATIVE_GUIDE_INDEX },
|
|
2591
|
+
templates.map((template) => `${template.id}: ${template.description}`).join("\n")
|
|
2592
|
+
);
|
|
2593
|
+
}
|
|
2594
|
+
function runGetCarouselSchemaTool() {
|
|
2595
|
+
return result(
|
|
2596
|
+
{ schemaVersion: "1.0", jsonSchema: PUBLIC_CAROUSEL_DECK_JSON_SCHEMA },
|
|
2597
|
+
"CarouselDeck schema version 1.0. Text, branding, numbering, and layout remain deterministic."
|
|
2598
|
+
);
|
|
2599
|
+
}
|
|
2600
|
+
function runScaffoldCarouselDeckTool(args) {
|
|
2601
|
+
const archetype = getArchetype(args.templateId);
|
|
2602
|
+
if (!archetype) {
|
|
2603
|
+
return {
|
|
2604
|
+
isError: true,
|
|
2605
|
+
content: [{ type: "text", text: "Unknown carousel template." }]
|
|
2606
|
+
};
|
|
2607
|
+
}
|
|
2608
|
+
const deck = archetype.build({
|
|
2609
|
+
hook: args.hook,
|
|
2610
|
+
subtitle: args.subtitle,
|
|
2611
|
+
points: args.points,
|
|
2612
|
+
payoff: args.payoff,
|
|
2613
|
+
cta: args.cta,
|
|
2614
|
+
handle: args.handle
|
|
622
2615
|
});
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
2616
|
+
deck.aspect = args.aspect;
|
|
2617
|
+
const validation = validateCarouselDeck(deck);
|
|
2618
|
+
if (!validation.ok) {
|
|
2619
|
+
return {
|
|
2620
|
+
isError: true,
|
|
2621
|
+
structuredContent: { issues: validation.issues },
|
|
2622
|
+
content: [
|
|
2623
|
+
{
|
|
2624
|
+
type: "text",
|
|
2625
|
+
text: `Carousel deck is invalid: ${validation.issues.join("; ")}`
|
|
2626
|
+
}
|
|
2627
|
+
]
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
return result(
|
|
2631
|
+
{
|
|
2632
|
+
schemaVersion: "1.0",
|
|
2633
|
+
templateId: args.templateId,
|
|
2634
|
+
deck,
|
|
2635
|
+
validation
|
|
634
2636
|
},
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
2637
|
+
`A ${validation.slideCount}-slide ${args.templateId} deck is ready for local rendering. Review every claim and add only rights-cleared images.`
|
|
2638
|
+
);
|
|
2639
|
+
}
|
|
2640
|
+
function runListTemplatesTool(args, paired) {
|
|
2641
|
+
const templates = args.aspect ? PUBLIC_VIDEO_TEMPLATES.filter(
|
|
2642
|
+
(template) => template.aspects.includes(args.aspect)
|
|
2643
|
+
) : PUBLIC_VIDEO_TEMPLATES;
|
|
2644
|
+
return result(
|
|
2645
|
+
{
|
|
2646
|
+
templates: templates.map(
|
|
2647
|
+
(template) => withFrameworks({ ...template }, paired)
|
|
2648
|
+
)
|
|
641
2649
|
},
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
};
|
|
2650
|
+
templates.map((template) => `${template.id}: ${template.description}`).join("\n")
|
|
2651
|
+
);
|
|
645
2652
|
}
|
|
646
|
-
function
|
|
647
|
-
|
|
648
|
-
|
|
2653
|
+
function runGetTemplateTool(args, paired) {
|
|
2654
|
+
const template = findPublicTemplate(args.templateId);
|
|
2655
|
+
if (!template) {
|
|
2656
|
+
return {
|
|
2657
|
+
isError: true,
|
|
2658
|
+
content: [{ type: "text", text: "Unknown template." }]
|
|
2659
|
+
};
|
|
2660
|
+
}
|
|
2661
|
+
return result(
|
|
2662
|
+
{ template: withFrameworks({ ...template }, paired) },
|
|
2663
|
+
`${template.name}
|
|
2664
|
+
${template.description}`
|
|
2665
|
+
);
|
|
2666
|
+
}
|
|
2667
|
+
function runGetVideoSchemaTool() {
|
|
2668
|
+
return result(
|
|
2669
|
+
{ schemaVersion: "1.0", jsonSchema: VIDEO_SPEC_JSON_SCHEMA },
|
|
2670
|
+
"VideoSpec schema version 1.0. Use only published template and scene identifiers."
|
|
2671
|
+
);
|
|
2672
|
+
}
|
|
2673
|
+
function runScaffoldVideoSpecTool(args, paired, acceptedPreview) {
|
|
2674
|
+
const spec = scaffoldVideoSpec(args, { pairedTemplates: paired });
|
|
2675
|
+
return result(
|
|
2676
|
+
{
|
|
2677
|
+
spec,
|
|
2678
|
+
filename: `${String(spec.id)}.video.json`,
|
|
2679
|
+
planningContext: {
|
|
2680
|
+
brief: args.brief,
|
|
2681
|
+
title: args.title,
|
|
2682
|
+
body: args.body,
|
|
2683
|
+
cta: args.cta,
|
|
2684
|
+
aspect: args.aspect,
|
|
2685
|
+
templateId: args.templateId,
|
|
2686
|
+
acceptedDesign: args.acceptedDesign,
|
|
2687
|
+
acceptedPreview: acceptedPreview?.(args)
|
|
2688
|
+
}
|
|
2689
|
+
},
|
|
2690
|
+
`Starter VideoSpec created for ${args.templateId}. Replace placeholder copy and media before rendering.`
|
|
2691
|
+
);
|
|
2692
|
+
}
|
|
2693
|
+
function runGetExampleTool(args, paired) {
|
|
2694
|
+
const spec = scaffoldVideoSpec(
|
|
2695
|
+
{
|
|
2696
|
+
templateId: args.templateId,
|
|
2697
|
+
title: `Example ${args.templateId}`,
|
|
2698
|
+
aspect: "9:16"
|
|
2699
|
+
},
|
|
2700
|
+
{ pairedTemplates: paired }
|
|
2701
|
+
);
|
|
2702
|
+
return result(
|
|
2703
|
+
{ templateId: args.templateId, spec },
|
|
2704
|
+
`Example VideoSpec for ${args.templateId}.`
|
|
2705
|
+
);
|
|
2706
|
+
}
|
|
2707
|
+
function runGetRenderInstructionsTool(args) {
|
|
2708
|
+
const commands = [
|
|
2709
|
+
`git clone ${TEMPLATE_REPOSITORY}.git`,
|
|
2710
|
+
"cd remotion-templates",
|
|
2711
|
+
"npm install",
|
|
2712
|
+
`# Generate narration at ${VOICE_STUDIO_URL}, with one paragraph per scene`,
|
|
2713
|
+
"npm run video:timing -- path/to/video.video.json --audio public/generated/narration.wav --manifest path/to/narration.timing.json",
|
|
2714
|
+
"npm run video:validate -- path/to/video.video.json",
|
|
2715
|
+
"npm run video:preview -- path/to/video.video.json",
|
|
2716
|
+
"npm run video:render -- path/to/video.video.json",
|
|
2717
|
+
"npm run video:qa -- path/to/video.video.json path/to/video.mp4"
|
|
2718
|
+
];
|
|
2719
|
+
const guidance = args.client === "codex" ? "Ask Codex to clone the repository, read AGENTS.md and the make-video skill, author a VideoSpec, validate it, preview representative frames, and render only after validation passes." : args.client === "claude-code" ? "Ask Claude Code to clone the repository, read CLAUDE.md and the make-video skill, author a VideoSpec, validate it, preview representative frames, and render only after validation passes." : "Follow the repository README and run the commands in order.";
|
|
2720
|
+
return result(
|
|
2721
|
+
{
|
|
2722
|
+
repository: TEMPLATE_REPOSITORY,
|
|
2723
|
+
client: args.client,
|
|
2724
|
+
guidance,
|
|
2725
|
+
commands
|
|
2726
|
+
},
|
|
2727
|
+
`${guidance}
|
|
2728
|
+
|
|
2729
|
+
${commands.join("\n")}`
|
|
2730
|
+
);
|
|
2731
|
+
}
|
|
2732
|
+
function runDiagnoseRenderErrorTool(args) {
|
|
2733
|
+
const text3 = args.errorText.toLowerCase();
|
|
2734
|
+
let diagnosis = "Run validation first, confirm Node 22 or newer, and retry with the smallest included example.";
|
|
2735
|
+
let code = "unknown-render-error";
|
|
2736
|
+
if (text3.includes("enoent") || text3.includes("not found")) {
|
|
2737
|
+
code = "missing-file";
|
|
2738
|
+
diagnosis = "A referenced spec, media, font, or asset path is missing. Resolve paths relative to the repository and confirm exact filename case.";
|
|
2739
|
+
} else if (text3.includes("ffmpeg") || text3.includes("codec")) {
|
|
2740
|
+
code = "media-codec";
|
|
2741
|
+
diagnosis = "The source media or local ffmpeg setup is incompatible. Probe the file, then transcode to H.264 video with AAC audio.";
|
|
2742
|
+
} else if (text3.includes("schema") || text3.includes("invalid") || text3.includes("zod")) {
|
|
2743
|
+
code = "invalid-video-spec";
|
|
2744
|
+
diagnosis = "The VideoSpec does not match schema version 1.0. Fetch get_video_schema and correct the reported field before rendering.";
|
|
2745
|
+
} else if (text3.includes("chrome") || text3.includes("browser")) {
|
|
2746
|
+
code = "browser-runtime";
|
|
2747
|
+
diagnosis = "Remotion could not start its browser runtime. Reinstall dependencies and follow the platform-specific browser setup in the repository README.";
|
|
2748
|
+
}
|
|
2749
|
+
return result(
|
|
2750
|
+
{
|
|
2751
|
+
code,
|
|
2752
|
+
diagnosis,
|
|
2753
|
+
nextStep: "Use get_render_instructions for the canonical local workflow."
|
|
2754
|
+
},
|
|
2755
|
+
`${code}: ${diagnosis}`
|
|
2756
|
+
);
|
|
2757
|
+
}
|
|
2758
|
+
var READ_ONLY_ANNOTATIONS = {
|
|
2759
|
+
readOnlyHint: true,
|
|
2760
|
+
openWorldHint: false,
|
|
2761
|
+
destructiveHint: false
|
|
2762
|
+
};
|
|
2763
|
+
function registerSharedTemplateTools(server, options) {
|
|
2764
|
+
const paired = options.pairedTemplates;
|
|
2765
|
+
const meta = (name) => options.toolMeta ? { _meta: options.toolMeta(name) } : {};
|
|
2766
|
+
const catalogue = paired ? "video" : "Remotion";
|
|
649
2767
|
server.registerTool(
|
|
650
|
-
"
|
|
2768
|
+
"list_carousel_templates",
|
|
651
2769
|
{
|
|
652
|
-
title: "
|
|
653
|
-
description: "
|
|
2770
|
+
title: "List carousel templates",
|
|
2771
|
+
description: "List the account-free editorial carousel story structures.",
|
|
654
2772
|
inputSchema: {},
|
|
655
|
-
annotations
|
|
2773
|
+
annotations: {
|
|
2774
|
+
title: "List carousel templates",
|
|
2775
|
+
...READ_ONLY_ANNOTATIONS
|
|
2776
|
+
},
|
|
2777
|
+
...meta("list_carousel_templates")
|
|
656
2778
|
},
|
|
657
|
-
async () =>
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
"
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
2779
|
+
async () => runListCarouselTemplatesTool()
|
|
2780
|
+
);
|
|
2781
|
+
server.registerTool(
|
|
2782
|
+
"get_carousel_schema",
|
|
2783
|
+
{
|
|
2784
|
+
title: "Get CarouselDeck schema",
|
|
2785
|
+
description: "Return the JSON Schema for deterministic Instavar carousel decks.",
|
|
2786
|
+
inputSchema: {},
|
|
2787
|
+
annotations: { title: "Get carousel schema", ...READ_ONLY_ANNOTATIONS },
|
|
2788
|
+
...meta("get_carousel_schema")
|
|
2789
|
+
},
|
|
2790
|
+
async () => runGetCarouselSchemaTool()
|
|
2791
|
+
);
|
|
2792
|
+
server.registerTool(
|
|
2793
|
+
"scaffold_carousel_deck",
|
|
2794
|
+
{
|
|
2795
|
+
title: "Scaffold carousel deck",
|
|
2796
|
+
description: "Build a deterministic editorial carousel deck from accepted copy. This does not render, save, or publish it.",
|
|
2797
|
+
inputSchema: {
|
|
2798
|
+
templateId: carouselTemplateIdSchema.describe(
|
|
2799
|
+
"Story structure from list_carousel_templates."
|
|
2800
|
+
),
|
|
2801
|
+
hook: z5.string().min(1).max(100).describe("Cover line on the first slide."),
|
|
2802
|
+
subtitle: z5.string().min(1).max(160).optional().describe("Optional cover subtitle under the hook."),
|
|
2803
|
+
points: z5.array(z5.string().min(1).max(120)).min(1).max(5).describe(
|
|
2804
|
+
"Body beats in order. listicle lists them on one slide, manifesto gives each its own slide, before-after uses the first as the problem and the last as the contrast, and value-first-reveal uses the first three."
|
|
2805
|
+
),
|
|
2806
|
+
payoff: z5.string().min(1).max(120).describe("Closing takeaway on the final slide."),
|
|
2807
|
+
cta: z5.string().min(1).max(140).optional().describe("Optional call to action on the final slide."),
|
|
2808
|
+
handle: z5.string().min(1).max(40).optional().describe("Optional brand handle shown in the slide footer."),
|
|
2809
|
+
aspect: z5.enum(["4:5", "1:1", "9:16"]).default("4:5").describe("Slide aspect ratio. Defaults to 4:5.")
|
|
686
2810
|
},
|
|
687
|
-
|
|
688
|
-
|
|
2811
|
+
annotations: {
|
|
2812
|
+
title: "Scaffold carousel deck",
|
|
2813
|
+
...READ_ONLY_ANNOTATIONS
|
|
2814
|
+
},
|
|
2815
|
+
...meta("scaffold_carousel_deck")
|
|
2816
|
+
},
|
|
2817
|
+
async (args) => runScaffoldCarouselDeckTool(args)
|
|
689
2818
|
);
|
|
690
2819
|
server.registerTool(
|
|
691
2820
|
"list_templates",
|
|
692
2821
|
{
|
|
693
|
-
title:
|
|
694
|
-
description: "List
|
|
695
|
-
inputSchema: {
|
|
696
|
-
|
|
2822
|
+
title: `List ${catalogue} templates`,
|
|
2823
|
+
description: paired ? "List the published video template families, each available in Remotion and HyperFrames, and optionally filter by output aspect ratio." : "List the published Remotion template families and optionally filter by output aspect ratio.",
|
|
2824
|
+
inputSchema: {
|
|
2825
|
+
aspect: aspectSchema.optional().describe(
|
|
2826
|
+
"Only list templates that support this aspect ratio. Omit to list them all."
|
|
2827
|
+
)
|
|
2828
|
+
},
|
|
2829
|
+
annotations: { title: "List templates", ...READ_ONLY_ANNOTATIONS },
|
|
2830
|
+
...meta("list_templates")
|
|
697
2831
|
},
|
|
698
|
-
async (
|
|
699
|
-
const filtered = aspect ? templates.filter((template) => template.aspects.includes(aspect)) : templates;
|
|
700
|
-
return result(
|
|
701
|
-
{ templates: filtered },
|
|
702
|
-
filtered.map((template) => `${template.id}: ${template.description}`).join("\n")
|
|
703
|
-
);
|
|
704
|
-
}
|
|
2832
|
+
async (args) => runListTemplatesTool(args, paired)
|
|
705
2833
|
);
|
|
706
2834
|
server.registerTool(
|
|
707
2835
|
"get_template",
|
|
708
2836
|
{
|
|
709
|
-
title:
|
|
710
|
-
description: "Get one
|
|
711
|
-
inputSchema: {
|
|
712
|
-
|
|
2837
|
+
title: `Get ${catalogue} template`,
|
|
2838
|
+
description: "Get one template's purpose, aspect ratios, scene kinds, and example identifier.",
|
|
2839
|
+
inputSchema: {
|
|
2840
|
+
templateId: templateIdSchema.describe(
|
|
2841
|
+
"Template family from list_templates."
|
|
2842
|
+
)
|
|
2843
|
+
},
|
|
2844
|
+
annotations: { title: "Get template", ...READ_ONLY_ANNOTATIONS },
|
|
2845
|
+
...meta("get_template")
|
|
713
2846
|
},
|
|
714
|
-
async (
|
|
715
|
-
const template = templates.find((item) => item.id === templateId);
|
|
716
|
-
return result(
|
|
717
|
-
{ template },
|
|
718
|
-
`${template?.name ?? templateId}
|
|
719
|
-
${template?.description ?? ""}`
|
|
720
|
-
);
|
|
721
|
-
}
|
|
2847
|
+
async (args) => runGetTemplateTool(args, paired)
|
|
722
2848
|
);
|
|
723
2849
|
server.registerTool(
|
|
724
2850
|
"get_video_schema",
|
|
725
2851
|
{
|
|
726
2852
|
title: "Get VideoSpec schema",
|
|
727
|
-
description: "Return the
|
|
2853
|
+
description: "Return the published JSON Schema for VideoSpec version 1.0.",
|
|
728
2854
|
inputSchema: {},
|
|
729
|
-
annotations
|
|
2855
|
+
annotations: { title: "Get video schema", ...READ_ONLY_ANNOTATIONS },
|
|
2856
|
+
...meta("get_video_schema")
|
|
730
2857
|
},
|
|
731
|
-
async () =>
|
|
732
|
-
{ schemaVersion: "1.0", jsonSchema: videoSpecSchema },
|
|
733
|
-
"VideoSpec schema version 1.0."
|
|
734
|
-
)
|
|
2858
|
+
async () => runGetVideoSchemaTool()
|
|
735
2859
|
);
|
|
736
2860
|
server.registerTool(
|
|
737
2861
|
"get_example",
|
|
738
2862
|
{
|
|
739
2863
|
title: "Get VideoSpec example",
|
|
740
|
-
description: "Return a starter example for one template.",
|
|
741
|
-
inputSchema: {
|
|
742
|
-
|
|
2864
|
+
description: "Return a valid starter example for one template family.",
|
|
2865
|
+
inputSchema: {
|
|
2866
|
+
templateId: templateIdSchema.describe(
|
|
2867
|
+
"Template family whose starter VideoSpec to return."
|
|
2868
|
+
)
|
|
2869
|
+
},
|
|
2870
|
+
annotations: { title: "Get example", ...READ_ONLY_ANNOTATIONS },
|
|
2871
|
+
...meta("get_example")
|
|
743
2872
|
},
|
|
744
|
-
async (
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
2873
|
+
async (args) => runGetExampleTool(args, paired)
|
|
2874
|
+
);
|
|
2875
|
+
server.registerTool(
|
|
2876
|
+
"propose_video_storyboard",
|
|
2877
|
+
{
|
|
2878
|
+
title: "Propose a video storyboard",
|
|
2879
|
+
description: "Turn an ordinary video request into a three-beat visual proposal and recommended template. Pass templateId to preserve an accepted template when changing only the aspect; omit it for a new direction. This tool does not generate images, save a project, render, or publish.",
|
|
2880
|
+
inputSchema: {
|
|
2881
|
+
brief: storyboardBriefSchema.describe(
|
|
2882
|
+
"The user's video request in their own words. It picks the recommended template and, without a title, the working title."
|
|
2883
|
+
),
|
|
2884
|
+
templateId: templateIdSchema.optional().describe(
|
|
2885
|
+
"Accepted template family to keep, for example when only the aspect changes. Omit for a new direction."
|
|
2886
|
+
),
|
|
2887
|
+
title: z5.string().min(1).max(160).optional().describe(
|
|
2888
|
+
"Accepted title, if the user supplied one. Omit to derive one from the brief."
|
|
2889
|
+
),
|
|
2890
|
+
body: z5.string().min(1).max(2e3).optional().describe("Accepted body copy, kept exactly as supplied."),
|
|
2891
|
+
cta: z5.string().min(1).max(500).optional().describe("Accepted call to action, kept exactly as supplied."),
|
|
2892
|
+
acceptedDesign: acceptedDesignSchema.optional().describe(
|
|
2893
|
+
"The design the user accepted: style, duration in seconds, audio, illustration and exclusions. Pass it on to scaffold_video_spec and hosted tools."
|
|
2894
|
+
),
|
|
2895
|
+
aspect: aspectSchema.default("9:16").describe("Output aspect ratio. Defaults to 9:16.")
|
|
752
2896
|
},
|
|
753
|
-
|
|
754
|
-
|
|
2897
|
+
annotations: {
|
|
2898
|
+
title: "Propose video storyboard",
|
|
2899
|
+
...READ_ONLY_ANNOTATIONS
|
|
2900
|
+
},
|
|
2901
|
+
...meta("propose_video_storyboard")
|
|
2902
|
+
},
|
|
2903
|
+
async (args) => runProposeVideoStoryboardTool(args, options.acceptedPreview)
|
|
755
2904
|
);
|
|
756
2905
|
server.registerTool(
|
|
757
2906
|
"scaffold_video_spec",
|
|
758
2907
|
{
|
|
759
2908
|
title: "Scaffold VideoSpec",
|
|
760
|
-
description: "Return a deterministic VideoSpec skeleton
|
|
2909
|
+
description: "Return a deterministic VideoSpec skeleton. This tool does not generate creative copy or write files.",
|
|
761
2910
|
inputSchema: {
|
|
762
|
-
templateId: templateIdSchema
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
2911
|
+
templateId: templateIdSchema.describe(
|
|
2912
|
+
"Template family from list_templates."
|
|
2913
|
+
),
|
|
2914
|
+
title: z5.string().min(1).max(160).describe(
|
|
2915
|
+
"Video title, shown in the opening scene and used for the spec ID."
|
|
2916
|
+
),
|
|
2917
|
+
brief: z5.string().min(1).max(600).optional().describe(
|
|
2918
|
+
"Original intent and constraints to preserve through planning and handoff. This is not generated video copy."
|
|
2919
|
+
),
|
|
2920
|
+
aspect: aspectSchema.default("9:16").describe("Output aspect ratio. Defaults to 9:16."),
|
|
2921
|
+
mediaSrc: z5.string().min(1).max(500).optional().describe(
|
|
2922
|
+
"Source video path for the video window in qa-ad and social-remix. Other templates ignore it; without it the spec holds a placeholder path."
|
|
2923
|
+
),
|
|
2924
|
+
body: z5.string().min(1).max(2e3).optional().describe(
|
|
2925
|
+
"Accepted body copy. It replaces the placeholder copy and appears once."
|
|
2926
|
+
),
|
|
2927
|
+
cta: z5.string().min(1).max(500).optional().describe("Accepted call to action for the closing scene."),
|
|
2928
|
+
acceptedDesign: acceptedDesignSchema.optional().describe(
|
|
2929
|
+
"The design the user accepted. Returned in planningContext for later steps; it does not change the VideoSpec."
|
|
2930
|
+
)
|
|
766
2931
|
},
|
|
767
|
-
annotations
|
|
2932
|
+
annotations: { title: "Scaffold video spec", ...READ_ONLY_ANNOTATIONS },
|
|
2933
|
+
...meta("scaffold_video_spec")
|
|
768
2934
|
},
|
|
769
|
-
async (args) =>
|
|
770
|
-
const spec = scaffoldVideoSpec(args);
|
|
771
|
-
return result(
|
|
772
|
-
{ spec, filename: `${String(spec.id)}.video.json` },
|
|
773
|
-
"Starter VideoSpec created. Replace placeholder copy and media."
|
|
774
|
-
);
|
|
775
|
-
}
|
|
2935
|
+
async (args) => runScaffoldVideoSpecTool(args, paired, options.acceptedPreview)
|
|
776
2936
|
);
|
|
777
2937
|
server.registerTool(
|
|
778
2938
|
"get_render_instructions",
|
|
779
2939
|
{
|
|
780
2940
|
title: "Get local render instructions",
|
|
781
|
-
description: "Return clone, install, validate, preview, and
|
|
2941
|
+
description: "Return the clone, install, validate, preview, render, and QA workflow.",
|
|
782
2942
|
inputSchema: {
|
|
783
|
-
client:
|
|
2943
|
+
client: z5.enum(["codex", "claude-code", "manual"]).default("manual").describe(
|
|
2944
|
+
"Who will run the workflow. It changes only the guidance text. Defaults to manual."
|
|
2945
|
+
)
|
|
784
2946
|
},
|
|
785
|
-
annotations
|
|
2947
|
+
annotations: {
|
|
2948
|
+
title: "Get render instructions",
|
|
2949
|
+
...READ_ONLY_ANNOTATIONS
|
|
2950
|
+
},
|
|
2951
|
+
...meta("get_render_instructions")
|
|
786
2952
|
},
|
|
787
|
-
async (
|
|
788
|
-
const commands = [
|
|
789
|
-
`git clone ${TEMPLATE_REPOSITORY}.git`,
|
|
790
|
-
"cd remotion-templates",
|
|
791
|
-
"npm install",
|
|
792
|
-
`# Generate one narration paragraph per scene at ${VOICE_STUDIO_URL}`,
|
|
793
|
-
"npm run video:timing -- path/to/video.video.json --audio public/generated/narration.wav --manifest path/to/narration.timing.json",
|
|
794
|
-
"npm run video:validate -- path/to/video.video.json",
|
|
795
|
-
"npm run video:preview -- path/to/video.video.json",
|
|
796
|
-
"npm run video:render -- path/to/video.video.json",
|
|
797
|
-
"npm run video:qa -- path/to/video.video.json path/to/video.mp4"
|
|
798
|
-
];
|
|
799
|
-
return result(
|
|
800
|
-
{ repository: TEMPLATE_REPOSITORY, client, commands },
|
|
801
|
-
commands.join("\n")
|
|
802
|
-
);
|
|
803
|
-
}
|
|
2953
|
+
async (args) => runGetRenderInstructionsTool(args)
|
|
804
2954
|
);
|
|
805
2955
|
server.registerTool(
|
|
806
2956
|
"diagnose_render_error",
|
|
807
2957
|
{
|
|
808
2958
|
title: "Diagnose render error",
|
|
809
|
-
description: "Classify a local validation or render error.",
|
|
810
|
-
inputSchema: {
|
|
2959
|
+
description: "Classify a local validation or render error and return a safe next step.",
|
|
2960
|
+
inputSchema: {
|
|
2961
|
+
errorText: z5.string().min(1).max(1e4).describe("The validation or render error output to classify.")
|
|
2962
|
+
},
|
|
2963
|
+
annotations: { title: "Diagnose render error", ...READ_ONLY_ANNOTATIONS },
|
|
2964
|
+
...meta("diagnose_render_error")
|
|
2965
|
+
},
|
|
2966
|
+
async (args) => runDiagnoseRenderErrorTool(args)
|
|
2967
|
+
);
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
// src/public-templates.ts
|
|
2971
|
+
var TEMPLATE_ENDPOINT = "https://instavar.com/api/mcp/templates";
|
|
2972
|
+
var annotations = {
|
|
2973
|
+
readOnlyHint: true,
|
|
2974
|
+
openWorldHint: false,
|
|
2975
|
+
destructiveHint: false
|
|
2976
|
+
};
|
|
2977
|
+
function result2(value, summary) {
|
|
2978
|
+
return {
|
|
2979
|
+
structuredContent: value,
|
|
2980
|
+
content: [{ type: "text", text: summary }]
|
|
2981
|
+
};
|
|
2982
|
+
}
|
|
2983
|
+
function registerPublicTemplateTools(server, options = {}) {
|
|
2984
|
+
const paired = options.pairedTemplates ?? pairedTemplatesEnabled();
|
|
2985
|
+
registerCreativeGuides(server);
|
|
2986
|
+
registerPairedAnnouncement(server, { pairedCatalogue: paired });
|
|
2987
|
+
if (paired) registerPairedTemplates(server);
|
|
2988
|
+
server.registerTool(
|
|
2989
|
+
"get_capabilities",
|
|
2990
|
+
{
|
|
2991
|
+
title: "Get video capabilities",
|
|
2992
|
+
description: "Call first. Reports free local features and optional account features.",
|
|
2993
|
+
inputSchema: {},
|
|
811
2994
|
annotations
|
|
812
2995
|
},
|
|
813
|
-
async (
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
2996
|
+
async () => result2(
|
|
2997
|
+
{
|
|
2998
|
+
authentication: "optional",
|
|
2999
|
+
apiKeyConfigured: Boolean(process.env.INSTAVAR_API_KEY?.trim()),
|
|
3000
|
+
repository: TEMPLATE_REPOSITORY,
|
|
3001
|
+
endpoint: TEMPLATE_ENDPOINT,
|
|
3002
|
+
creativeGuides: CREATIVE_GUIDE_INDEX,
|
|
3003
|
+
pairedAnnouncement: PAIRED_ANNOUNCEMENT_CAPABILITY,
|
|
3004
|
+
...paired ? { pairedTemplates: PAIRED_TEMPLATE_CAPABILITY } : {},
|
|
3005
|
+
localFeatures: [
|
|
3006
|
+
"template discovery",
|
|
3007
|
+
"three-beat storyboard proposals",
|
|
3008
|
+
"VideoSpec schema and examples",
|
|
3009
|
+
"deterministic scaffolding",
|
|
3010
|
+
"editorial carousel discovery and deterministic deck scaffolding",
|
|
3011
|
+
"local render instructions",
|
|
3012
|
+
"render error diagnosis",
|
|
3013
|
+
"keyless on-device narration with scene timing",
|
|
3014
|
+
"every-scene preview frames",
|
|
3015
|
+
"post-render audible-audio verification"
|
|
3016
|
+
],
|
|
3017
|
+
narration: {
|
|
3018
|
+
url: VOICE_STUDIO_URL,
|
|
3019
|
+
cost: "free",
|
|
3020
|
+
apiKeyRequired: false,
|
|
3021
|
+
guidance: "Write one paragraph per scene. Download the WAV and timing manifest, then apply both before rendering."
|
|
820
3022
|
},
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
3023
|
+
accountFeatures: [
|
|
3024
|
+
"managed rendering and storage",
|
|
3025
|
+
"workflow review and approval",
|
|
3026
|
+
"social publishing and metrics"
|
|
3027
|
+
]
|
|
3028
|
+
},
|
|
3029
|
+
process.env.INSTAVAR_API_KEY?.trim() ? "Free local tools and authenticated Instavar Studio tools are available." : "Free local tools are available. Add INSTAVAR_API_KEY to unlock hosted Studio tools."
|
|
3030
|
+
)
|
|
824
3031
|
);
|
|
3032
|
+
registerSharedTemplateTools(server, { pairedTemplates: paired });
|
|
825
3033
|
}
|
|
826
3034
|
|
|
827
3035
|
// src/tools.ts
|
|
828
|
-
import { z as
|
|
3036
|
+
import { z as z8 } from "zod";
|
|
829
3037
|
|
|
830
3038
|
// src/http.ts
|
|
831
3039
|
import { randomUUID } from "crypto";
|
|
@@ -871,11 +3079,11 @@ function requestFailure(error) {
|
|
|
871
3079
|
return error instanceof Error ? error.message : String(error);
|
|
872
3080
|
}
|
|
873
3081
|
async function parse(res) {
|
|
874
|
-
const
|
|
3082
|
+
const text3 = await res.text();
|
|
875
3083
|
try {
|
|
876
|
-
return JSON.parse(
|
|
3084
|
+
return JSON.parse(text3);
|
|
877
3085
|
} catch {
|
|
878
|
-
return
|
|
3086
|
+
return text3;
|
|
879
3087
|
}
|
|
880
3088
|
}
|
|
881
3089
|
async function apiGet(path) {
|
|
@@ -923,15 +3131,15 @@ ${open}
|
|
|
923
3131
|
${json}
|
|
924
3132
|
${close}`;
|
|
925
3133
|
}
|
|
926
|
-
function toToolResult(
|
|
3134
|
+
function toToolResult(result3) {
|
|
927
3135
|
return {
|
|
928
3136
|
content: [
|
|
929
3137
|
{
|
|
930
3138
|
type: "text",
|
|
931
|
-
text: fenceForModel(JSON.stringify(
|
|
3139
|
+
text: fenceForModel(JSON.stringify(result3.body, null, 2))
|
|
932
3140
|
}
|
|
933
3141
|
],
|
|
934
|
-
isError: !
|
|
3142
|
+
isError: !result3.ok
|
|
935
3143
|
};
|
|
936
3144
|
}
|
|
937
3145
|
async function runTool(fn) {
|
|
@@ -951,7 +3159,7 @@ async function runTool(fn) {
|
|
|
951
3159
|
}
|
|
952
3160
|
|
|
953
3161
|
// src/brief-schema.ts
|
|
954
|
-
import { z as
|
|
3162
|
+
import { z as z6 } from "zod";
|
|
955
3163
|
var PUBLISH_TARGETS = [
|
|
956
3164
|
"linkedin",
|
|
957
3165
|
"x",
|
|
@@ -983,16 +3191,16 @@ var REFERENCE_ROLES = [
|
|
|
983
3191
|
"environment",
|
|
984
3192
|
"style"
|
|
985
3193
|
];
|
|
986
|
-
var optionalDirectionText =
|
|
987
|
-
var visualDirectionSchema =
|
|
3194
|
+
var optionalDirectionText = z6.string().trim().min(1).max(500).optional();
|
|
3195
|
+
var visualDirectionSchema = z6.object({
|
|
988
3196
|
communicationJob: optionalDirectionText,
|
|
989
3197
|
emotionalTarget: optionalDirectionText,
|
|
990
|
-
attentionPath:
|
|
3198
|
+
attentionPath: z6.object({
|
|
991
3199
|
first: optionalDirectionText,
|
|
992
3200
|
next: optionalDirectionText,
|
|
993
3201
|
proof: optionalDirectionText
|
|
994
3202
|
}).optional(),
|
|
995
|
-
visualLanguage:
|
|
3203
|
+
visualLanguage: z6.object({
|
|
996
3204
|
space: optionalDirectionText,
|
|
997
3205
|
line: optionalDirectionText,
|
|
998
3206
|
shape: optionalDirectionText,
|
|
@@ -1001,24 +3209,24 @@ var visualDirectionSchema = z4.object({
|
|
|
1001
3209
|
movement: optionalDirectionText,
|
|
1002
3210
|
rhythm: optionalDirectionText
|
|
1003
3211
|
}).optional(),
|
|
1004
|
-
camera:
|
|
3212
|
+
camera: z6.object({
|
|
1005
3213
|
shotSize: optionalDirectionText,
|
|
1006
3214
|
angle: optionalDirectionText,
|
|
1007
3215
|
movement: optionalDirectionText,
|
|
1008
3216
|
lens: optionalDirectionText
|
|
1009
3217
|
}).optional()
|
|
1010
3218
|
});
|
|
1011
|
-
var generationContractSchema =
|
|
1012
|
-
globalInvariants:
|
|
1013
|
-
shotDelta:
|
|
1014
|
-
referenceBindings:
|
|
1015
|
-
|
|
1016
|
-
assetId:
|
|
1017
|
-
roles:
|
|
3219
|
+
var generationContractSchema = z6.object({
|
|
3220
|
+
globalInvariants: z6.array(z6.string().trim().min(1).max(300)).max(20).optional(),
|
|
3221
|
+
shotDelta: z6.string().trim().min(1).max(500).optional(),
|
|
3222
|
+
referenceBindings: z6.array(
|
|
3223
|
+
z6.object({
|
|
3224
|
+
assetId: z6.string().trim().min(1).max(200),
|
|
3225
|
+
roles: z6.array(z6.enum(REFERENCE_ROLES)).min(1).max(7)
|
|
1018
3226
|
})
|
|
1019
3227
|
).max(20).optional(),
|
|
1020
|
-
prohibitedChanges:
|
|
1021
|
-
reviewCriteria:
|
|
3228
|
+
prohibitedChanges: z6.array(z6.string().trim().min(1).max(300)).max(20).optional(),
|
|
3229
|
+
reviewCriteria: z6.array(z6.string().trim().min(1).max(300)).max(20).optional()
|
|
1022
3230
|
});
|
|
1023
3231
|
var REGION_NAMES = [
|
|
1024
3232
|
"background",
|
|
@@ -1035,55 +3243,55 @@ var ASSET_ROLES = [
|
|
|
1035
3243
|
"lower-third",
|
|
1036
3244
|
"diagram-node"
|
|
1037
3245
|
];
|
|
1038
|
-
var assetSlotStyleSchema =
|
|
1039
|
-
opacity:
|
|
1040
|
-
fit:
|
|
1041
|
-
z:
|
|
3246
|
+
var assetSlotStyleSchema = z6.object({
|
|
3247
|
+
opacity: z6.number().min(0).max(1).optional(),
|
|
3248
|
+
fit: z6.enum(["cover", "contain"]).optional(),
|
|
3249
|
+
z: z6.number().optional()
|
|
1042
3250
|
}).strict();
|
|
1043
|
-
var assetSlotSchema =
|
|
1044
|
-
id:
|
|
1045
|
-
sceneId:
|
|
1046
|
-
region:
|
|
1047
|
-
role:
|
|
1048
|
-
kind:
|
|
1049
|
-
source:
|
|
1050
|
-
intent:
|
|
3251
|
+
var assetSlotSchema = z6.object({
|
|
3252
|
+
id: z6.string().min(1),
|
|
3253
|
+
sceneId: z6.string().min(1).optional(),
|
|
3254
|
+
region: z6.enum(REGION_NAMES),
|
|
3255
|
+
role: z6.enum(ASSET_ROLES),
|
|
3256
|
+
kind: z6.enum(["image", "video"]),
|
|
3257
|
+
source: z6.enum(["auto", "stock", "generate"]),
|
|
3258
|
+
intent: z6.string(),
|
|
1051
3259
|
style: assetSlotStyleSchema.optional(),
|
|
1052
|
-
timing:
|
|
1053
|
-
fromPct:
|
|
1054
|
-
toPct:
|
|
3260
|
+
timing: z6.object({
|
|
3261
|
+
fromPct: z6.number().min(0).max(1).optional(),
|
|
3262
|
+
toPct: z6.number().min(0).max(1).optional()
|
|
1055
3263
|
}).strict().optional(),
|
|
1056
|
-
url:
|
|
3264
|
+
url: z6.string().url().optional()
|
|
1057
3265
|
}).strict();
|
|
1058
|
-
var briefStepSchema =
|
|
1059
|
-
label:
|
|
1060
|
-
latex:
|
|
1061
|
-
explanation:
|
|
3266
|
+
var briefStepSchema = z6.object({
|
|
3267
|
+
label: z6.string(),
|
|
3268
|
+
latex: z6.string().optional(),
|
|
3269
|
+
explanation: z6.string().optional()
|
|
1062
3270
|
});
|
|
1063
|
-
var briefAudioLayerSchema =
|
|
1064
|
-
id:
|
|
1065
|
-
src:
|
|
1066
|
-
startFrame:
|
|
1067
|
-
durationInFrames:
|
|
1068
|
-
volume:
|
|
1069
|
-
fadeFrames:
|
|
1070
|
-
role:
|
|
3271
|
+
var briefAudioLayerSchema = z6.object({
|
|
3272
|
+
id: z6.string().min(1),
|
|
3273
|
+
src: z6.string().url(),
|
|
3274
|
+
startFrame: z6.number().int().nonnegative().default(0),
|
|
3275
|
+
durationInFrames: z6.number().int().positive().optional(),
|
|
3276
|
+
volume: z6.number().min(0).max(1).default(1),
|
|
3277
|
+
fadeFrames: z6.number().int().nonnegative().default(0),
|
|
3278
|
+
role: z6.enum(["music", "foley", "transition", "effect"])
|
|
1071
3279
|
});
|
|
1072
|
-
var protectedRegionEffectSchema =
|
|
1073
|
-
id:
|
|
1074
|
-
kind:
|
|
1075
|
-
coordinateSpace:
|
|
1076
|
-
region:
|
|
1077
|
-
leftPercent:
|
|
1078
|
-
topPercent:
|
|
1079
|
-
widthPercent:
|
|
1080
|
-
heightPercent:
|
|
3280
|
+
var protectedRegionEffectSchema = z6.object({
|
|
3281
|
+
id: z6.string().min(1).max(80),
|
|
3282
|
+
kind: z6.literal("border-glint"),
|
|
3283
|
+
coordinateSpace: z6.literal("output-frame-v1").default("output-frame-v1"),
|
|
3284
|
+
region: z6.object({
|
|
3285
|
+
leftPercent: z6.number().min(0).max(100),
|
|
3286
|
+
topPercent: z6.number().min(0).max(100),
|
|
3287
|
+
widthPercent: z6.number().positive().max(100),
|
|
3288
|
+
heightPercent: z6.number().positive().max(100)
|
|
1081
3289
|
}),
|
|
1082
|
-
startFrame:
|
|
1083
|
-
endFrame:
|
|
1084
|
-
speedUpFrame:
|
|
1085
|
-
color:
|
|
1086
|
-
thicknessPercent:
|
|
3290
|
+
startFrame: z6.number().int().nonnegative().default(0),
|
|
3291
|
+
endFrame: z6.number().int().positive(),
|
|
3292
|
+
speedUpFrame: z6.number().int().nonnegative().optional(),
|
|
3293
|
+
color: z6.string().max(64).default("#ffffff"),
|
|
3294
|
+
thicknessPercent: z6.number().positive().max(20).default(3)
|
|
1087
3295
|
}).superRefine((effect2, context) => {
|
|
1088
3296
|
if (effect2.region.leftPercent + effect2.region.widthPercent > 100) {
|
|
1089
3297
|
context.addIssue({
|
|
@@ -1114,13 +3322,13 @@ var protectedRegionEffectSchema = z4.object({
|
|
|
1114
3322
|
});
|
|
1115
3323
|
}
|
|
1116
3324
|
});
|
|
1117
|
-
var beatVisualRequestSchema =
|
|
1118
|
-
intent:
|
|
1119
|
-
media:
|
|
1120
|
-
source:
|
|
3325
|
+
var beatVisualRequestSchema = z6.object({
|
|
3326
|
+
intent: z6.string().trim().min(1).optional(),
|
|
3327
|
+
media: z6.enum(["still", "motion"]).default("still"),
|
|
3328
|
+
source: z6.enum(["auto", "stock", "generate"]).default("auto")
|
|
1121
3329
|
}).strict();
|
|
1122
|
-
var briefSceneSchema =
|
|
1123
|
-
type:
|
|
3330
|
+
var briefSceneSchema = z6.object({
|
|
3331
|
+
type: z6.enum([
|
|
1124
3332
|
"hero",
|
|
1125
3333
|
"points",
|
|
1126
3334
|
"equation",
|
|
@@ -1143,106 +3351,106 @@ var briefSceneSchema = z4.object({
|
|
|
1143
3351
|
"image-window",
|
|
1144
3352
|
"custom"
|
|
1145
3353
|
]),
|
|
1146
|
-
layoutPreset:
|
|
3354
|
+
layoutPreset: z6.literal("dark-cafe-announcement").optional().describe(
|
|
1147
3355
|
"Built-in portrait coffee illustration with title, subtitle body and text CTA. Use only on a custom scene after the user selects this design. No generated assets."
|
|
1148
3356
|
),
|
|
1149
|
-
durationInFrames:
|
|
1150
|
-
narrationText:
|
|
3357
|
+
durationInFrames: z6.number().optional(),
|
|
3358
|
+
narrationText: z6.string().optional(),
|
|
1151
3359
|
visualResolution: beatVisualRequestSchema.optional(),
|
|
1152
|
-
title:
|
|
1153
|
-
subtitle:
|
|
1154
|
-
text:
|
|
1155
|
-
points:
|
|
1156
|
-
latex:
|
|
1157
|
-
value:
|
|
1158
|
-
max:
|
|
1159
|
-
label:
|
|
1160
|
-
unit:
|
|
1161
|
-
prefix:
|
|
1162
|
-
suffix:
|
|
1163
|
-
decimals:
|
|
1164
|
-
color:
|
|
1165
|
-
tone:
|
|
1166
|
-
iconKey:
|
|
1167
|
-
stepNumber:
|
|
1168
|
-
totalSteps:
|
|
1169
|
-
items:
|
|
1170
|
-
src:
|
|
1171
|
-
caption:
|
|
3360
|
+
title: z6.string().optional(),
|
|
3361
|
+
subtitle: z6.string().optional(),
|
|
3362
|
+
text: z6.string().optional(),
|
|
3363
|
+
points: z6.array(z6.string()).optional(),
|
|
3364
|
+
latex: z6.string().optional(),
|
|
3365
|
+
value: z6.union([z6.string(), z6.number()]).optional(),
|
|
3366
|
+
max: z6.number().optional(),
|
|
3367
|
+
label: z6.string().optional(),
|
|
3368
|
+
unit: z6.string().optional(),
|
|
3369
|
+
prefix: z6.string().optional(),
|
|
3370
|
+
suffix: z6.string().optional(),
|
|
3371
|
+
decimals: z6.number().optional(),
|
|
3372
|
+
color: z6.string().optional(),
|
|
3373
|
+
tone: z6.string().optional(),
|
|
3374
|
+
iconKey: z6.string().optional(),
|
|
3375
|
+
stepNumber: z6.number().optional(),
|
|
3376
|
+
totalSteps: z6.number().optional(),
|
|
3377
|
+
items: z6.array(z6.object({ label: z6.string(), value: z6.number() })).optional(),
|
|
3378
|
+
src: z6.string().optional(),
|
|
3379
|
+
caption: z6.string().optional(),
|
|
1172
3380
|
// A-roll generation (video-window scenes): source "generate" triggers
|
|
1173
3381
|
// flag-gated WaveSpeed image/video gen server-side from generatePrompt;
|
|
1174
3382
|
// "url" (default) uses src as-is. Kept in sync with the app's
|
|
1175
3383
|
// briefSceneSchema - see the parity test in the app suite.
|
|
1176
|
-
source:
|
|
1177
|
-
generatePrompt:
|
|
3384
|
+
source: z6.enum(["url", "generate"]).optional(),
|
|
3385
|
+
generatePrompt: z6.string().optional(),
|
|
1178
3386
|
generationContract: generationContractSchema.optional(),
|
|
1179
|
-
trimStartSeconds:
|
|
1180
|
-
trimEndSeconds:
|
|
1181
|
-
playbackRate:
|
|
1182
|
-
objectFit:
|
|
1183
|
-
objectPosition:
|
|
1184
|
-
scaleFrom:
|
|
1185
|
-
scaleTo:
|
|
1186
|
-
panFromX:
|
|
1187
|
-
panToX:
|
|
1188
|
-
panFromY:
|
|
1189
|
-
panToY:
|
|
1190
|
-
routeFrom:
|
|
1191
|
-
routeTo:
|
|
1192
|
-
routeLabel:
|
|
1193
|
-
overlayKind:
|
|
1194
|
-
overlayPosition:
|
|
1195
|
-
accentColor:
|
|
1196
|
-
overlayBackgroundColor:
|
|
1197
|
-
cutFlash:
|
|
1198
|
-
muted:
|
|
1199
|
-
protectedRegionEffects:
|
|
1200
|
-
flashAtFrame:
|
|
1201
|
-
flashDurationFrames:
|
|
3387
|
+
trimStartSeconds: z6.number().nonnegative().optional(),
|
|
3388
|
+
trimEndSeconds: z6.number().positive().optional(),
|
|
3389
|
+
playbackRate: z6.number().min(0.25).max(8).optional(),
|
|
3390
|
+
objectFit: z6.enum(["cover", "contain"]).optional(),
|
|
3391
|
+
objectPosition: z6.string().optional(),
|
|
3392
|
+
scaleFrom: z6.number().min(0.5).max(2).optional(),
|
|
3393
|
+
scaleTo: z6.number().min(0.5).max(2).optional(),
|
|
3394
|
+
panFromX: z6.number().min(-50).max(50).optional(),
|
|
3395
|
+
panToX: z6.number().min(-50).max(50).optional(),
|
|
3396
|
+
panFromY: z6.number().min(-50).max(50).optional(),
|
|
3397
|
+
panToY: z6.number().min(-50).max(50).optional(),
|
|
3398
|
+
routeFrom: z6.string().optional(),
|
|
3399
|
+
routeTo: z6.string().optional(),
|
|
3400
|
+
routeLabel: z6.string().optional(),
|
|
3401
|
+
overlayKind: z6.enum(["none", "hook", "stage", "end-card"]).optional(),
|
|
3402
|
+
overlayPosition: z6.enum(["center", "top", "bottom"]).optional(),
|
|
3403
|
+
accentColor: z6.string().optional(),
|
|
3404
|
+
overlayBackgroundColor: z6.string().optional(),
|
|
3405
|
+
cutFlash: z6.boolean().optional(),
|
|
3406
|
+
muted: z6.boolean().optional(),
|
|
3407
|
+
protectedRegionEffects: z6.array(protectedRegionEffectSchema).max(4).optional(),
|
|
3408
|
+
flashAtFrame: z6.number().int().nonnegative().optional(),
|
|
3409
|
+
flashDurationFrames: z6.number().int().positive().max(15).optional()
|
|
1202
3410
|
});
|
|
1203
|
-
var directionsBriefSchema =
|
|
1204
|
-
subtitle:
|
|
1205
|
-
unitLabel:
|
|
1206
|
-
distanceTimeChip:
|
|
1207
|
-
clips:
|
|
1208
|
-
|
|
1209
|
-
src:
|
|
1210
|
-
trimStartSeconds:
|
|
1211
|
-
trimEndSeconds:
|
|
1212
|
-
speed:
|
|
1213
|
-
mirrorFlip:
|
|
3411
|
+
var directionsBriefSchema = z6.object({
|
|
3412
|
+
subtitle: z6.string().optional(),
|
|
3413
|
+
unitLabel: z6.string().optional(),
|
|
3414
|
+
distanceTimeChip: z6.string().optional(),
|
|
3415
|
+
clips: z6.array(
|
|
3416
|
+
z6.object({
|
|
3417
|
+
src: z6.string(),
|
|
3418
|
+
trimStartSeconds: z6.number().optional(),
|
|
3419
|
+
trimEndSeconds: z6.number(),
|
|
3420
|
+
speed: z6.number().optional(),
|
|
3421
|
+
mirrorFlip: z6.boolean().optional()
|
|
1214
3422
|
})
|
|
1215
3423
|
).min(1),
|
|
1216
|
-
cues:
|
|
1217
|
-
|
|
1218
|
-
atSeconds:
|
|
1219
|
-
text:
|
|
1220
|
-
arrow:
|
|
1221
|
-
step:
|
|
3424
|
+
cues: z6.array(
|
|
3425
|
+
z6.object({
|
|
3426
|
+
atSeconds: z6.number(),
|
|
3427
|
+
text: z6.string(),
|
|
3428
|
+
arrow: z6.enum(["left", "right", "forward"]).optional(),
|
|
3429
|
+
step: z6.number().optional()
|
|
1222
3430
|
})
|
|
1223
3431
|
),
|
|
1224
|
-
outro:
|
|
1225
|
-
heading:
|
|
1226
|
-
body:
|
|
1227
|
-
seconds:
|
|
3432
|
+
outro: z6.object({
|
|
3433
|
+
heading: z6.string().optional(),
|
|
3434
|
+
body: z6.array(z6.string()).optional(),
|
|
3435
|
+
seconds: z6.number().optional()
|
|
1228
3436
|
}).optional(),
|
|
1229
|
-
music:
|
|
1230
|
-
perSegment:
|
|
1231
|
-
|
|
1232
|
-
src:
|
|
1233
|
-
clipIndexStart:
|
|
1234
|
-
hotOffsetSeconds:
|
|
1235
|
-
volume:
|
|
3437
|
+
music: z6.object({
|
|
3438
|
+
perSegment: z6.array(
|
|
3439
|
+
z6.object({
|
|
3440
|
+
src: z6.string(),
|
|
3441
|
+
clipIndexStart: z6.number().optional(),
|
|
3442
|
+
hotOffsetSeconds: z6.number().optional(),
|
|
3443
|
+
volume: z6.number().optional()
|
|
1236
3444
|
})
|
|
1237
3445
|
).optional(),
|
|
1238
|
-
crossfadeSeconds:
|
|
3446
|
+
crossfadeSeconds: z6.number().optional()
|
|
1239
3447
|
}).optional()
|
|
1240
3448
|
});
|
|
1241
3449
|
var PERSON_CONSENT_WORDING_VERSION = "2026-10-19.1";
|
|
1242
3450
|
var PERSON_CONSENT_WORDING = "I have permission from everyone shown, and no one shown is under 18 unless I am their parent or guardian.";
|
|
1243
|
-
var personConsentSchema =
|
|
1244
|
-
confirmed:
|
|
1245
|
-
wordingVersion:
|
|
3451
|
+
var personConsentSchema = z6.object({
|
|
3452
|
+
confirmed: z6.literal(true).describe("Must be true: the caller confirms the declaration below."),
|
|
3453
|
+
wordingVersion: z6.string().describe(
|
|
1246
3454
|
`Version of the declaration the caller confirmed. Current: ${PERSON_CONSENT_WORDING_VERSION}.`
|
|
1247
3455
|
)
|
|
1248
3456
|
}).strict().describe(
|
|
@@ -1252,79 +3460,79 @@ var LIKENESS_WARNING_WORDING_VERSION = "2026-10-19.1";
|
|
|
1252
3460
|
var LIKENESS_WARNING_WORDING = "This photo seems to show someone other than you. Only continue if you have their permission.";
|
|
1253
3461
|
var LIKENESS_MINOR_WARNING_WORDING_VERSION = "2026-10-19.1";
|
|
1254
3462
|
var LIKENESS_MINOR_WARNING_WORDING = "This may show someone under 18. Continue only if you are their parent or guardian, or have their parent or guardian's permission.";
|
|
1255
|
-
var likenessAcknowledgementSchema =
|
|
1256
|
-
confirmed:
|
|
1257
|
-
kind:
|
|
3463
|
+
var likenessAcknowledgementSchema = z6.object({
|
|
3464
|
+
confirmed: z6.literal(true).describe("Must be true: the user accepted the warning below."),
|
|
3465
|
+
kind: z6.enum(["other_person", "guardian"]).optional().describe(
|
|
1258
3466
|
`Which warning was accepted. "other_person" (default): "${LIKENESS_WARNING_WORDING}" "guardian": "${LIKENESS_MINOR_WARNING_WORDING}"`
|
|
1259
3467
|
),
|
|
1260
|
-
wordingVersion:
|
|
3468
|
+
wordingVersion: z6.string().describe(
|
|
1261
3469
|
`Version of the warning accepted. Current: other_person ${LIKENESS_WARNING_WORDING_VERSION}, guardian ${LIKENESS_MINOR_WARNING_WORDING_VERSION}.`
|
|
1262
3470
|
)
|
|
1263
3471
|
}).strict().describe(
|
|
1264
3472
|
"Optional. Send only after the user has seen and accepted the warning. Without it, a job that animates an uploaded photo of someone other than the account holder stops with likeness_acknowledgement_required, or likeness_guardian_confirmation_required when the person may be under 18."
|
|
1265
3473
|
);
|
|
1266
3474
|
var createBriefShape = {
|
|
1267
|
-
title:
|
|
1268
|
-
script:
|
|
1269
|
-
caption:
|
|
1270
|
-
publishTarget:
|
|
1271
|
-
publishTargets:
|
|
1272
|
-
objective:
|
|
1273
|
-
targetDurationSeconds:
|
|
1274
|
-
narrationMode:
|
|
1275
|
-
lessonTitle:
|
|
3475
|
+
title: z6.string().min(1).max(120).describe("Short title (max 120 chars)"),
|
|
3476
|
+
script: z6.string().min(1).describe("Full narration script (sent to TTS)"),
|
|
3477
|
+
caption: z6.string().min(1).describe("Social caption with hooks, hashtags, CTA"),
|
|
3478
|
+
publishTarget: z6.enum(PUBLISH_TARGETS).describe("Target social platform"),
|
|
3479
|
+
publishTargets: z6.array(z6.enum(PUBLISH_TARGETS)).max(9).optional().describe("All allowed social destinations for this video"),
|
|
3480
|
+
objective: z6.enum(BRIEF_OBJECTIVES).describe("Content objective (drives composition routing)"),
|
|
3481
|
+
targetDurationSeconds: z6.number().int().min(5).max(600).optional().describe("Requested final video duration in seconds"),
|
|
3482
|
+
narrationMode: z6.enum(["tts", "off"]).optional().describe("Disable synthesized narration for source-audio or silent work"),
|
|
3483
|
+
lessonTitle: z6.string().optional().describe("Display title rendered inside the video"),
|
|
1276
3484
|
visualDirection: visualDirectionSchema.optional().describe(
|
|
1277
3485
|
"Whole-video communication, attention, visual-language, and camera direction."
|
|
1278
3486
|
),
|
|
1279
|
-
steps:
|
|
1280
|
-
scenes:
|
|
3487
|
+
steps: z6.array(briefStepSchema).optional().describe("Structured steps for educational content (LaTeX)"),
|
|
3488
|
+
scenes: z6.array(briefSceneSchema).optional().describe("Scene graph for the UniversalVideo composition"),
|
|
1281
3489
|
// No zod .default() here: a default would materialize into the parsed tool
|
|
1282
3490
|
// arguments and relay to /api/v1/jobs as if the caller stated it, corrupting
|
|
1283
3491
|
// the resolution provenance the API echoes back. Unset must stay absent;
|
|
1284
3492
|
// the API re-applies 9:16 server-side.
|
|
1285
|
-
aspectRatio:
|
|
1286
|
-
compositionId:
|
|
1287
|
-
voiceId:
|
|
3493
|
+
aspectRatio: z6.enum(ASPECT_RATIOS).optional().describe("Aspect ratio (defaults to 9:16 when omitted)"),
|
|
3494
|
+
compositionId: z6.string().optional().describe("Remotion composition ID override (advanced)"),
|
|
3495
|
+
voiceId: z6.string().optional().describe(
|
|
1288
3496
|
"TTS narrator voice ID. Unknown IDs are rejected before rendering; when omitted, the org's saved voice preference or the system default is used and reported in the response's resolution block."
|
|
1289
3497
|
),
|
|
1290
3498
|
directions: directionsBriefSchema.optional().describe(
|
|
1291
3499
|
"DirectionsWalkthrough 'find-us' payload: source clips (legs), decision-point cues on the realtime walk timeline, outro, and optional music. Set compositionId 'DirectionsWalkthrough' (or objective 'directions')."
|
|
1292
3500
|
),
|
|
1293
|
-
assetSlots:
|
|
3501
|
+
assetSlots: z6.array(assetSlotSchema).optional().describe(
|
|
1294
3502
|
"Optional storyboard asset slots: region-placed image/video assets or placeholders attached to the whole video or a specific scene."
|
|
1295
3503
|
),
|
|
1296
|
-
audioLayers:
|
|
3504
|
+
audioLayers: z6.array(briefAudioLayerSchema).optional().describe("Optional typed music, Foley, transition, and effect layers."),
|
|
1297
3505
|
personConsent: personConsentSchema.optional(),
|
|
1298
3506
|
likenessAcknowledgement: likenessAcknowledgementSchema.optional()
|
|
1299
3507
|
};
|
|
1300
3508
|
var editBriefShape = {
|
|
1301
|
-
jobId:
|
|
3509
|
+
jobId: z6.string().uuid().describe("The job to edit"),
|
|
1302
3510
|
personConsent: personConsentSchema.optional(),
|
|
1303
3511
|
likenessAcknowledgement: likenessAcknowledgementSchema.optional(),
|
|
1304
|
-
title:
|
|
1305
|
-
script:
|
|
1306
|
-
caption:
|
|
1307
|
-
lessonTitle:
|
|
1308
|
-
narrationMode:
|
|
3512
|
+
title: z6.string().optional().describe("Updated title"),
|
|
3513
|
+
script: z6.string().optional().describe("Updated narration script"),
|
|
3514
|
+
caption: z6.string().optional().describe("Updated social caption"),
|
|
3515
|
+
lessonTitle: z6.string().optional().describe("Updated lesson title"),
|
|
3516
|
+
narrationMode: z6.enum(["tts", "off"]).optional().describe("Updated synthesized narration mode"),
|
|
1309
3517
|
visualDirection: visualDirectionSchema.optional().describe("Updated whole-video visual direction"),
|
|
1310
|
-
steps:
|
|
1311
|
-
scenes:
|
|
1312
|
-
aspectRatio:
|
|
1313
|
-
objective:
|
|
3518
|
+
steps: z6.array(briefStepSchema).optional().describe("Updated steps (replaces the array)"),
|
|
3519
|
+
scenes: z6.array(briefSceneSchema).optional().describe("Updated scenes (replaces the array)"),
|
|
3520
|
+
aspectRatio: z6.enum(ASPECT_RATIOS).optional().describe("Updated aspect ratio"),
|
|
3521
|
+
objective: z6.enum(BRIEF_OBJECTIVES).optional().describe(
|
|
1314
3522
|
"Updated content objective. Re-routes the composition and triggers a re-render."
|
|
1315
3523
|
),
|
|
1316
|
-
publishTarget:
|
|
3524
|
+
publishTarget: z6.enum(PUBLISH_TARGETS).optional().describe(
|
|
1317
3525
|
"Updated target social platform. Distribution metadata only - does not trigger a re-render."
|
|
1318
3526
|
),
|
|
1319
|
-
publishTargets:
|
|
1320
|
-
targetDurationSeconds:
|
|
1321
|
-
voiceId:
|
|
3527
|
+
publishTargets: z6.array(z6.enum(PUBLISH_TARGETS)).max(9).optional().describe("Replacement allowed social destination list"),
|
|
3528
|
+
targetDurationSeconds: z6.number().int().min(5).max(600).optional().describe("Updated final video duration in seconds"),
|
|
3529
|
+
voiceId: z6.string().optional().describe(
|
|
1322
3530
|
"Updated TTS narrator voice ID. Triggers a re-render with the new voice."
|
|
1323
3531
|
),
|
|
1324
|
-
assetSlots:
|
|
3532
|
+
assetSlots: z6.array(assetSlotSchema).optional().describe(
|
|
1325
3533
|
"Replacement storyboard asset slots. Omit to preserve existing slots; pass [] to clear them."
|
|
1326
3534
|
),
|
|
1327
|
-
audioLayers:
|
|
3535
|
+
audioLayers: z6.array(briefAudioLayerSchema).optional().describe(
|
|
1328
3536
|
"Replacement typed audio layers. Omit to preserve existing layers; pass [] to clear them."
|
|
1329
3537
|
)
|
|
1330
3538
|
};
|
|
@@ -1361,7 +3569,7 @@ function verificationMatches(verifications, pattern) {
|
|
|
1361
3569
|
}
|
|
1362
3570
|
function buildRenderDiagnosticSnapshot(params) {
|
|
1363
3571
|
const failed = [params.jobStatus, params.videoState].find(
|
|
1364
|
-
(
|
|
3572
|
+
(result3) => !result3.ok
|
|
1365
3573
|
);
|
|
1366
3574
|
if (failed) return failed;
|
|
1367
3575
|
const status = record(params.jobStatus.body);
|
|
@@ -1432,165 +3640,165 @@ function buildRenderDiagnosticSnapshot(params) {
|
|
|
1432
3640
|
}
|
|
1433
3641
|
|
|
1434
3642
|
// src/design-action-schema.ts
|
|
1435
|
-
import { z as
|
|
1436
|
-
var id =
|
|
1437
|
-
var point =
|
|
1438
|
-
var polygonShape =
|
|
1439
|
-
kind:
|
|
1440
|
-
points:
|
|
3643
|
+
import { z as z7 } from "zod";
|
|
3644
|
+
var id = z7.string().min(1).max(120);
|
|
3645
|
+
var point = z7.object({ x: z7.number().min(0).max(1), y: z7.number().min(0).max(1) }).strict();
|
|
3646
|
+
var polygonShape = z7.object({
|
|
3647
|
+
kind: z7.literal("polygon"),
|
|
3648
|
+
points: z7.array(point).min(3).max(512)
|
|
1441
3649
|
}).strict();
|
|
1442
|
-
var rasterShape =
|
|
1443
|
-
kind:
|
|
1444
|
-
source:
|
|
3650
|
+
var rasterShape = z7.object({
|
|
3651
|
+
kind: z7.literal("raster"),
|
|
3652
|
+
source: z7.object({
|
|
1445
3653
|
assetId: id,
|
|
1446
|
-
expectedSha256:
|
|
1447
|
-
sourceRange:
|
|
1448
|
-
startFrame:
|
|
1449
|
-
endFrame:
|
|
3654
|
+
expectedSha256: z7.string().regex(/^[a-f0-9]{64}$/),
|
|
3655
|
+
sourceRange: z7.object({
|
|
3656
|
+
startFrame: z7.number().int().nonnegative(),
|
|
3657
|
+
endFrame: z7.number().int().positive()
|
|
1450
3658
|
}).strict().optional()
|
|
1451
3659
|
}).strict(),
|
|
1452
|
-
widthPx:
|
|
1453
|
-
heightPx:
|
|
3660
|
+
widthPx: z7.number().int().positive().max(8192),
|
|
3661
|
+
heightPx: z7.number().int().positive().max(8192)
|
|
1454
3662
|
}).strict();
|
|
1455
|
-
var shape =
|
|
1456
|
-
var mask =
|
|
3663
|
+
var shape = z7.discriminatedUnion("kind", [polygonShape, rasterShape]);
|
|
3664
|
+
var mask = z7.object({
|
|
1457
3665
|
id,
|
|
1458
3666
|
shape,
|
|
1459
|
-
operation:
|
|
1460
|
-
inverted:
|
|
1461
|
-
feather:
|
|
1462
|
-
allowedSupportMargin:
|
|
1463
|
-
reviewState:
|
|
1464
|
-
provenance:
|
|
1465
|
-
kind:
|
|
3667
|
+
operation: z7.enum(["add", "subtract"]),
|
|
3668
|
+
inverted: z7.boolean().default(false),
|
|
3669
|
+
feather: z7.number().finite().nonnegative().max(0.25).default(0),
|
|
3670
|
+
allowedSupportMargin: z7.number().finite().nonnegative().max(0.25).default(0),
|
|
3671
|
+
reviewState: z7.enum(["draft", "approved", "rejected"]),
|
|
3672
|
+
provenance: z7.object({
|
|
3673
|
+
kind: z7.enum(["manual", "imported", "model-proposed"]),
|
|
1466
3674
|
sourceAssetId: id.optional(),
|
|
1467
|
-
modelId:
|
|
3675
|
+
modelId: z7.string().min(1).max(160).optional()
|
|
1468
3676
|
}).strict()
|
|
1469
3677
|
}).strict();
|
|
1470
|
-
var transform =
|
|
1471
|
-
x:
|
|
1472
|
-
y:
|
|
1473
|
-
width:
|
|
1474
|
-
height:
|
|
1475
|
-
rotationDegrees:
|
|
1476
|
-
opacity:
|
|
3678
|
+
var transform = z7.object({
|
|
3679
|
+
x: z7.number().min(0).max(1),
|
|
3680
|
+
y: z7.number().min(0).max(1),
|
|
3681
|
+
width: z7.number().positive().max(1),
|
|
3682
|
+
height: z7.number().positive().max(1),
|
|
3683
|
+
rotationDegrees: z7.number().min(-360).max(360),
|
|
3684
|
+
opacity: z7.number().min(0).max(1)
|
|
1477
3685
|
}).strict();
|
|
1478
3686
|
var colourParameters = {
|
|
1479
|
-
exposure:
|
|
1480
|
-
contrast:
|
|
1481
|
-
saturation:
|
|
1482
|
-
temperature:
|
|
1483
|
-
tint:
|
|
3687
|
+
exposure: z7.number().finite().min(-2).max(2).default(0),
|
|
3688
|
+
contrast: z7.number().finite().min(0.5).max(2).default(1),
|
|
3689
|
+
saturation: z7.number().finite().min(0).max(2).default(1),
|
|
3690
|
+
temperature: z7.number().finite().min(-1).max(1).default(0),
|
|
3691
|
+
tint: z7.number().finite().min(-1).max(1).default(0)
|
|
1484
3692
|
};
|
|
1485
|
-
var effect =
|
|
3693
|
+
var effect = z7.object({
|
|
1486
3694
|
id,
|
|
1487
|
-
kind:
|
|
1488
|
-
version:
|
|
3695
|
+
kind: z7.literal("local-colour"),
|
|
3696
|
+
version: z7.literal(1),
|
|
1489
3697
|
maskId: id,
|
|
1490
3698
|
...colourParameters
|
|
1491
3699
|
}).strict();
|
|
1492
|
-
var motionPath =
|
|
3700
|
+
var motionPath = z7.object({
|
|
1493
3701
|
id,
|
|
1494
|
-
kind:
|
|
1495
|
-
startFrame:
|
|
1496
|
-
endFrame:
|
|
3702
|
+
kind: z7.literal("cubic-bezier"),
|
|
3703
|
+
startFrame: z7.number().int().nonnegative(),
|
|
3704
|
+
endFrame: z7.number().int().positive(),
|
|
1497
3705
|
start: point,
|
|
1498
3706
|
control1: point,
|
|
1499
3707
|
control2: point,
|
|
1500
3708
|
end: point,
|
|
1501
|
-
easing:
|
|
1502
|
-
orientToPath:
|
|
3709
|
+
easing: z7.enum(["linear", "ease-in", "ease-out", "ease-in-out"]),
|
|
3710
|
+
orientToPath: z7.boolean().default(false)
|
|
1503
3711
|
}).strict();
|
|
1504
|
-
var region =
|
|
3712
|
+
var region = z7.object({ id, shape, label: z7.string().min(1).max(160) }).strict();
|
|
1505
3713
|
var base = { actionId: id, sceneId: id };
|
|
1506
|
-
var vendoredDesignActionSchema =
|
|
1507
|
-
|
|
3714
|
+
var vendoredDesignActionSchema = z7.discriminatedUnion("kind", [
|
|
3715
|
+
z7.object({
|
|
1508
3716
|
...base,
|
|
1509
|
-
kind:
|
|
3717
|
+
kind: z7.literal("set-layer-transform"),
|
|
1510
3718
|
layerId: id,
|
|
1511
3719
|
transform
|
|
1512
3720
|
}).strict(),
|
|
1513
|
-
|
|
3721
|
+
z7.object({
|
|
1514
3722
|
...base,
|
|
1515
|
-
kind:
|
|
3723
|
+
kind: z7.literal("set-local-colour-effect"),
|
|
1516
3724
|
effectId: id,
|
|
1517
|
-
parameters:
|
|
3725
|
+
parameters: z7.object(colourParameters).strict()
|
|
1518
3726
|
}).strict(),
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
3727
|
+
z7.object({ ...base, kind: z7.literal("create-mask"), mask }).strict(),
|
|
3728
|
+
z7.object({ ...base, kind: z7.literal("set-mask"), maskId: id, mask }).strict(),
|
|
3729
|
+
z7.object({
|
|
1522
3730
|
...base,
|
|
1523
|
-
kind:
|
|
3731
|
+
kind: z7.literal("set-mask-review-state"),
|
|
1524
3732
|
maskId: id,
|
|
1525
|
-
reviewState:
|
|
3733
|
+
reviewState: z7.enum(["draft", "approved", "rejected"])
|
|
1526
3734
|
}).strict(),
|
|
1527
|
-
|
|
1528
|
-
|
|
3735
|
+
z7.object({ ...base, kind: z7.literal("delete-mask"), maskId: id }).strict(),
|
|
3736
|
+
z7.object({
|
|
1529
3737
|
...base,
|
|
1530
|
-
kind:
|
|
3738
|
+
kind: z7.literal("attach-effect"),
|
|
1531
3739
|
effect,
|
|
1532
|
-
layerIds:
|
|
3740
|
+
layerIds: z7.array(id).min(1).max(50)
|
|
1533
3741
|
}).strict(),
|
|
1534
|
-
|
|
1535
|
-
|
|
3742
|
+
z7.object({ ...base, kind: z7.literal("remove-effect"), effectId: id }).strict(),
|
|
3743
|
+
z7.object({
|
|
1536
3744
|
...base,
|
|
1537
|
-
kind:
|
|
3745
|
+
kind: z7.literal("set-layer-text"),
|
|
1538
3746
|
layerId: id,
|
|
1539
|
-
text:
|
|
3747
|
+
text: z7.string().max(1e4)
|
|
1540
3748
|
}).strict(),
|
|
1541
|
-
|
|
3749
|
+
z7.object({
|
|
1542
3750
|
...base,
|
|
1543
|
-
kind:
|
|
3751
|
+
kind: z7.literal("set-layer-visibility"),
|
|
1544
3752
|
layerId: id,
|
|
1545
|
-
visible:
|
|
3753
|
+
visible: z7.boolean()
|
|
1546
3754
|
}).strict(),
|
|
1547
|
-
|
|
3755
|
+
z7.object({
|
|
1548
3756
|
...base,
|
|
1549
|
-
kind:
|
|
3757
|
+
kind: z7.literal("set-layer-locked"),
|
|
1550
3758
|
layerId: id,
|
|
1551
|
-
locked:
|
|
3759
|
+
locked: z7.boolean()
|
|
1552
3760
|
}).strict(),
|
|
1553
|
-
|
|
3761
|
+
z7.object({
|
|
1554
3762
|
...base,
|
|
1555
|
-
kind:
|
|
3763
|
+
kind: z7.literal("set-layer-mask-ids"),
|
|
1556
3764
|
layerId: id,
|
|
1557
|
-
maskIds:
|
|
3765
|
+
maskIds: z7.array(id).max(16)
|
|
1558
3766
|
}).strict(),
|
|
1559
|
-
|
|
3767
|
+
z7.object({
|
|
1560
3768
|
...base,
|
|
1561
|
-
kind:
|
|
1562
|
-
orderedLayerIds:
|
|
3769
|
+
kind: z7.literal("set-layer-order"),
|
|
3770
|
+
orderedLayerIds: z7.array(id).min(1).max(500)
|
|
1563
3771
|
}).strict(),
|
|
1564
|
-
|
|
3772
|
+
z7.object({
|
|
1565
3773
|
...base,
|
|
1566
|
-
kind:
|
|
3774
|
+
kind: z7.literal("set-motion-path"),
|
|
1567
3775
|
path: motionPath,
|
|
1568
|
-
layerIds:
|
|
3776
|
+
layerIds: z7.array(id).max(50).default([])
|
|
1569
3777
|
}).strict(),
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
3778
|
+
z7.object({ ...base, kind: z7.literal("remove-motion-path"), pathId: id }).strict(),
|
|
3779
|
+
z7.object({ ...base, kind: z7.literal("set-protected-region"), region }).strict(),
|
|
3780
|
+
z7.object({
|
|
1573
3781
|
...base,
|
|
1574
|
-
kind:
|
|
3782
|
+
kind: z7.literal("remove-protected-region"),
|
|
1575
3783
|
regionId: id
|
|
1576
3784
|
}).strict()
|
|
1577
3785
|
]);
|
|
1578
|
-
var inspectDesignDocumentToolSchema =
|
|
1579
|
-
projectId:
|
|
1580
|
-
documentId:
|
|
1581
|
-
includeVersions:
|
|
3786
|
+
var inspectDesignDocumentToolSchema = z7.object({
|
|
3787
|
+
projectId: z7.string().uuid(),
|
|
3788
|
+
documentId: z7.string().uuid(),
|
|
3789
|
+
includeVersions: z7.boolean().default(true)
|
|
1582
3790
|
}).strict();
|
|
1583
|
-
var applyDesignActionsToolSchema =
|
|
1584
|
-
projectId:
|
|
1585
|
-
documentId:
|
|
1586
|
-
actionGroupId:
|
|
1587
|
-
expectedVersion:
|
|
1588
|
-
actions:
|
|
1589
|
-
clientCreatedAt:
|
|
3791
|
+
var applyDesignActionsToolSchema = z7.object({
|
|
3792
|
+
projectId: z7.string().uuid(),
|
|
3793
|
+
documentId: z7.string().uuid(),
|
|
3794
|
+
actionGroupId: z7.string().min(1).max(120),
|
|
3795
|
+
expectedVersion: z7.number().int().positive(),
|
|
3796
|
+
actions: z7.array(vendoredDesignActionSchema).min(1).max(50),
|
|
3797
|
+
clientCreatedAt: z7.string().datetime()
|
|
1590
3798
|
}).strict();
|
|
1591
3799
|
|
|
1592
3800
|
// src/tools.ts
|
|
1593
|
-
var uuid = (desc) =>
|
|
3801
|
+
var uuid = (desc) => z8.string().uuid().describe(desc);
|
|
1594
3802
|
var READ_ONLY = {
|
|
1595
3803
|
readOnlyHint: true,
|
|
1596
3804
|
destructiveHint: false,
|
|
@@ -1609,9 +3817,9 @@ function registerTools(server) {
|
|
|
1609
3817
|
{
|
|
1610
3818
|
title: "List jobs",
|
|
1611
3819
|
description: "List recent video jobs for the current organization (newest first). Start here to discover job IDs; for one job's full detail use get_job_status.",
|
|
1612
|
-
inputSchema: {
|
|
1613
|
-
limit:
|
|
1614
|
-
status:
|
|
3820
|
+
inputSchema: z8.object({
|
|
3821
|
+
limit: z8.number().int().min(1).max(50).default(10).describe("Number of jobs to return (default 10, max 50)"),
|
|
3822
|
+
status: z8.enum([
|
|
1615
3823
|
"queued",
|
|
1616
3824
|
"rendering",
|
|
1617
3825
|
"draft_ready",
|
|
@@ -1621,7 +3829,7 @@ function registerTools(server) {
|
|
|
1621
3829
|
"published",
|
|
1622
3830
|
"failed"
|
|
1623
3831
|
]).optional().describe("Filter by job status")
|
|
1624
|
-
},
|
|
3832
|
+
}).strict(),
|
|
1625
3833
|
annotations: READ_ONLY
|
|
1626
3834
|
},
|
|
1627
3835
|
async ({ limit, status }) => {
|
|
@@ -1635,7 +3843,7 @@ function registerTools(server) {
|
|
|
1635
3843
|
{
|
|
1636
3844
|
title: "Get job status",
|
|
1637
3845
|
description: "Get detailed status for a job: runs, artifacts, verifications, plus the current video and thumbnail URLs. The main polling target after create_video_brief or edit_video_brief; for post-publish engagement numbers use get_job_metrics instead.",
|
|
1638
|
-
inputSchema: { jobId: uuid("The job ID to check") },
|
|
3846
|
+
inputSchema: z8.object({ jobId: uuid("The job ID to check") }).strict(),
|
|
1639
3847
|
annotations: READ_ONLY
|
|
1640
3848
|
},
|
|
1641
3849
|
async ({ jobId }) => runTool(() => apiGet(`/api/v1/jobs/${encodeURIComponent(jobId)}`))
|
|
@@ -1645,7 +3853,7 @@ function registerTools(server) {
|
|
|
1645
3853
|
{
|
|
1646
3854
|
title: "Get video state",
|
|
1647
3855
|
description: "Get the materialized video state for a job (composition decisions: scenes, duration, aspect ratio, etc.). Use it to inspect what the video contains; for pipeline progress and video URLs use get_job_status.",
|
|
1648
|
-
inputSchema: { jobId: uuid("The job ID") },
|
|
3856
|
+
inputSchema: z8.object({ jobId: uuid("The job ID") }).strict(),
|
|
1649
3857
|
annotations: READ_ONLY
|
|
1650
3858
|
},
|
|
1651
3859
|
async ({ jobId }) => runTool(
|
|
@@ -1657,7 +3865,7 @@ function registerTools(server) {
|
|
|
1657
3865
|
{
|
|
1658
3866
|
title: "Diagnose render",
|
|
1659
3867
|
description: "Build one read-only troubleshooting snapshot from the current job status and video state. Reports run failures, artifact binding, duration evidence, retry recommendations, rights checks, and whether blank-frame or placeholder verification evidence exists. It does not visually inspect the video.",
|
|
1660
|
-
inputSchema: { jobId: uuid("The job ID to diagnose") },
|
|
3868
|
+
inputSchema: z8.object({ jobId: uuid("The job ID to diagnose") }).strict(),
|
|
1661
3869
|
annotations: READ_ONLY
|
|
1662
3870
|
},
|
|
1663
3871
|
async ({ jobId }) => runTool(async () => {
|
|
@@ -1674,10 +3882,10 @@ function registerTools(server) {
|
|
|
1674
3882
|
{
|
|
1675
3883
|
title: "Get job metrics",
|
|
1676
3884
|
description: "Get the latest platform engagement metric snapshots for a published job (newest first). Read-only. Only meaningful after publish_job; for render progress use get_job_status.",
|
|
1677
|
-
inputSchema: {
|
|
3885
|
+
inputSchema: z8.object({
|
|
1678
3886
|
jobId: uuid("The job ID"),
|
|
1679
|
-
limit:
|
|
1680
|
-
},
|
|
3887
|
+
limit: z8.number().int().min(1).max(20).default(10).describe("Max metric snapshots (default 10, max 20)")
|
|
3888
|
+
}).strict(),
|
|
1681
3889
|
annotations: READ_ONLY
|
|
1682
3890
|
},
|
|
1683
3891
|
async ({ jobId, limit }) => runTool(
|
|
@@ -1691,12 +3899,12 @@ function registerTools(server) {
|
|
|
1691
3899
|
{
|
|
1692
3900
|
title: "Get cost summary",
|
|
1693
3901
|
description: "Query production infrastructure costs (Lambda, RunPod, R2, WaveSpeed, PoYo), optionally with reconciliation drift. This is spend data, not engagement \u2014 for a published job's audience numbers use get_job_metrics.",
|
|
1694
|
-
inputSchema: {
|
|
1695
|
-
since:
|
|
1696
|
-
provider:
|
|
1697
|
-
groupBy:
|
|
1698
|
-
includeReconciliation:
|
|
1699
|
-
},
|
|
3902
|
+
inputSchema: z8.object({
|
|
3903
|
+
since: z8.string().optional().describe("ISO date \u2014 only include events after this date"),
|
|
3904
|
+
provider: z8.enum(["runpod", "lambda", "r2", "wavespeed", "poyo", "all"]).default("all").describe("Filter by provider"),
|
|
3905
|
+
groupBy: z8.enum(["provider", "job", "day"]).default("provider").describe("How to aggregate the summary"),
|
|
3906
|
+
includeReconciliation: z8.boolean().default(false).describe("Include account-wide reconciliation events")
|
|
3907
|
+
}).strict(),
|
|
1700
3908
|
annotations: READ_ONLY
|
|
1701
3909
|
},
|
|
1702
3910
|
async ({ since, provider, groupBy, includeReconciliation }) => {
|
|
@@ -1711,7 +3919,7 @@ function registerTools(server) {
|
|
|
1711
3919
|
{
|
|
1712
3920
|
title: "Inspect workflow",
|
|
1713
3921
|
description: "Inspect the current Studio workflow and its version history for a job.",
|
|
1714
|
-
inputSchema:
|
|
3922
|
+
inputSchema: z8.object({ jobId: uuid("The job ID") }).strict(),
|
|
1715
3923
|
annotations: READ_ONLY
|
|
1716
3924
|
},
|
|
1717
3925
|
async ({ jobId }) => runTool(
|
|
@@ -1760,7 +3968,10 @@ function registerTools(server) {
|
|
|
1760
3968
|
{
|
|
1761
3969
|
title: "Create video brief",
|
|
1762
3970
|
description: "Create a new video job from a structured brief and trigger the render pipeline. Returns the job and run IDs immediately; rendering continues asynchronously (typically a few minutes) \u2014 poll get_job_status until draft_ready. Requires a key with write scope. To change an existing job, use edit_video_brief instead of creating a duplicate.",
|
|
1763
|
-
|
|
3971
|
+
// Strict like the remote Studio tool: an unsupported field such as a
|
|
3972
|
+
// framework choice is rejected instead of being stripped and rendered
|
|
3973
|
+
// with the default renderer.
|
|
3974
|
+
inputSchema: z8.object(createBriefShape).strict(),
|
|
1764
3975
|
annotations: SAFE_WRITE
|
|
1765
3976
|
},
|
|
1766
3977
|
async (input) => runTool(() => apiPost(`/api/v1/jobs`, input))
|
|
@@ -1770,7 +3981,7 @@ function registerTools(server) {
|
|
|
1770
3981
|
{
|
|
1771
3982
|
title: "Edit video brief",
|
|
1772
3983
|
description: "Edit an existing job's brief (only provided fields change). Changing the script, caption, title, lessonTitle, objective, steps, scenes, aspectRatio or voiceId triggers a re-render and returns the new run ID; objective also re-routes the composition. Changing only publishTarget updates the job's target platform without re-rendering. For a brand-new video use create_video_brief.",
|
|
1773
|
-
inputSchema: editBriefShape,
|
|
3984
|
+
inputSchema: z8.object(editBriefShape).strict(),
|
|
1774
3985
|
annotations: SAFE_WRITE
|
|
1775
3986
|
},
|
|
1776
3987
|
async (input) => {
|
|
@@ -1791,10 +4002,10 @@ function registerTools(server) {
|
|
|
1791
4002
|
{
|
|
1792
4003
|
title: "Draft workflow",
|
|
1793
4004
|
description: "Create a durable Documentary Journey or Transformation Montage workflow version. Inspect first and pass its current version, or 0 only when no workflow exists.",
|
|
1794
|
-
inputSchema:
|
|
4005
|
+
inputSchema: z8.object({
|
|
1795
4006
|
jobId: uuid("The job ID"),
|
|
1796
|
-
expectedVersion:
|
|
1797
|
-
document:
|
|
4007
|
+
expectedVersion: z8.number().int().nonnegative().describe("Use 0 when no workflow exists yet"),
|
|
4008
|
+
document: z8.unknown().describe("The complete Studio workflow document")
|
|
1798
4009
|
}).strict(),
|
|
1799
4010
|
annotations: SAFE_WRITE
|
|
1800
4011
|
},
|
|
@@ -1805,10 +4016,10 @@ function registerTools(server) {
|
|
|
1805
4016
|
{
|
|
1806
4017
|
title: "Patch workflow",
|
|
1807
4018
|
description: "Create a new workflow version by recursively merging a patch into the current version.",
|
|
1808
|
-
inputSchema:
|
|
4019
|
+
inputSchema: z8.object({
|
|
1809
4020
|
jobId: uuid("The job ID"),
|
|
1810
|
-
expectedVersion:
|
|
1811
|
-
patch:
|
|
4021
|
+
expectedVersion: z8.number().int().positive(),
|
|
4022
|
+
patch: z8.record(z8.string(), z8.unknown())
|
|
1812
4023
|
}).strict(),
|
|
1813
4024
|
annotations: SAFE_WRITE
|
|
1814
4025
|
},
|
|
@@ -1819,9 +4030,9 @@ function registerTools(server) {
|
|
|
1819
4030
|
{
|
|
1820
4031
|
title: "Validate workflow",
|
|
1821
4032
|
description: "Validate and compile the inspected workflow version without rendering or publishing it.",
|
|
1822
|
-
inputSchema:
|
|
4033
|
+
inputSchema: z8.object({
|
|
1823
4034
|
jobId: uuid("The job ID"),
|
|
1824
|
-
expectedVersion:
|
|
4035
|
+
expectedVersion: z8.number().int().positive()
|
|
1825
4036
|
}).strict(),
|
|
1826
4037
|
annotations: SAFE_WRITE
|
|
1827
4038
|
},
|
|
@@ -1832,7 +4043,7 @@ function registerTools(server) {
|
|
|
1832
4043
|
{
|
|
1833
4044
|
title: "Preview workflow",
|
|
1834
4045
|
description: "Return the current workflow's compiled UniversalVideo brief for a safe pre-render preview.",
|
|
1835
|
-
inputSchema:
|
|
4046
|
+
inputSchema: z8.object({ jobId: uuid("The job ID") }).strict(),
|
|
1836
4047
|
annotations: READ_ONLY
|
|
1837
4048
|
},
|
|
1838
4049
|
async ({ jobId }) => runTool(
|
|
@@ -1846,10 +4057,10 @@ function registerTools(server) {
|
|
|
1846
4057
|
{
|
|
1847
4058
|
title: "Compare workflow versions",
|
|
1848
4059
|
description: "Compare two durable workflow versions field by field.",
|
|
1849
|
-
inputSchema:
|
|
4060
|
+
inputSchema: z8.object({
|
|
1850
4061
|
jobId: uuid("The job ID"),
|
|
1851
|
-
fromVersion:
|
|
1852
|
-
toVersion:
|
|
4062
|
+
fromVersion: z8.number().int().positive(),
|
|
4063
|
+
toVersion: z8.number().int().positive()
|
|
1853
4064
|
}).strict(),
|
|
1854
4065
|
annotations: READ_ONLY
|
|
1855
4066
|
},
|
|
@@ -1873,9 +4084,9 @@ function registerTools(server) {
|
|
|
1873
4084
|
{
|
|
1874
4085
|
title: "Approve workflow",
|
|
1875
4086
|
description: "Validate and approve the inspected workflow version. This does not approve or publish the video job.",
|
|
1876
|
-
inputSchema:
|
|
4087
|
+
inputSchema: z8.object({
|
|
1877
4088
|
jobId: uuid("The job ID"),
|
|
1878
|
-
expectedVersion:
|
|
4089
|
+
expectedVersion: z8.number().int().positive()
|
|
1879
4090
|
}).strict(),
|
|
1880
4091
|
annotations: {
|
|
1881
4092
|
readOnlyHint: false,
|
|
@@ -1891,11 +4102,11 @@ function registerTools(server) {
|
|
|
1891
4102
|
{
|
|
1892
4103
|
title: "Retry video QA",
|
|
1893
4104
|
description: "Retry the default QA evaluator against the same current frozen video. Use only when get_job_status recommends retryTarget qa_only, or when QA is missing, unavailable, or parse-degraded. This does not regenerate media and cannot clear publish gates by itself.",
|
|
1894
|
-
inputSchema: {
|
|
4105
|
+
inputSchema: z8.object({
|
|
1895
4106
|
jobId: uuid("The current job ID"),
|
|
1896
4107
|
runId: uuid("The current render run ID"),
|
|
1897
4108
|
videoArtifactId: uuid("The current frozen video artifact ID")
|
|
1898
|
-
},
|
|
4109
|
+
}).strict(),
|
|
1899
4110
|
annotations: SAFE_WRITE
|
|
1900
4111
|
},
|
|
1901
4112
|
async ({ jobId, runId, videoArtifactId }) => runTool(
|
|
@@ -1910,11 +4121,11 @@ function registerTools(server) {
|
|
|
1910
4121
|
{
|
|
1911
4122
|
title: "Audit video QA",
|
|
1912
4123
|
description: "Run Qwen3.6-Plus as an advisory second evaluator against the same current frozen video. Use only on an explicit request for another model, second opinion, or evaluator escalation. The result is separate from default QA and has no publish-gate effect.",
|
|
1913
|
-
inputSchema: {
|
|
4124
|
+
inputSchema: z8.object({
|
|
1914
4125
|
jobId: uuid("The current job ID"),
|
|
1915
4126
|
runId: uuid("The current render run ID"),
|
|
1916
4127
|
videoArtifactId: uuid("The current frozen video artifact ID")
|
|
1917
|
-
},
|
|
4128
|
+
}).strict(),
|
|
1918
4129
|
annotations: SAFE_WRITE
|
|
1919
4130
|
},
|
|
1920
4131
|
async ({ jobId, runId, videoArtifactId }) => runTool(
|
|
@@ -1929,11 +4140,11 @@ function registerTools(server) {
|
|
|
1929
4140
|
{
|
|
1930
4141
|
title: "Regenerate narration",
|
|
1931
4142
|
description: "Regenerate narration from the unchanged durable brief and then regenerate its dependent video in a paid adaptation run. Use only on an explicit user request or when get_job_status recommends retryTarget tts. This is an audio_and_video cascade, consumes rerender quota, and requires fresh QA.",
|
|
1932
|
-
inputSchema: {
|
|
4143
|
+
inputSchema: z8.object({
|
|
1933
4144
|
jobId: uuid("The current job ID"),
|
|
1934
4145
|
sourceRunId: uuid("The current run ID"),
|
|
1935
|
-
trigger:
|
|
1936
|
-
},
|
|
4146
|
+
trigger: z8.enum(["user_requested", "verifier_recommendation"])
|
|
4147
|
+
}).strict(),
|
|
1937
4148
|
annotations: SAFE_WRITE
|
|
1938
4149
|
},
|
|
1939
4150
|
async ({ jobId, sourceRunId, trigger }) => runTool(
|
|
@@ -1948,7 +4159,7 @@ function registerTools(server) {
|
|
|
1948
4159
|
{
|
|
1949
4160
|
title: "Approve job",
|
|
1950
4161
|
description: "Approve a rendered job so it becomes publishable (draft_ready/awaiting_review/needs_changes -> approved) and revoke active review links. Required before publish_job will accept the job.",
|
|
1951
|
-
inputSchema: { jobId: uuid("The job ID to approve") },
|
|
4162
|
+
inputSchema: z8.object({ jobId: uuid("The job ID to approve") }).strict(),
|
|
1952
4163
|
annotations: {
|
|
1953
4164
|
readOnlyHint: false,
|
|
1954
4165
|
// Approval revokes active review links, so this is not purely additive.
|
|
@@ -1967,28 +4178,30 @@ function registerTools(server) {
|
|
|
1967
4178
|
{
|
|
1968
4179
|
title: "Publish job",
|
|
1969
4180
|
description: "Publish an APPROVED job to its connected social destination via the gated publish API. All production gates run (paid plan, ownership, abuse, QA, disclosure, destination, rights, moderation). Requires a key with publish scope; the post goes live on the external platform and cannot be recalled by the API. For YouTube pass youtubePrivacyStatus:'private' for safe runs.",
|
|
1970
|
-
inputSchema: {
|
|
4181
|
+
inputSchema: z8.object({
|
|
1971
4182
|
jobId: uuid("The approved job to publish"),
|
|
1972
|
-
xPreviewArtifactId:
|
|
1973
|
-
xCaption:
|
|
1974
|
-
xMadeWithAi:
|
|
1975
|
-
confirmXPublicPost:
|
|
1976
|
-
|
|
4183
|
+
xPreviewArtifactId: z8.string().min(1).optional().describe("X only. Exact reviewed video artifact ID."),
|
|
4184
|
+
xCaption: z8.string().trim().min(1).max(2e3).optional().describe("X only. Exact post text approved by the user."),
|
|
4185
|
+
xMadeWithAi: z8.boolean().optional().describe("X only. Enable the Made with AI disclosure."),
|
|
4186
|
+
confirmXPublicPost: z8.boolean().optional().describe(
|
|
4187
|
+
"True only after the user authorizes this public X post."
|
|
4188
|
+
),
|
|
4189
|
+
confirmXAutomationPolicy: z8.boolean().optional().describe(
|
|
1977
4190
|
"True only after the user confirms the X automation policy."
|
|
1978
4191
|
),
|
|
1979
|
-
confirmXOriginalContent:
|
|
4192
|
+
confirmXOriginalContent: z8.boolean().optional().describe(
|
|
1980
4193
|
"True only after the user confirms ownership or publishing rights."
|
|
1981
4194
|
),
|
|
1982
|
-
youtubePrivacyStatus:
|
|
1983
|
-
tiktokPostMode:
|
|
1984
|
-
connectedAccountDestinationId:
|
|
4195
|
+
youtubePrivacyStatus: z8.enum(["private", "unlisted", "public"]).optional().describe("YouTube only. Use 'private' for safe dogfood runs."),
|
|
4196
|
+
tiktokPostMode: z8.enum(["direct_publish", "draft_upload"]).optional().describe("TikTok only. Use 'draft_upload' for safe dogfood runs."),
|
|
4197
|
+
connectedAccountDestinationId: z8.string().uuid().optional().describe(
|
|
1985
4198
|
"Specific destination when the org has multiple accounts for the provider."
|
|
1986
4199
|
),
|
|
1987
|
-
confirmSyntheticDisclosure:
|
|
4200
|
+
confirmSyntheticDisclosure: z8.boolean().optional().describe(
|
|
1988
4201
|
"Set true when the job requires an AI synthetic-media disclosure."
|
|
1989
4202
|
),
|
|
1990
|
-
confirmQaOverride:
|
|
1991
|
-
},
|
|
4203
|
+
confirmQaOverride: z8.boolean().optional().describe("Set true to override a non-passing QA gate.")
|
|
4204
|
+
}).strict(),
|
|
1992
4205
|
annotations: {
|
|
1993
4206
|
readOnlyHint: false,
|
|
1994
4207
|
// A live post on an external platform cannot be recalled by the API.
|
|
@@ -2009,8 +4222,8 @@ function registerTools(server) {
|
|
|
2009
4222
|
{
|
|
2010
4223
|
title: "Connect social account",
|
|
2011
4224
|
description: "Start connecting a social account headlessly (no local browser needed). Returns an approve URL + a short confirm code: open the URL in a browser signed in to Instavar, check the code matches THIS terminal, and approve. Then poll connect_account_status with the returned pairingId until it reports 'connected'.",
|
|
2012
|
-
inputSchema: {
|
|
2013
|
-
provider:
|
|
4225
|
+
inputSchema: z8.object({
|
|
4226
|
+
provider: z8.enum([
|
|
2014
4227
|
"youtube",
|
|
2015
4228
|
"x",
|
|
2016
4229
|
"tiktok",
|
|
@@ -2019,7 +4232,7 @@ function registerTools(server) {
|
|
|
2019
4232
|
"instagram",
|
|
2020
4233
|
"threads"
|
|
2021
4234
|
]).describe("The social platform to connect")
|
|
2022
|
-
},
|
|
4235
|
+
}).strict(),
|
|
2023
4236
|
annotations: {
|
|
2024
4237
|
readOnlyHint: false,
|
|
2025
4238
|
destructiveHint: false,
|
|
@@ -2035,9 +4248,9 @@ function registerTools(server) {
|
|
|
2035
4248
|
{
|
|
2036
4249
|
title: "Check account connection",
|
|
2037
4250
|
description: "Poll a pending account connection started by connect_account. Returns status 'pending' | 'connected' | 'expired', plus the connected account label once connected.",
|
|
2038
|
-
inputSchema: {
|
|
4251
|
+
inputSchema: z8.object({
|
|
2039
4252
|
pairingId: uuid("The pairingId returned by connect_account")
|
|
2040
|
-
},
|
|
4253
|
+
}).strict(),
|
|
2041
4254
|
annotations: READ_ONLY
|
|
2042
4255
|
},
|
|
2043
4256
|
async ({ pairingId }) => runTool(
|
|
@@ -2059,9 +4272,9 @@ function registerConfiguredTools(server, apiKey = process.env.INSTAVAR_API_KEY)
|
|
|
2059
4272
|
// package.json
|
|
2060
4273
|
var package_default = {
|
|
2061
4274
|
name: "@instavar/mcp-server",
|
|
2062
|
-
version: "0.
|
|
4275
|
+
version: "0.7.0",
|
|
2063
4276
|
mcpName: "com.instavar/mcp-server",
|
|
2064
|
-
description: "Make local videos with Instavar Remotion
|
|
4277
|
+
description: "Make local videos with Instavar templates for Remotion or HyperFrames for free, then add an API key for hosted Studio workflows.",
|
|
2065
4278
|
type: "module",
|
|
2066
4279
|
license: "MIT",
|
|
2067
4280
|
homepage: "https://instavar.com",
|
|
@@ -2077,6 +4290,8 @@ var package_default = {
|
|
|
2077
4290
|
"model-context-protocol",
|
|
2078
4291
|
"instavar",
|
|
2079
4292
|
"video",
|
|
4293
|
+
"remotion",
|
|
4294
|
+
"hyperframes",
|
|
2080
4295
|
"claude",
|
|
2081
4296
|
"codex"
|
|
2082
4297
|
],
|
|
@@ -2136,7 +4351,7 @@ async function main() {
|
|
|
2136
4351
|
const server = new McpServer(
|
|
2137
4352
|
{ name: "instavar", version: VERSION },
|
|
2138
4353
|
{
|
|
2139
|
-
instructions:
|
|
4354
|
+
instructions: pairedAnnouncementInstructions(pairedTemplatesEnabled()) + (pairedTemplatesEnabled() ? " " + PAIRED_TEMPLATE_INSTRUCTIONS : "") + " Before drafting concise carousel or video copy, call get_creative_guide for visual-copy. Use the carousel-stories, visual-design and human-review guides as relevant. Apply them to the user brief; the guides do not generate, render, save or publish media. For an ordinary video request, call propose_video_storyboard with the user's words and an aspect, passing accepted title, body and CTA as separate fields, and show the three beats before scaffolding. The storyboard contains visual briefs, not generated images. Use scaffold_video_spec only after the user accepts or refines the direction, preserving supplied body and CTA. For a carousel, choose a structure with list_carousel_templates and build it with scaffold_carousel_deck from accepted copy."
|
|
2140
4355
|
}
|
|
2141
4356
|
);
|
|
2142
4357
|
registerConfiguredTools(server);
|