@instavar/mcp-server 0.5.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.
Files changed (3) hide show
  1. package/README.md +62 -18
  2. package/dist/index.js +3131 -597
  3. package/package.json +4 -2
package/dist/index.js CHANGED
@@ -1,11 +1,1445 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/paired-announcement.ts
4
+ import { z } from "zod";
5
+
6
+ // src/announcement-source.ts
7
+ var htmlEscape = (value) => value.replace(
8
+ /[&<>"']/g,
9
+ (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]
10
+ );
11
+ function announcementFiles(project) {
12
+ const copy = project.copy;
13
+ const files = {};
14
+ const addFile = (name, contents) => {
15
+ files[name] = contents;
16
+ };
17
+ const css = `@font-face{font-family:AnnouncementDisplay;src:url(__ANNOUNCEMENT_DISPLAY_FONT__);font-weight:100 900;}
18
+ @font-face{font-family:AnnouncementBody;src:url(__ANNOUNCEMENT_BODY_FONT__);font-weight:400;}
19
+ *{box-sizing:border-box}body{margin:0;background:#f4f0e7;color:#202d29}
20
+ #announcement-root{position:relative;width:1280px;height:720px;overflow:hidden;background:#f4f0e7;color:#202d29}
21
+ .clip{position:absolute;inset:0}
22
+ h1,p{margin:0;overflow-wrap:anywhere}h1{position:absolute;left:88px;top:110px;width:1040px;font-family:AnnouncementDisplay;font-size:${copy.title.length > 45 ? 48 : copy.title.length > 24 ? 64 : 92}px;font-weight:650;line-height:1.08;letter-spacing:-3px}
23
+ #body{position:absolute;left:92px;top:370px;width:1050px;font-family:AnnouncementBody;font-size:${copy.body.length > 60 ? 28 : 34}px;font-weight:400;line-height:1.4}
24
+ #cta{position:absolute;left:92px;top:535px;width:1050px;font-family:AnnouncementBody;font-size:${copy.cta.length > 30 ? 26 : 30}px;font-weight:400;line-height:1.4;color:#345a45}
25
+ #rule{position:absolute;left:92px;top:620px;width:1096px;height:4px;background:#345a45;transform-origin:left center}
26
+ html,body,*{text-rendering:auto}
27
+ `;
28
+ addFile("style.css", css);
29
+ const manifest = {
30
+ framework: project.framework,
31
+ copy,
32
+ width: 1280,
33
+ height: 720,
34
+ fps: 30,
35
+ durationSeconds: 10
36
+ };
37
+ addFile("manifest.json", JSON.stringify(manifest, null, 2) + "\n");
38
+ if (project.framework === "hyperframes") {
39
+ addFile(
40
+ "hyperframes.json",
41
+ JSON.stringify({ authoringSkill: "general-video" })
42
+ );
43
+ addFile(
44
+ "index.html",
45
+ `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Instavar announcement card</title><link rel="stylesheet" href="style.css"><script src="gsap.min.js"></script></head><body>
46
+ <div id="announcement-root" data-composition-id="announcement" data-start="0" data-width="1280" data-height="720" data-duration="10">
47
+ <section id="announcement-scene" class="clip" data-start="0" data-duration="10" data-track-index="0">
48
+ <h1 id="title">${htmlEscape(copy.title)}</h1><p id="body">${htmlEscape(
49
+ copy.body
50
+ )}</p><p id="cta">${htmlEscape(
51
+ copy.cta
52
+ )}</p><div id="rule"></div></section></div>
53
+ <script>
54
+ // Adapted from HyperFrames registry staggered-fade-up: three whole text blocks.
55
+ const tl=gsap.timeline({paused:true});
56
+ tl.fromTo('#title',{opacity:0,y:22},{opacity:1,y:0,duration:0.6,ease:'power3.out'},0.25);
57
+ tl.fromTo('#body',{opacity:0,y:22},{opacity:1,y:0,duration:0.6,ease:'power3.out'},2);
58
+ tl.fromTo('#cta',{opacity:0,y:22},{opacity:1,y:0,duration:0.6,ease:'power3.out'},5);
59
+ tl.fromTo('#rule',{scaleX:0},{scaleX:1,duration:9.5,ease:'none'},0.25);
60
+ window.__timelines['announcement']=tl;
61
+ </script></body></html>`
62
+ );
63
+ addFile(
64
+ "index.motion.json",
65
+ JSON.stringify(
66
+ {
67
+ duration: 10,
68
+ assertions: [
69
+ { kind: "appearsBy", selector: "#title", bySec: 1 },
70
+ { kind: "before", a: "#title", b: "#body" },
71
+ { kind: "before", a: "#body", b: "#cta" },
72
+ { kind: "staysInFrame", selector: "#title" },
73
+ { kind: "staysInFrame", selector: "#body" },
74
+ { kind: "staysInFrame", selector: "#cta" }
75
+ ]
76
+ },
77
+ null,
78
+ 2
79
+ )
80
+ );
81
+ } else {
82
+ addFile(
83
+ "index.jsx",
84
+ `import React from 'react';
85
+ import {Composition,registerRoot,useCurrentFrame,useVideoConfig} from 'remotion';
86
+ const css=${JSON.stringify(css)};
87
+ const copy=${JSON.stringify(copy)};
88
+ const ease=t=>1-Math.pow(1-t,4);
89
+ const reveal=(t,start)=>{const p=ease(Math.max(0,Math.min(1,(t-start)/0.6)));return {opacity:p,transform:'translateY('+(22*(1-p))+'px)'}};
90
+ function Announcement(){const frame=useCurrentFrame();const {fps}=useVideoConfig();const t=frame/fps;return <div id="announcement-root"><style>{css}</style><section className="clip"><h1 id="title" style={reveal(t,0.25)}>{copy.title}</h1><p id="body" style={reveal(t,2)}>{copy.body}</p><p id="cta" style={reveal(t,5)}>{copy.cta}</p><div id="rule" style={{transform:'scaleX('+Math.max(0,Math.min(1,(t-0.25)/9.5))+')'}}/></section></div>}
91
+ registerRoot(()=> <Composition id="Announcement" component={Announcement} width={1280} height={720} fps={30} durationInFrames={300}/>);
92
+ `
93
+ );
94
+ }
95
+ const dependencies = project.framework === "hyperframes" ? { hyperframes: "0.8.38", gsap: "3.14.2" } : {
96
+ remotion: "4.0.496",
97
+ "@remotion/cli": "4.0.496",
98
+ react: "19.1.0",
99
+ "react-dom": "19.1.0"
100
+ };
101
+ addFile(
102
+ "package.json",
103
+ JSON.stringify(
104
+ {
105
+ private: true,
106
+ type: "module",
107
+ scripts: {
108
+ setup: "node setup.mjs",
109
+ preview: project.framework === "hyperframes" ? "hyperframes preview" : "remotion studio index.jsx",
110
+ render: project.framework === "hyperframes" ? "hyperframes render --fps 30 --output video.mp4" : "remotion render index.jsx Announcement video.mp4"
111
+ },
112
+ dependencies
113
+ },
114
+ null,
115
+ 2
116
+ )
117
+ );
118
+ addFile(
119
+ "setup.mjs",
120
+ `import {readFile,writeFile,copyFile} from 'node:fs/promises';
121
+ const fonts = [['__ANNOUNCEMENT_DISPLAY_FONT__','Fraunces-Variable.ttf'],['__ANNOUNCEMENT_BODY_FONT__','Inter-Regular.ttf']];
122
+ for (const [marker,name] of fonts) {
123
+ const response = await fetch('https://instavar.com/fonts/'+name);
124
+ if (!response.ok) throw new Error('Font download failed: '+name);
125
+ const uri = 'data:font/ttf;base64,'+Buffer.from(await response.arrayBuffer()).toString('base64');
126
+ for (const file of ${JSON.stringify(
127
+ project.framework === "hyperframes" ? ["style.css"] : ["style.css", "index.jsx"]
128
+ )}) {
129
+ const source = await readFile(file,'utf8');
130
+ await writeFile(file,source.replaceAll(marker,uri));
131
+ }
132
+ }
133
+ ${project.framework === "hyperframes" ? "await copyFile('node_modules/gsap/dist/gsap.min.js','gsap.min.js');" : ""}
134
+ `
135
+ );
136
+ return files;
137
+ }
138
+
139
+ // src/paired-announcement.ts
140
+ var framework = z.enum(["remotion", "hyperframes"]);
141
+ var text = (max) => z.string().trim().min(1).max(max);
142
+ var copyFields = { title: text(70), body: text(100), cta: text(40) };
143
+ var copySchema = z.object(copyFields).strict();
144
+ var announcementProjectSchema = z.object({
145
+ templateId: z.literal("announcement-card-v1"),
146
+ framework,
147
+ revision: z.number().int().min(1).max(Number.MAX_SAFE_INTEGER - 1),
148
+ copy: copySchema
149
+ }).strict();
150
+ var pairedAnnouncementInput = {
151
+ framework: z.enum(["remotion", "hyperframes", "no-preference"]).optional().describe(
152
+ "Only supply an explicit user choice. Omit for an ambiguous request or to inherit the existing project framework."
153
+ ),
154
+ copy: copySchema.optional().describe("Accepted copy for a new project only."),
155
+ project: announcementProjectSchema.optional().describe(
156
+ "The latest project document returned by this tool. Caller-held, not a saved hosted project."
157
+ ),
158
+ expectedRevision: z.number().int().min(1).optional().describe(
159
+ "Required with project. Must match its revision. This checks the supplied document, not a server-side stored version."
160
+ ),
161
+ edits: copySchema.partial().optional().describe(
162
+ "Explicit copy edits to apply before switching. Other source or timeline edits cannot be preserved."
163
+ )
164
+ };
165
+ var inputSchema = z.object(pairedAnnouncementInput).strict();
166
+ var PAIRED_ANNOUNCEMENT_CAPABILITY = {
167
+ templateId: "announcement-card-v1",
168
+ frameworks: ["remotion", "hyperframes"],
169
+ tool: "prepare_announcement",
170
+ dimensions: { width: 1280, height: 720, fps: 30, durationSeconds: 10 },
171
+ scope: "One silent announcement card. Returns portable source and caller-held copy state; does not save or render. Only title, body and action survive switching."
172
+ };
173
+ var PAIRED_ANNOUNCEMENT_INSTRUCTIONS = "For the paired announcement-card-v1 or a HyperFrames announcement request, use prepare_announcement. Never infer a framework from creative wording. Omit framework unless the user explicitly chooses one or says no preference. If needs-selection, ask the user. Continue with the latest returned project and expectedRevision; apply edits through edits before switching. Keep the returned project document for later calls. Never describe this caller-held document as a saved hosted project. The existing VideoSpec catalogue remains Remotion-only.";
174
+ function prepareAnnouncement(raw) {
175
+ const input = inputSchema.parse(raw);
176
+ const inputReceipt = {
177
+ contract: "paired-announcement-input-receipt/v1",
178
+ stage: "validated-input",
179
+ normalizedInput: JSON.parse(JSON.stringify(input))
180
+ };
181
+ const old = input.project;
182
+ if (old && input.expectedRevision !== old.revision)
183
+ throw new Error(
184
+ "Supply the latest project document and its matching expectedRevision."
185
+ );
186
+ if (old && input.copy)
187
+ throw new Error(
188
+ "Use edits to change existing copy; do not replace it with new-project copy."
189
+ );
190
+ if (!old && (input.edits || input.expectedRevision !== void 0))
191
+ throw new Error("Edits and expectedRevision require an existing project.");
192
+ const selected = input.framework === "remotion" || input.framework === "hyperframes" ? input.framework : old?.framework ?? (input.framework === "no-preference" ? "remotion" : void 0);
193
+ if (!selected)
194
+ return {
195
+ inputReceipt,
196
+ status: "needs-selection",
197
+ options: ["remotion", "hyperframes", "no-preference"],
198
+ message: "Which framework would you like: Remotion, HyperFrames, or no preference? No source was generated."
199
+ };
200
+ if (!old && !input.copy)
201
+ return {
202
+ inputReceipt,
203
+ status: "needs-copy",
204
+ framework: selected,
205
+ required: ["title", "body", "cta"]
206
+ };
207
+ const copy = copySchema.parse({
208
+ ...old?.copy ?? input.copy,
209
+ ...input.edits
210
+ });
211
+ const changed = old && Object.keys(copyFields).some(
212
+ (key) => copy[key] !== old.copy[key]
213
+ );
214
+ const project = {
215
+ templateId: "announcement-card-v1",
216
+ framework: selected,
217
+ revision: (old?.revision ?? 1) + (changed ? 1 : 0),
218
+ copy
219
+ };
220
+ return {
221
+ inputReceipt,
222
+ status: "ready",
223
+ reason: input.framework && input.framework !== "no-preference" ? "explicit" : old ? "existing-project" : "visible-default",
224
+ project,
225
+ files: {
226
+ ...announcementFiles(project),
227
+ "announcement.project.json": JSON.stringify(project, null, 2) + "\n"
228
+ },
229
+ commands: [
230
+ "npm install",
231
+ "npm run setup",
232
+ ...selected === "hyperframes" ? ["npx hyperframes check --samples 15 --strict"] : [],
233
+ "npm run preview",
234
+ "npm run render"
235
+ ],
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
+ };
238
+ }
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 = {}) {
246
+ server.registerTool(
247
+ "prepare_announcement",
248
+ {
249
+ title: "Prepare an announcement in Remotion or HyperFrames",
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),
251
+ inputSchema: pairedAnnouncementInput,
252
+ annotations: {
253
+ readOnlyHint: true,
254
+ destructiveHint: false,
255
+ openWorldHint: false
256
+ },
257
+ _meta: { securitySchemes: [{ type: "noauth" }] }
258
+ },
259
+ async (args) => {
260
+ try {
261
+ const result3 = prepareAnnouncement(args);
262
+ return {
263
+ structuredContent: result3,
264
+ content: [{ type: "text", text: JSON.stringify(result3) }]
265
+ };
266
+ } catch (error) {
267
+ return {
268
+ isError: true,
269
+ content: [
270
+ {
271
+ type: "text",
272
+ text: error instanceof Error ? error.message : "Invalid announcement input"
273
+ }
274
+ ]
275
+ };
276
+ }
277
+ }
278
+ );
279
+ }
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
+ "&": "&amp;",
481
+ "<": "&lt;",
482
+ ">": "&gt;",
483
+ '"': "&quot;",
484
+ "'": "&#39;"
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
+
3
1437
  // src/index.ts
4
1438
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
1439
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
1440
 
7
1441
  // src/creative-guides.ts
8
- import { z } from "zod";
1442
+ import { z as z4 } from "zod";
9
1443
 
10
1444
  // src/creative-guides.generated.ts
11
1445
  var CREATIVE_GUIDES = [
@@ -36,16 +1470,16 @@ var CREATIVE_GUIDES = [
36
1470
  ];
37
1471
 
38
1472
  // src/creative-guides.ts
39
- var creativeGuideIdSchema = z.enum([
1473
+ var creativeGuideIdSchema = z4.enum([
40
1474
  "visual-copy",
41
1475
  "carousel-stories",
42
1476
  "visual-design",
43
1477
  "human-review"
44
1478
  ]);
45
1479
  var CREATIVE_GUIDE_INDEX = CREATIVE_GUIDES.map(
46
- ({ id: id2, title, uri }) => ({
1480
+ ({ id: id2, title: title2, uri }) => ({
47
1481
  id: id2,
48
- title,
1482
+ title: title2,
49
1483
  uri
50
1484
  })
51
1485
  );
@@ -66,83 +1500,513 @@ function registerCreativeGuides(server) {
66
1500
  })
67
1501
  );
68
1502
  }
69
- server.registerTool(
70
- "get_creative_guide",
71
- {
72
- title: "Get a visual communication guide",
73
- 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.",
74
- inputSchema: { guideId: creativeGuideIdSchema.optional() },
75
- annotations: {
76
- readOnlyHint: true,
77
- destructiveHint: false,
78
- openWorldHint: false
79
- },
80
- _meta: { securitySchemes: [{ type: "noauth" }] }
81
- },
82
- async ({ guideId }) => {
83
- const guide = CREATIVE_GUIDES.find((entry) => entry.id === guideId);
84
- return {
85
- structuredContent: guide ? { guide } : { guides: CREATIVE_GUIDE_INDEX },
86
- content: [
87
- {
88
- type: "text",
89
- text: guide ? guide.text : CREATIVE_GUIDE_INDEX.map(
90
- (entry) => `${entry.id}: ${entry.title} (${entry.uri})`
91
- ).join("\n")
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
+ );
92
1951
  }
93
- ]
94
- };
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`);
95
1986
  }
96
- );
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 };
97
2006
  }
98
2007
 
99
- // src/public-templates.ts
100
- import { z as z2 } from "zod";
101
- var TEMPLATE_REPOSITORY = "https://github.com/instavar/remotion-templates";
102
- var TEMPLATE_ENDPOINT = "https://instavar.com/api/mcp/templates";
103
- var VOICE_STUDIO_URL = "https://instavar.com/voice";
104
- var templateIds = [
105
- "qa-ad",
106
- "proof-walkthrough",
107
- "announcement-brief",
108
- "social-remix",
109
- "finance-brief"
110
- ];
111
- var templateIdSchema = z2.enum(templateIds);
112
- var aspectSchema = z2.enum(["9:16", "4:5", "1:1"]);
113
- var templates = [
114
- {
115
- id: "qa-ad",
116
- name: "Question and answer ad",
117
- description: "A short video with a hook, source-video window, explanation points, and call to action.",
118
- aspects: ["9:16", "4:5", "1:1"]
119
- },
120
- {
121
- id: "proof-walkthrough",
122
- name: "Proof walkthrough",
123
- description: "A paced explanation for equations, worked reasoning, and a final takeaway.",
124
- aspects: ["9:16", "4:5", "1:1"]
125
- },
126
- {
127
- id: "announcement-brief",
128
- name: "Announcement brief",
129
- description: "A concise announcement with a headline, supporting points, optional media, and next action.",
130
- aspects: ["9:16", "4:5", "1:1"]
131
- },
132
- {
133
- id: "social-remix",
134
- name: "Social remix",
135
- description: "A caption-forward treatment that frames an existing clip with a hook, context, and call to action.",
136
- aspects: ["9:16", "4:5", "1:1"]
137
- },
138
- {
139
- id: "finance-brief",
140
- name: "Data and finance brief",
141
- description: "An editorial brief for comparisons, numeric highlights, evidence, caveats, and a recommendation.",
142
- aspects: ["9:16", "4:5", "1:1"]
143
- }
144
- ];
145
- var videoSpecSchema = {
2008
+ // src/videospec-json-schema.ts
2009
+ var VIDEO_SPEC_JSON_SCHEMA = {
146
2010
  $schema: "https://json-schema.org/draft/2020-12/schema",
147
2011
  $id: "https://instavar.com/schemas/video-spec-1.0.json",
148
2012
  title: "Instavar VideoSpec",
@@ -159,52 +2023,94 @@ var videoSpecSchema = {
159
2023
  properties: {
160
2024
  schemaVersion: { const: "1.0" },
161
2025
  id: { type: "string", pattern: "^[a-z0-9][a-z0-9-]{2,80}$" },
162
- templateFamily: { enum: templateIds },
2026
+ templateFamily: {
2027
+ enum: [
2028
+ "qa-ad",
2029
+ "proof-walkthrough",
2030
+ "announcement-brief",
2031
+ "social-remix",
2032
+ "finance-brief"
2033
+ ]
2034
+ },
163
2035
  meta: {
164
2036
  type: "object",
2037
+ additionalProperties: false,
165
2038
  required: ["title"],
166
2039
  properties: {
167
2040
  title: { type: "string", minLength: 1 },
168
2041
  description: { type: "string" },
169
- tags: { type: "array", items: { type: "string" } }
2042
+ tags: { type: "array", items: { type: "string" }, default: [] }
170
2043
  }
171
2044
  },
172
2045
  target: {
173
2046
  type: "object",
174
- required: ["aspect", "fps", "durationMode"],
2047
+ additionalProperties: false,
2048
+ required: ["aspect"],
175
2049
  properties: {
176
2050
  aspect: { enum: ["9:16", "4:5", "1:1"] },
177
- fps: { type: "integer", minimum: 1 },
178
- durationMode: { enum: ["auto", "fixed"] },
2051
+ fps: { type: "integer", minimum: 1, default: 30 },
2052
+ durationMode: { enum: ["auto", "fixed"], default: "auto" },
179
2053
  fixedDurationSec: { type: "number", exclusiveMinimum: 0 }
180
2054
  }
181
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
+ },
182
2072
  audio: {
183
2073
  type: "object",
2074
+ additionalProperties: false,
184
2075
  properties: {
185
- mode: { enum: ["auto", "narration", "music", "silent"] },
2076
+ mode: {
2077
+ enum: ["auto", "narration", "music", "silent"],
2078
+ default: "auto"
2079
+ },
186
2080
  narrationText: { type: "string" },
187
2081
  narrationSrc: { type: "string" },
188
- narrationVolume: { type: "number", minimum: 0, maximum: 1 },
2082
+ narrationVolume: {
2083
+ type: "number",
2084
+ minimum: 0,
2085
+ maximum: 1,
2086
+ default: 1
2087
+ },
189
2088
  musicSrc: { type: "string" },
190
- musicVolume: { type: "number", minimum: 0, maximum: 1 },
191
- ducking: { type: "boolean" },
2089
+ musicVolume: {
2090
+ type: "number",
2091
+ minimum: 0,
2092
+ maximum: 1,
2093
+ default: 0.16
2094
+ },
2095
+ ducking: { type: "boolean", default: true },
192
2096
  narrationTiming: {
193
2097
  type: "object",
2098
+ additionalProperties: false,
194
2099
  required: ["schemaVersion", "source", "durationMs", "segments"],
195
2100
  properties: {
196
2101
  schemaVersion: { const: "1.0" },
197
2102
  source: { const: "segmented-synthesis" },
198
- durationMs: { type: "integer", minimum: 1 },
2103
+ durationMs: { type: "integer", minimum: 1, maximum: 12e5 },
199
2104
  segments: {
200
2105
  type: "array",
201
2106
  minItems: 1,
202
2107
  maxItems: 24,
203
2108
  items: {
204
2109
  type: "object",
2110
+ additionalProperties: false,
205
2111
  required: ["sceneId", "startMs", "endMs", "durationMs"],
206
2112
  properties: {
207
- sceneId: { type: "string", minLength: 1 },
2113
+ sceneId: { type: "string", minLength: 1, maxLength: 80 },
208
2114
  startMs: { type: "integer", minimum: 0 },
209
2115
  endMs: { type: "integer", minimum: 1 },
210
2116
  durationMs: { type: "integer", minimum: 1 }
@@ -215,11 +2121,20 @@ var videoSpecSchema = {
215
2121
  }
216
2122
  }
217
2123
  },
2124
+ assets: {
2125
+ type: "object",
2126
+ additionalProperties: false,
2127
+ properties: {
2128
+ logoSrc: { type: "string" },
2129
+ posterSrc: { type: "string" }
2130
+ }
2131
+ },
218
2132
  scenes: {
219
2133
  type: "array",
220
2134
  minItems: 1,
221
2135
  items: {
222
2136
  type: "object",
2137
+ additionalProperties: false,
223
2138
  required: ["id", "kind", "content"],
224
2139
  properties: {
225
2140
  id: { type: "string", minLength: 1 },
@@ -236,27 +2151,136 @@ var videoSpecSchema = {
236
2151
  ]
237
2152
  },
238
2153
  content: { type: "object" },
239
- timing: { type: "object" }
2154
+ timing: {
2155
+ type: "object",
2156
+ additionalProperties: false,
2157
+ properties: {
2158
+ durationMs: { type: "integer", exclusiveMinimum: 0 }
2159
+ }
2160
+ }
240
2161
  }
241
2162
  }
242
- },
243
- style: {
244
- type: "object",
245
- properties: {
246
- theme: {
247
- enum: ["default", "editorial-light", "editorial-dark", "clinical"]
248
- },
249
- variant: { type: "string" },
250
- safeAreaProfile: { enum: ["baseline", "metaSafe"] },
251
- showSceneLabels: { type: "boolean", default: false }
252
- }
253
2163
  }
254
2164
  }
255
2165
  };
256
- var annotations = {
257
- readOnlyHint: true,
258
- openWorldHint: false,
259
- destructiveHint: false
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
+ }
260
2284
  };
261
2285
  function result(value, summary) {
262
2286
  return {
@@ -268,14 +2292,18 @@ function slugify(value) {
268
2292
  const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 72);
269
2293
  return slug.length >= 3 ? slug : "new-video";
270
2294
  }
271
- function scaffoldVideoSpec(args) {
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";
272
2300
  const scenes = [
273
2301
  {
274
2302
  id: "hero",
275
2303
  kind: "hero",
276
2304
  content: {
277
2305
  title: args.title,
278
- subtitle: "Replace with a concise promise"
2306
+ ...announcement && args.body ? {} : { subtitle: args.body ?? "Replace with a concise promise" }
279
2307
  },
280
2308
  timing: { durationMs: 4e3 }
281
2309
  }
@@ -291,7 +2319,14 @@ function scaffoldVideoSpec(args) {
291
2319
  timing: { durationMs: 7e3 }
292
2320
  });
293
2321
  }
294
- if (args.templateId === "announcement-brief") {
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) {
295
2330
  scenes.push(
296
2331
  {
297
2332
  id: "context",
@@ -322,14 +2357,21 @@ function scaffoldVideoSpec(args) {
322
2357
  timing: { durationMs: 7e3 }
323
2358
  }
324
2359
  );
325
- } else {
2360
+ } else if (args.templateId === "proof-walkthrough") {
326
2361
  scenes.push({
327
- id: args.templateId === "proof-walkthrough" ? "working" : "key-points",
328
- kind: args.templateId === "proof-walkthrough" ? "equation" : "points",
329
- content: args.templateId === "proof-walkthrough" ? {
2362
+ id: "working",
2363
+ kind: "equation",
2364
+ content: {
330
2365
  title: "Work through the idea",
331
2366
  latexBlocks: ["x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}"]
332
- } : {
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] } : {
333
2375
  title: "Key points",
334
2376
  points: [
335
2377
  "Replace with evidence",
@@ -340,218 +2382,658 @@ function scaffoldVideoSpec(args) {
340
2382
  timing: { durationMs: 8e3 }
341
2383
  });
342
2384
  }
343
- scenes.push({
344
- id: "cta",
345
- kind: "cta",
346
- content: { line1: "Replace with the next action" },
347
- timing: {
348
- durationMs: args.templateId === "announcement-brief" ? 6e3 : 5e3
349
- }
350
- });
351
- return {
352
- schemaVersion: "1.0",
353
- id: slugify(args.title),
354
- templateFamily: args.templateId,
355
- meta: { title: args.title, tags: [] },
356
- target: { aspect: args.aspect, fps: 30, durationMode: "auto" },
357
- style: {
358
- theme: args.templateId === "announcement-brief" ? "editorial-light" : "default",
359
- variant: "default",
360
- safeAreaProfile: "metaSafe",
361
- showSceneLabels: false
2385
+ scenes.push({
2386
+ id: "cta",
2387
+ kind: "cta",
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."
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
2615
+ });
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
2636
+ },
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
+ )
2649
+ },
2650
+ templates.map((template) => `${template.id}: ${template.description}`).join("\n")
2651
+ );
2652
+ }
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
+ }
362
2689
  },
363
- audio: {
364
- mode: "silent",
365
- narrationText: "",
366
- narrationVolume: 1,
367
- musicVolume: 0.16,
368
- ducking: true
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"
369
2699
  },
370
- assets: {},
371
- scenes
372
- };
2700
+ { pairedTemplates: paired }
2701
+ );
2702
+ return result(
2703
+ { templateId: args.templateId, spec },
2704
+ `Example VideoSpec for ${args.templateId}.`
2705
+ );
373
2706
  }
374
- function registerPublicTemplateTools(server) {
375
- registerCreativeGuides(server);
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";
376
2767
  server.registerTool(
377
- "get_capabilities",
2768
+ "list_carousel_templates",
378
2769
  {
379
- title: "Get video capabilities",
380
- description: "Call first. Reports free local features and optional account features.",
2770
+ title: "List carousel templates",
2771
+ description: "List the account-free editorial carousel story structures.",
381
2772
  inputSchema: {},
382
- annotations
2773
+ annotations: {
2774
+ title: "List carousel templates",
2775
+ ...READ_ONLY_ANNOTATIONS
2776
+ },
2777
+ ...meta("list_carousel_templates")
383
2778
  },
384
- async () => result(
385
- {
386
- authentication: "optional",
387
- apiKeyConfigured: Boolean(process.env.INSTAVAR_API_KEY?.trim()),
388
- repository: TEMPLATE_REPOSITORY,
389
- endpoint: TEMPLATE_ENDPOINT,
390
- creativeGuides: CREATIVE_GUIDE_INDEX,
391
- localFeatures: [
392
- "template discovery",
393
- "VideoSpec schema and examples",
394
- "deterministic scaffolding",
395
- "local render instructions",
396
- "render error diagnosis",
397
- "keyless on-device narration with scene timing",
398
- "every-scene preview frames",
399
- "post-render audible-audio verification"
400
- ],
401
- narration: {
402
- url: VOICE_STUDIO_URL,
403
- cost: "free",
404
- apiKeyRequired: false,
405
- guidance: "Write one paragraph per scene. Download the WAV and timing manifest, then apply both before rendering."
406
- },
407
- accountFeatures: [
408
- "managed rendering and storage",
409
- "workflow review and approval",
410
- "social publishing and metrics"
411
- ]
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.")
412
2810
  },
413
- 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."
414
- )
2811
+ annotations: {
2812
+ title: "Scaffold carousel deck",
2813
+ ...READ_ONLY_ANNOTATIONS
2814
+ },
2815
+ ...meta("scaffold_carousel_deck")
2816
+ },
2817
+ async (args) => runScaffoldCarouselDeckTool(args)
415
2818
  );
416
2819
  server.registerTool(
417
2820
  "list_templates",
418
2821
  {
419
- title: "List Remotion templates",
420
- description: "List public templates, optionally filtered by aspect ratio.",
421
- inputSchema: { aspect: aspectSchema.optional() },
422
- annotations
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")
423
2831
  },
424
- async ({ aspect }) => {
425
- const filtered = aspect ? templates.filter((template) => template.aspects.includes(aspect)) : templates;
426
- return result(
427
- { templates: filtered },
428
- filtered.map((template) => `${template.id}: ${template.description}`).join("\n")
429
- );
430
- }
2832
+ async (args) => runListTemplatesTool(args, paired)
431
2833
  );
432
2834
  server.registerTool(
433
2835
  "get_template",
434
2836
  {
435
- title: "Get Remotion template",
436
- description: "Get one public template contract.",
437
- inputSchema: { templateId: templateIdSchema },
438
- annotations
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")
439
2846
  },
440
- async ({ templateId }) => {
441
- const template = templates.find((item) => item.id === templateId);
442
- return result(
443
- { template },
444
- `${template?.name ?? templateId}
445
- ${template?.description ?? ""}`
446
- );
447
- }
2847
+ async (args) => runGetTemplateTool(args, paired)
448
2848
  );
449
2849
  server.registerTool(
450
2850
  "get_video_schema",
451
2851
  {
452
2852
  title: "Get VideoSpec schema",
453
- description: "Return the public VideoSpec JSON Schema.",
2853
+ description: "Return the published JSON Schema for VideoSpec version 1.0.",
454
2854
  inputSchema: {},
455
- annotations
2855
+ annotations: { title: "Get video schema", ...READ_ONLY_ANNOTATIONS },
2856
+ ...meta("get_video_schema")
456
2857
  },
457
- async () => result(
458
- { schemaVersion: "1.0", jsonSchema: videoSpecSchema },
459
- "VideoSpec schema version 1.0."
460
- )
2858
+ async () => runGetVideoSchemaTool()
461
2859
  );
462
2860
  server.registerTool(
463
2861
  "get_example",
464
2862
  {
465
2863
  title: "Get VideoSpec example",
466
- description: "Return a starter example for one template.",
467
- inputSchema: { templateId: templateIdSchema },
468
- annotations
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")
469
2872
  },
470
- async ({ templateId }) => result(
471
- {
472
- templateId,
473
- spec: scaffoldVideoSpec({
474
- templateId,
475
- title: `Example ${templateId}`,
476
- aspect: "9:16"
477
- })
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.")
478
2896
  },
479
- `Example VideoSpec for ${templateId}.`
480
- )
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)
481
2904
  );
482
2905
  server.registerTool(
483
2906
  "scaffold_video_spec",
484
2907
  {
485
2908
  title: "Scaffold VideoSpec",
486
- description: "Return a deterministic VideoSpec skeleton without writing files.",
2909
+ description: "Return a deterministic VideoSpec skeleton. This tool does not generate creative copy or write files.",
487
2910
  inputSchema: {
488
- templateId: templateIdSchema,
489
- title: z2.string().min(1).max(160),
490
- aspect: aspectSchema.default("9:16"),
491
- mediaSrc: z2.string().min(1).max(500).optional()
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
+ )
492
2931
  },
493
- annotations
2932
+ annotations: { title: "Scaffold video spec", ...READ_ONLY_ANNOTATIONS },
2933
+ ...meta("scaffold_video_spec")
494
2934
  },
495
- async (args) => {
496
- const spec = scaffoldVideoSpec(args);
497
- return result(
498
- { spec, filename: `${String(spec.id)}.video.json` },
499
- "Starter VideoSpec created. Replace placeholder copy and media."
500
- );
501
- }
2935
+ async (args) => runScaffoldVideoSpecTool(args, paired, options.acceptedPreview)
502
2936
  );
503
2937
  server.registerTool(
504
2938
  "get_render_instructions",
505
2939
  {
506
2940
  title: "Get local render instructions",
507
- description: "Return clone, install, validate, preview, and render commands.",
2941
+ description: "Return the clone, install, validate, preview, render, and QA workflow.",
508
2942
  inputSchema: {
509
- client: z2.enum(["codex", "claude-code", "manual"]).default("manual")
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
+ )
510
2946
  },
511
- annotations
2947
+ annotations: {
2948
+ title: "Get render instructions",
2949
+ ...READ_ONLY_ANNOTATIONS
2950
+ },
2951
+ ...meta("get_render_instructions")
512
2952
  },
513
- async ({ client }) => {
514
- const commands = [
515
- `git clone ${TEMPLATE_REPOSITORY}.git`,
516
- "cd remotion-templates",
517
- "npm install",
518
- `# Generate one narration paragraph per scene at ${VOICE_STUDIO_URL}`,
519
- "npm run video:timing -- path/to/video.video.json --audio public/generated/narration.wav --manifest path/to/narration.timing.json",
520
- "npm run video:validate -- path/to/video.video.json",
521
- "npm run video:preview -- path/to/video.video.json",
522
- "npm run video:render -- path/to/video.video.json",
523
- "npm run video:qa -- path/to/video.video.json path/to/video.mp4"
524
- ];
525
- return result(
526
- { repository: TEMPLATE_REPOSITORY, client, commands },
527
- commands.join("\n")
528
- );
529
- }
2953
+ async (args) => runGetRenderInstructionsTool(args)
530
2954
  );
531
2955
  server.registerTool(
532
2956
  "diagnose_render_error",
533
2957
  {
534
2958
  title: "Diagnose render error",
535
- description: "Classify a local validation or render error.",
536
- inputSchema: { errorText: z2.string().min(1).max(1e4) },
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: {},
537
2994
  annotations
538
2995
  },
539
- async ({ errorText }) => {
540
- const text = errorText.toLowerCase();
541
- const code = text.includes("enoent") || text.includes("not found") ? "missing-file" : text.includes("ffmpeg") || text.includes("codec") ? "media-codec" : text.includes("schema") || text.includes("invalid") || text.includes("zod") ? "invalid-video-spec" : text.includes("chrome") || text.includes("browser") ? "browser-runtime" : "unknown-render-error";
542
- return result(
543
- {
544
- code,
545
- nextStep: "Run validation first, confirm Node 22 or newer, and retry with the smallest included example."
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."
546
3022
  },
547
- `Diagnosis: ${code}.`
548
- );
549
- }
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
+ )
550
3031
  );
3032
+ registerSharedTemplateTools(server, { pairedTemplates: paired });
551
3033
  }
552
3034
 
553
3035
  // src/tools.ts
554
- import { z as z5 } from "zod";
3036
+ import { z as z8 } from "zod";
555
3037
 
556
3038
  // src/http.ts
557
3039
  import { randomUUID } from "crypto";
@@ -597,11 +3079,11 @@ function requestFailure(error) {
597
3079
  return error instanceof Error ? error.message : String(error);
598
3080
  }
599
3081
  async function parse(res) {
600
- const text = await res.text();
3082
+ const text3 = await res.text();
601
3083
  try {
602
- return JSON.parse(text);
3084
+ return JSON.parse(text3);
603
3085
  } catch {
604
- return text;
3086
+ return text3;
605
3087
  }
606
3088
  }
607
3089
  async function apiGet(path) {
@@ -625,7 +3107,9 @@ async function apiPost(path, payload) {
625
3107
  method: "POST",
626
3108
  headers: {
627
3109
  [API_KEY_HEADER]: getApiKey(),
628
- "content-type": "application/json"
3110
+ "content-type": "application/json",
3111
+ // Labels person-consent declarations made through MCP (surface only).
3112
+ "x-instavar-client": "mcp"
629
3113
  },
630
3114
  body: JSON.stringify(clean),
631
3115
  signal: AbortSignal.timeout(API_REQUEST_TIMEOUT_MS)
@@ -647,15 +3131,15 @@ ${open}
647
3131
  ${json}
648
3132
  ${close}`;
649
3133
  }
650
- function toToolResult(result2) {
3134
+ function toToolResult(result3) {
651
3135
  return {
652
3136
  content: [
653
3137
  {
654
3138
  type: "text",
655
- text: fenceForModel(JSON.stringify(result2.body, null, 2))
3139
+ text: fenceForModel(JSON.stringify(result3.body, null, 2))
656
3140
  }
657
3141
  ],
658
- isError: !result2.ok
3142
+ isError: !result3.ok
659
3143
  };
660
3144
  }
661
3145
  async function runTool(fn) {
@@ -675,7 +3159,7 @@ async function runTool(fn) {
675
3159
  }
676
3160
 
677
3161
  // src/brief-schema.ts
678
- import { z as z3 } from "zod";
3162
+ import { z as z6 } from "zod";
679
3163
  var PUBLISH_TARGETS = [
680
3164
  "linkedin",
681
3165
  "x",
@@ -707,16 +3191,16 @@ var REFERENCE_ROLES = [
707
3191
  "environment",
708
3192
  "style"
709
3193
  ];
710
- var optionalDirectionText = z3.string().trim().min(1).max(500).optional();
711
- var visualDirectionSchema = z3.object({
3194
+ var optionalDirectionText = z6.string().trim().min(1).max(500).optional();
3195
+ var visualDirectionSchema = z6.object({
712
3196
  communicationJob: optionalDirectionText,
713
3197
  emotionalTarget: optionalDirectionText,
714
- attentionPath: z3.object({
3198
+ attentionPath: z6.object({
715
3199
  first: optionalDirectionText,
716
3200
  next: optionalDirectionText,
717
3201
  proof: optionalDirectionText
718
3202
  }).optional(),
719
- visualLanguage: z3.object({
3203
+ visualLanguage: z6.object({
720
3204
  space: optionalDirectionText,
721
3205
  line: optionalDirectionText,
722
3206
  shape: optionalDirectionText,
@@ -725,24 +3209,24 @@ var visualDirectionSchema = z3.object({
725
3209
  movement: optionalDirectionText,
726
3210
  rhythm: optionalDirectionText
727
3211
  }).optional(),
728
- camera: z3.object({
3212
+ camera: z6.object({
729
3213
  shotSize: optionalDirectionText,
730
3214
  angle: optionalDirectionText,
731
3215
  movement: optionalDirectionText,
732
3216
  lens: optionalDirectionText
733
3217
  }).optional()
734
3218
  });
735
- var generationContractSchema = z3.object({
736
- globalInvariants: z3.array(z3.string().trim().min(1).max(300)).max(20).optional(),
737
- shotDelta: z3.string().trim().min(1).max(500).optional(),
738
- referenceBindings: z3.array(
739
- z3.object({
740
- assetId: z3.string().trim().min(1).max(200),
741
- roles: z3.array(z3.enum(REFERENCE_ROLES)).min(1).max(7)
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)
742
3226
  })
743
3227
  ).max(20).optional(),
744
- prohibitedChanges: z3.array(z3.string().trim().min(1).max(300)).max(20).optional(),
745
- reviewCriteria: z3.array(z3.string().trim().min(1).max(300)).max(20).optional()
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()
746
3230
  });
747
3231
  var REGION_NAMES = [
748
3232
  "background",
@@ -759,55 +3243,55 @@ var ASSET_ROLES = [
759
3243
  "lower-third",
760
3244
  "diagram-node"
761
3245
  ];
762
- var assetSlotStyleSchema = z3.object({
763
- opacity: z3.number().min(0).max(1).optional(),
764
- fit: z3.enum(["cover", "contain"]).optional(),
765
- z: z3.number().optional()
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()
766
3250
  }).strict();
767
- var assetSlotSchema = z3.object({
768
- id: z3.string().min(1),
769
- sceneId: z3.string().min(1).optional(),
770
- region: z3.enum(REGION_NAMES),
771
- role: z3.enum(ASSET_ROLES),
772
- kind: z3.enum(["image", "video"]),
773
- source: z3.enum(["auto", "stock", "generate"]),
774
- intent: z3.string(),
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(),
775
3259
  style: assetSlotStyleSchema.optional(),
776
- timing: z3.object({
777
- fromPct: z3.number().min(0).max(1).optional(),
778
- toPct: z3.number().min(0).max(1).optional()
3260
+ timing: z6.object({
3261
+ fromPct: z6.number().min(0).max(1).optional(),
3262
+ toPct: z6.number().min(0).max(1).optional()
779
3263
  }).strict().optional(),
780
- url: z3.string().url().optional()
3264
+ url: z6.string().url().optional()
781
3265
  }).strict();
782
- var briefStepSchema = z3.object({
783
- label: z3.string(),
784
- latex: z3.string().optional(),
785
- explanation: z3.string().optional()
3266
+ var briefStepSchema = z6.object({
3267
+ label: z6.string(),
3268
+ latex: z6.string().optional(),
3269
+ explanation: z6.string().optional()
786
3270
  });
787
- var briefAudioLayerSchema = z3.object({
788
- id: z3.string().min(1),
789
- src: z3.string().url(),
790
- startFrame: z3.number().int().nonnegative().default(0),
791
- durationInFrames: z3.number().int().positive().optional(),
792
- volume: z3.number().min(0).max(1).default(1),
793
- fadeFrames: z3.number().int().nonnegative().default(0),
794
- role: z3.enum(["music", "foley", "transition", "effect"])
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"])
795
3279
  });
796
- var protectedRegionEffectSchema = z3.object({
797
- id: z3.string().min(1).max(80),
798
- kind: z3.literal("border-glint"),
799
- coordinateSpace: z3.literal("output-frame-v1").default("output-frame-v1"),
800
- region: z3.object({
801
- leftPercent: z3.number().min(0).max(100),
802
- topPercent: z3.number().min(0).max(100),
803
- widthPercent: z3.number().positive().max(100),
804
- heightPercent: z3.number().positive().max(100)
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)
805
3289
  }),
806
- startFrame: z3.number().int().nonnegative().default(0),
807
- endFrame: z3.number().int().positive(),
808
- speedUpFrame: z3.number().int().nonnegative().optional(),
809
- color: z3.string().max(64).default("#ffffff"),
810
- thicknessPercent: z3.number().positive().max(20).default(3)
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)
811
3295
  }).superRefine((effect2, context) => {
812
3296
  if (effect2.region.leftPercent + effect2.region.widthPercent > 100) {
813
3297
  context.addIssue({
@@ -838,13 +3322,13 @@ var protectedRegionEffectSchema = z3.object({
838
3322
  });
839
3323
  }
840
3324
  });
841
- var beatVisualRequestSchema = z3.object({
842
- intent: z3.string().trim().min(1).optional(),
843
- media: z3.enum(["still", "motion"]).default("still"),
844
- source: z3.enum(["auto", "stock", "generate"]).default("auto")
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")
845
3329
  }).strict();
846
- var briefSceneSchema = z3.object({
847
- type: z3.enum([
3330
+ var briefSceneSchema = z6.object({
3331
+ type: z6.enum([
848
3332
  "hero",
849
3333
  "points",
850
3334
  "equation",
@@ -867,156 +3351,188 @@ var briefSceneSchema = z3.object({
867
3351
  "image-window",
868
3352
  "custom"
869
3353
  ]),
870
- durationInFrames: z3.number().optional(),
871
- narrationText: z3.string().optional(),
3354
+ layoutPreset: z6.literal("dark-cafe-announcement").optional().describe(
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."
3356
+ ),
3357
+ durationInFrames: z6.number().optional(),
3358
+ narrationText: z6.string().optional(),
872
3359
  visualResolution: beatVisualRequestSchema.optional(),
873
- title: z3.string().optional(),
874
- subtitle: z3.string().optional(),
875
- text: z3.string().optional(),
876
- points: z3.array(z3.string()).optional(),
877
- latex: z3.string().optional(),
878
- value: z3.union([z3.string(), z3.number()]).optional(),
879
- max: z3.number().optional(),
880
- label: z3.string().optional(),
881
- unit: z3.string().optional(),
882
- prefix: z3.string().optional(),
883
- suffix: z3.string().optional(),
884
- decimals: z3.number().optional(),
885
- color: z3.string().optional(),
886
- tone: z3.string().optional(),
887
- iconKey: z3.string().optional(),
888
- stepNumber: z3.number().optional(),
889
- totalSteps: z3.number().optional(),
890
- items: z3.array(z3.object({ label: z3.string(), value: z3.number() })).optional(),
891
- src: z3.string().optional(),
892
- caption: z3.string().optional(),
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(),
893
3380
  // A-roll generation (video-window scenes): source "generate" triggers
894
3381
  // flag-gated WaveSpeed image/video gen server-side from generatePrompt;
895
3382
  // "url" (default) uses src as-is. Kept in sync with the app's
896
3383
  // briefSceneSchema - see the parity test in the app suite.
897
- source: z3.enum(["url", "generate"]).optional(),
898
- generatePrompt: z3.string().optional(),
3384
+ source: z6.enum(["url", "generate"]).optional(),
3385
+ generatePrompt: z6.string().optional(),
899
3386
  generationContract: generationContractSchema.optional(),
900
- trimStartSeconds: z3.number().nonnegative().optional(),
901
- trimEndSeconds: z3.number().positive().optional(),
902
- playbackRate: z3.number().min(0.25).max(8).optional(),
903
- objectFit: z3.enum(["cover", "contain"]).optional(),
904
- objectPosition: z3.string().optional(),
905
- scaleFrom: z3.number().min(0.5).max(2).optional(),
906
- scaleTo: z3.number().min(0.5).max(2).optional(),
907
- panFromX: z3.number().min(-50).max(50).optional(),
908
- panToX: z3.number().min(-50).max(50).optional(),
909
- panFromY: z3.number().min(-50).max(50).optional(),
910
- panToY: z3.number().min(-50).max(50).optional(),
911
- routeFrom: z3.string().optional(),
912
- routeTo: z3.string().optional(),
913
- routeLabel: z3.string().optional(),
914
- overlayKind: z3.enum(["none", "hook", "stage", "end-card"]).optional(),
915
- overlayPosition: z3.enum(["center", "top", "bottom"]).optional(),
916
- accentColor: z3.string().optional(),
917
- overlayBackgroundColor: z3.string().optional(),
918
- cutFlash: z3.boolean().optional(),
919
- muted: z3.boolean().optional(),
920
- protectedRegionEffects: z3.array(protectedRegionEffectSchema).max(4).optional(),
921
- flashAtFrame: z3.number().int().nonnegative().optional(),
922
- flashDurationFrames: z3.number().int().positive().max(15).optional()
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()
923
3410
  });
924
- var directionsBriefSchema = z3.object({
925
- subtitle: z3.string().optional(),
926
- unitLabel: z3.string().optional(),
927
- distanceTimeChip: z3.string().optional(),
928
- clips: z3.array(
929
- z3.object({
930
- src: z3.string(),
931
- trimStartSeconds: z3.number().optional(),
932
- trimEndSeconds: z3.number(),
933
- speed: z3.number().optional(),
934
- mirrorFlip: z3.boolean().optional()
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()
935
3422
  })
936
3423
  ).min(1),
937
- cues: z3.array(
938
- z3.object({
939
- atSeconds: z3.number(),
940
- text: z3.string(),
941
- arrow: z3.enum(["left", "right", "forward"]).optional(),
942
- step: z3.number().optional()
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()
943
3430
  })
944
3431
  ),
945
- outro: z3.object({
946
- heading: z3.string().optional(),
947
- body: z3.array(z3.string()).optional(),
948
- seconds: z3.number().optional()
3432
+ outro: z6.object({
3433
+ heading: z6.string().optional(),
3434
+ body: z6.array(z6.string()).optional(),
3435
+ seconds: z6.number().optional()
949
3436
  }).optional(),
950
- music: z3.object({
951
- perSegment: z3.array(
952
- z3.object({
953
- src: z3.string(),
954
- clipIndexStart: z3.number().optional(),
955
- hotOffsetSeconds: z3.number().optional(),
956
- volume: z3.number().optional()
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()
957
3444
  })
958
3445
  ).optional(),
959
- crossfadeSeconds: z3.number().optional()
3446
+ crossfadeSeconds: z6.number().optional()
960
3447
  }).optional()
961
3448
  });
3449
+ var PERSON_CONSENT_WORDING_VERSION = "2026-10-19.1";
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.";
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(
3454
+ `Version of the declaration the caller confirmed. Current: ${PERSON_CONSENT_WORDING_VERSION}.`
3455
+ )
3456
+ }).strict().describe(
3457
+ `Required when the brief uses an uploaded image or video. Ask the user to confirm: "${PERSON_CONSENT_WORDING}"`
3458
+ );
3459
+ var LIKENESS_WARNING_WORDING_VERSION = "2026-10-19.1";
3460
+ var LIKENESS_WARNING_WORDING = "This photo seems to show someone other than you. Only continue if you have their permission.";
3461
+ var LIKENESS_MINOR_WARNING_WORDING_VERSION = "2026-10-19.1";
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.";
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(
3466
+ `Which warning was accepted. "other_person" (default): "${LIKENESS_WARNING_WORDING}" "guardian": "${LIKENESS_MINOR_WARNING_WORDING}"`
3467
+ ),
3468
+ wordingVersion: z6.string().describe(
3469
+ `Version of the warning accepted. Current: other_person ${LIKENESS_WARNING_WORDING_VERSION}, guardian ${LIKENESS_MINOR_WARNING_WORDING_VERSION}.`
3470
+ )
3471
+ }).strict().describe(
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."
3473
+ );
962
3474
  var createBriefShape = {
963
- title: z3.string().min(1).max(120).describe("Short title (max 120 chars)"),
964
- script: z3.string().min(1).describe("Full narration script (sent to TTS)"),
965
- caption: z3.string().min(1).describe("Social caption with hooks, hashtags, CTA"),
966
- publishTarget: z3.enum(PUBLISH_TARGETS).describe("Target social platform"),
967
- publishTargets: z3.array(z3.enum(PUBLISH_TARGETS)).max(9).optional().describe("All allowed social destinations for this video"),
968
- objective: z3.enum(BRIEF_OBJECTIVES).describe("Content objective (drives composition routing)"),
969
- targetDurationSeconds: z3.number().int().min(5).max(600).optional().describe("Requested final video duration in seconds"),
970
- narrationMode: z3.enum(["tts", "off"]).optional().describe("Disable synthesized narration for source-audio or silent work"),
971
- lessonTitle: z3.string().optional().describe("Display title rendered inside the video"),
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"),
972
3484
  visualDirection: visualDirectionSchema.optional().describe(
973
3485
  "Whole-video communication, attention, visual-language, and camera direction."
974
3486
  ),
975
- steps: z3.array(briefStepSchema).optional().describe("Structured steps for educational content (LaTeX)"),
976
- scenes: z3.array(briefSceneSchema).optional().describe("Scene graph for the UniversalVideo composition"),
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"),
977
3489
  // No zod .default() here: a default would materialize into the parsed tool
978
3490
  // arguments and relay to /api/v1/jobs as if the caller stated it, corrupting
979
3491
  // the resolution provenance the API echoes back. Unset must stay absent;
980
3492
  // the API re-applies 9:16 server-side.
981
- aspectRatio: z3.enum(ASPECT_RATIOS).optional().describe("Aspect ratio (defaults to 9:16 when omitted)"),
982
- compositionId: z3.string().optional().describe("Remotion composition ID override (advanced)"),
983
- voiceId: z3.string().optional().describe(
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(
984
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."
985
3497
  ),
986
3498
  directions: directionsBriefSchema.optional().describe(
987
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')."
988
3500
  ),
989
- assetSlots: z3.array(assetSlotSchema).optional().describe(
3501
+ assetSlots: z6.array(assetSlotSchema).optional().describe(
990
3502
  "Optional storyboard asset slots: region-placed image/video assets or placeholders attached to the whole video or a specific scene."
991
3503
  ),
992
- audioLayers: z3.array(briefAudioLayerSchema).optional().describe("Optional typed music, Foley, transition, and effect layers.")
3504
+ audioLayers: z6.array(briefAudioLayerSchema).optional().describe("Optional typed music, Foley, transition, and effect layers."),
3505
+ personConsent: personConsentSchema.optional(),
3506
+ likenessAcknowledgement: likenessAcknowledgementSchema.optional()
993
3507
  };
994
3508
  var editBriefShape = {
995
- jobId: z3.string().uuid().describe("The job to edit"),
996
- title: z3.string().optional().describe("Updated title"),
997
- script: z3.string().optional().describe("Updated narration script"),
998
- caption: z3.string().optional().describe("Updated social caption"),
999
- lessonTitle: z3.string().optional().describe("Updated lesson title"),
1000
- narrationMode: z3.enum(["tts", "off"]).optional().describe("Updated synthesized narration mode"),
3509
+ jobId: z6.string().uuid().describe("The job to edit"),
3510
+ personConsent: personConsentSchema.optional(),
3511
+ likenessAcknowledgement: likenessAcknowledgementSchema.optional(),
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"),
1001
3517
  visualDirection: visualDirectionSchema.optional().describe("Updated whole-video visual direction"),
1002
- steps: z3.array(briefStepSchema).optional().describe("Updated steps (replaces the array)"),
1003
- scenes: z3.array(briefSceneSchema).optional().describe("Updated scenes (replaces the array)"),
1004
- aspectRatio: z3.enum(ASPECT_RATIOS).optional().describe("Updated aspect ratio"),
1005
- objective: z3.enum(BRIEF_OBJECTIVES).optional().describe(
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(
1006
3522
  "Updated content objective. Re-routes the composition and triggers a re-render."
1007
3523
  ),
1008
- publishTarget: z3.enum(PUBLISH_TARGETS).optional().describe(
3524
+ publishTarget: z6.enum(PUBLISH_TARGETS).optional().describe(
1009
3525
  "Updated target social platform. Distribution metadata only - does not trigger a re-render."
1010
3526
  ),
1011
- publishTargets: z3.array(z3.enum(PUBLISH_TARGETS)).max(9).optional().describe("Replacement allowed social destination list"),
1012
- targetDurationSeconds: z3.number().int().min(5).max(600).optional().describe("Updated final video duration in seconds"),
1013
- voiceId: z3.string().optional().describe(
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(
1014
3530
  "Updated TTS narrator voice ID. Triggers a re-render with the new voice."
1015
3531
  ),
1016
- assetSlots: z3.array(assetSlotSchema).optional().describe(
3532
+ assetSlots: z6.array(assetSlotSchema).optional().describe(
1017
3533
  "Replacement storyboard asset slots. Omit to preserve existing slots; pass [] to clear them."
1018
3534
  ),
1019
- audioLayers: z3.array(briefAudioLayerSchema).optional().describe(
3535
+ audioLayers: z6.array(briefAudioLayerSchema).optional().describe(
1020
3536
  "Replacement typed audio layers. Omit to preserve existing layers; pass [] to clear them."
1021
3537
  )
1022
3538
  };
@@ -1053,7 +3569,7 @@ function verificationMatches(verifications, pattern) {
1053
3569
  }
1054
3570
  function buildRenderDiagnosticSnapshot(params) {
1055
3571
  const failed = [params.jobStatus, params.videoState].find(
1056
- (result2) => !result2.ok
3572
+ (result3) => !result3.ok
1057
3573
  );
1058
3574
  if (failed) return failed;
1059
3575
  const status = record(params.jobStatus.body);
@@ -1124,165 +3640,165 @@ function buildRenderDiagnosticSnapshot(params) {
1124
3640
  }
1125
3641
 
1126
3642
  // src/design-action-schema.ts
1127
- import { z as z4 } from "zod";
1128
- var id = z4.string().min(1).max(120);
1129
- var point = z4.object({ x: z4.number().min(0).max(1), y: z4.number().min(0).max(1) }).strict();
1130
- var polygonShape = z4.object({
1131
- kind: z4.literal("polygon"),
1132
- points: z4.array(point).min(3).max(512)
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)
1133
3649
  }).strict();
1134
- var rasterShape = z4.object({
1135
- kind: z4.literal("raster"),
1136
- source: z4.object({
3650
+ var rasterShape = z7.object({
3651
+ kind: z7.literal("raster"),
3652
+ source: z7.object({
1137
3653
  assetId: id,
1138
- expectedSha256: z4.string().regex(/^[a-f0-9]{64}$/),
1139
- sourceRange: z4.object({
1140
- startFrame: z4.number().int().nonnegative(),
1141
- endFrame: z4.number().int().positive()
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()
1142
3658
  }).strict().optional()
1143
3659
  }).strict(),
1144
- widthPx: z4.number().int().positive().max(8192),
1145
- heightPx: z4.number().int().positive().max(8192)
3660
+ widthPx: z7.number().int().positive().max(8192),
3661
+ heightPx: z7.number().int().positive().max(8192)
1146
3662
  }).strict();
1147
- var shape = z4.discriminatedUnion("kind", [polygonShape, rasterShape]);
1148
- var mask = z4.object({
3663
+ var shape = z7.discriminatedUnion("kind", [polygonShape, rasterShape]);
3664
+ var mask = z7.object({
1149
3665
  id,
1150
3666
  shape,
1151
- operation: z4.enum(["add", "subtract"]),
1152
- inverted: z4.boolean().default(false),
1153
- feather: z4.number().finite().nonnegative().max(0.25).default(0),
1154
- allowedSupportMargin: z4.number().finite().nonnegative().max(0.25).default(0),
1155
- reviewState: z4.enum(["draft", "approved", "rejected"]),
1156
- provenance: z4.object({
1157
- kind: z4.enum(["manual", "imported", "model-proposed"]),
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"]),
1158
3674
  sourceAssetId: id.optional(),
1159
- modelId: z4.string().min(1).max(160).optional()
3675
+ modelId: z7.string().min(1).max(160).optional()
1160
3676
  }).strict()
1161
3677
  }).strict();
1162
- var transform = z4.object({
1163
- x: z4.number().min(0).max(1),
1164
- y: z4.number().min(0).max(1),
1165
- width: z4.number().positive().max(1),
1166
- height: z4.number().positive().max(1),
1167
- rotationDegrees: z4.number().min(-360).max(360),
1168
- opacity: z4.number().min(0).max(1)
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)
1169
3685
  }).strict();
1170
3686
  var colourParameters = {
1171
- exposure: z4.number().finite().min(-2).max(2).default(0),
1172
- contrast: z4.number().finite().min(0.5).max(2).default(1),
1173
- saturation: z4.number().finite().min(0).max(2).default(1),
1174
- temperature: z4.number().finite().min(-1).max(1).default(0),
1175
- tint: z4.number().finite().min(-1).max(1).default(0)
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)
1176
3692
  };
1177
- var effect = z4.object({
3693
+ var effect = z7.object({
1178
3694
  id,
1179
- kind: z4.literal("local-colour"),
1180
- version: z4.literal(1),
3695
+ kind: z7.literal("local-colour"),
3696
+ version: z7.literal(1),
1181
3697
  maskId: id,
1182
3698
  ...colourParameters
1183
3699
  }).strict();
1184
- var motionPath = z4.object({
3700
+ var motionPath = z7.object({
1185
3701
  id,
1186
- kind: z4.literal("cubic-bezier"),
1187
- startFrame: z4.number().int().nonnegative(),
1188
- endFrame: z4.number().int().positive(),
3702
+ kind: z7.literal("cubic-bezier"),
3703
+ startFrame: z7.number().int().nonnegative(),
3704
+ endFrame: z7.number().int().positive(),
1189
3705
  start: point,
1190
3706
  control1: point,
1191
3707
  control2: point,
1192
3708
  end: point,
1193
- easing: z4.enum(["linear", "ease-in", "ease-out", "ease-in-out"]),
1194
- orientToPath: z4.boolean().default(false)
3709
+ easing: z7.enum(["linear", "ease-in", "ease-out", "ease-in-out"]),
3710
+ orientToPath: z7.boolean().default(false)
1195
3711
  }).strict();
1196
- var region = z4.object({ id, shape, label: z4.string().min(1).max(160) }).strict();
3712
+ var region = z7.object({ id, shape, label: z7.string().min(1).max(160) }).strict();
1197
3713
  var base = { actionId: id, sceneId: id };
1198
- var vendoredDesignActionSchema = z4.discriminatedUnion("kind", [
1199
- z4.object({
3714
+ var vendoredDesignActionSchema = z7.discriminatedUnion("kind", [
3715
+ z7.object({
1200
3716
  ...base,
1201
- kind: z4.literal("set-layer-transform"),
3717
+ kind: z7.literal("set-layer-transform"),
1202
3718
  layerId: id,
1203
3719
  transform
1204
3720
  }).strict(),
1205
- z4.object({
3721
+ z7.object({
1206
3722
  ...base,
1207
- kind: z4.literal("set-local-colour-effect"),
3723
+ kind: z7.literal("set-local-colour-effect"),
1208
3724
  effectId: id,
1209
- parameters: z4.object(colourParameters).strict()
3725
+ parameters: z7.object(colourParameters).strict()
1210
3726
  }).strict(),
1211
- z4.object({ ...base, kind: z4.literal("create-mask"), mask }).strict(),
1212
- z4.object({ ...base, kind: z4.literal("set-mask"), maskId: id, mask }).strict(),
1213
- z4.object({
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({
1214
3730
  ...base,
1215
- kind: z4.literal("set-mask-review-state"),
3731
+ kind: z7.literal("set-mask-review-state"),
1216
3732
  maskId: id,
1217
- reviewState: z4.enum(["draft", "approved", "rejected"])
3733
+ reviewState: z7.enum(["draft", "approved", "rejected"])
1218
3734
  }).strict(),
1219
- z4.object({ ...base, kind: z4.literal("delete-mask"), maskId: id }).strict(),
1220
- z4.object({
3735
+ z7.object({ ...base, kind: z7.literal("delete-mask"), maskId: id }).strict(),
3736
+ z7.object({
1221
3737
  ...base,
1222
- kind: z4.literal("attach-effect"),
3738
+ kind: z7.literal("attach-effect"),
1223
3739
  effect,
1224
- layerIds: z4.array(id).min(1).max(50)
3740
+ layerIds: z7.array(id).min(1).max(50)
1225
3741
  }).strict(),
1226
- z4.object({ ...base, kind: z4.literal("remove-effect"), effectId: id }).strict(),
1227
- z4.object({
3742
+ z7.object({ ...base, kind: z7.literal("remove-effect"), effectId: id }).strict(),
3743
+ z7.object({
1228
3744
  ...base,
1229
- kind: z4.literal("set-layer-text"),
3745
+ kind: z7.literal("set-layer-text"),
1230
3746
  layerId: id,
1231
- text: z4.string().max(1e4)
3747
+ text: z7.string().max(1e4)
1232
3748
  }).strict(),
1233
- z4.object({
3749
+ z7.object({
1234
3750
  ...base,
1235
- kind: z4.literal("set-layer-visibility"),
3751
+ kind: z7.literal("set-layer-visibility"),
1236
3752
  layerId: id,
1237
- visible: z4.boolean()
3753
+ visible: z7.boolean()
1238
3754
  }).strict(),
1239
- z4.object({
3755
+ z7.object({
1240
3756
  ...base,
1241
- kind: z4.literal("set-layer-locked"),
3757
+ kind: z7.literal("set-layer-locked"),
1242
3758
  layerId: id,
1243
- locked: z4.boolean()
3759
+ locked: z7.boolean()
1244
3760
  }).strict(),
1245
- z4.object({
3761
+ z7.object({
1246
3762
  ...base,
1247
- kind: z4.literal("set-layer-mask-ids"),
3763
+ kind: z7.literal("set-layer-mask-ids"),
1248
3764
  layerId: id,
1249
- maskIds: z4.array(id).max(16)
3765
+ maskIds: z7.array(id).max(16)
1250
3766
  }).strict(),
1251
- z4.object({
3767
+ z7.object({
1252
3768
  ...base,
1253
- kind: z4.literal("set-layer-order"),
1254
- orderedLayerIds: z4.array(id).min(1).max(500)
3769
+ kind: z7.literal("set-layer-order"),
3770
+ orderedLayerIds: z7.array(id).min(1).max(500)
1255
3771
  }).strict(),
1256
- z4.object({
3772
+ z7.object({
1257
3773
  ...base,
1258
- kind: z4.literal("set-motion-path"),
3774
+ kind: z7.literal("set-motion-path"),
1259
3775
  path: motionPath,
1260
- layerIds: z4.array(id).max(50).default([])
3776
+ layerIds: z7.array(id).max(50).default([])
1261
3777
  }).strict(),
1262
- z4.object({ ...base, kind: z4.literal("remove-motion-path"), pathId: id }).strict(),
1263
- z4.object({ ...base, kind: z4.literal("set-protected-region"), region }).strict(),
1264
- z4.object({
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({
1265
3781
  ...base,
1266
- kind: z4.literal("remove-protected-region"),
3782
+ kind: z7.literal("remove-protected-region"),
1267
3783
  regionId: id
1268
3784
  }).strict()
1269
3785
  ]);
1270
- var inspectDesignDocumentToolSchema = z4.object({
1271
- projectId: z4.string().uuid(),
1272
- documentId: z4.string().uuid(),
1273
- includeVersions: z4.boolean().default(true)
3786
+ var inspectDesignDocumentToolSchema = z7.object({
3787
+ projectId: z7.string().uuid(),
3788
+ documentId: z7.string().uuid(),
3789
+ includeVersions: z7.boolean().default(true)
1274
3790
  }).strict();
1275
- var applyDesignActionsToolSchema = z4.object({
1276
- projectId: z4.string().uuid(),
1277
- documentId: z4.string().uuid(),
1278
- actionGroupId: z4.string().min(1).max(120),
1279
- expectedVersion: z4.number().int().positive(),
1280
- actions: z4.array(vendoredDesignActionSchema).min(1).max(50),
1281
- clientCreatedAt: z4.string().datetime()
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()
1282
3798
  }).strict();
1283
3799
 
1284
3800
  // src/tools.ts
1285
- var uuid = (desc) => z5.string().uuid().describe(desc);
3801
+ var uuid = (desc) => z8.string().uuid().describe(desc);
1286
3802
  var READ_ONLY = {
1287
3803
  readOnlyHint: true,
1288
3804
  destructiveHint: false,
@@ -1301,9 +3817,9 @@ function registerTools(server) {
1301
3817
  {
1302
3818
  title: "List jobs",
1303
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.",
1304
- inputSchema: {
1305
- limit: z5.number().int().min(1).max(50).default(10).describe("Number of jobs to return (default 10, max 50)"),
1306
- status: z5.enum([
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([
1307
3823
  "queued",
1308
3824
  "rendering",
1309
3825
  "draft_ready",
@@ -1313,7 +3829,7 @@ function registerTools(server) {
1313
3829
  "published",
1314
3830
  "failed"
1315
3831
  ]).optional().describe("Filter by job status")
1316
- },
3832
+ }).strict(),
1317
3833
  annotations: READ_ONLY
1318
3834
  },
1319
3835
  async ({ limit, status }) => {
@@ -1327,7 +3843,7 @@ function registerTools(server) {
1327
3843
  {
1328
3844
  title: "Get job status",
1329
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.",
1330
- inputSchema: { jobId: uuid("The job ID to check") },
3846
+ inputSchema: z8.object({ jobId: uuid("The job ID to check") }).strict(),
1331
3847
  annotations: READ_ONLY
1332
3848
  },
1333
3849
  async ({ jobId }) => runTool(() => apiGet(`/api/v1/jobs/${encodeURIComponent(jobId)}`))
@@ -1337,7 +3853,7 @@ function registerTools(server) {
1337
3853
  {
1338
3854
  title: "Get video state",
1339
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.",
1340
- inputSchema: { jobId: uuid("The job ID") },
3856
+ inputSchema: z8.object({ jobId: uuid("The job ID") }).strict(),
1341
3857
  annotations: READ_ONLY
1342
3858
  },
1343
3859
  async ({ jobId }) => runTool(
@@ -1349,7 +3865,7 @@ function registerTools(server) {
1349
3865
  {
1350
3866
  title: "Diagnose render",
1351
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.",
1352
- inputSchema: { jobId: uuid("The job ID to diagnose") },
3868
+ inputSchema: z8.object({ jobId: uuid("The job ID to diagnose") }).strict(),
1353
3869
  annotations: READ_ONLY
1354
3870
  },
1355
3871
  async ({ jobId }) => runTool(async () => {
@@ -1366,10 +3882,10 @@ function registerTools(server) {
1366
3882
  {
1367
3883
  title: "Get job metrics",
1368
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.",
1369
- inputSchema: {
3885
+ inputSchema: z8.object({
1370
3886
  jobId: uuid("The job ID"),
1371
- limit: z5.number().int().min(1).max(20).default(10).describe("Max metric snapshots (default 10, max 20)")
1372
- },
3887
+ limit: z8.number().int().min(1).max(20).default(10).describe("Max metric snapshots (default 10, max 20)")
3888
+ }).strict(),
1373
3889
  annotations: READ_ONLY
1374
3890
  },
1375
3891
  async ({ jobId, limit }) => runTool(
@@ -1383,12 +3899,12 @@ function registerTools(server) {
1383
3899
  {
1384
3900
  title: "Get cost summary",
1385
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.",
1386
- inputSchema: {
1387
- since: z5.string().optional().describe("ISO date \u2014 only include events after this date"),
1388
- provider: z5.enum(["runpod", "lambda", "r2", "wavespeed", "poyo", "all"]).default("all").describe("Filter by provider"),
1389
- groupBy: z5.enum(["provider", "job", "day"]).default("provider").describe("How to aggregate the summary"),
1390
- includeReconciliation: z5.boolean().default(false).describe("Include account-wide reconciliation events")
1391
- },
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(),
1392
3908
  annotations: READ_ONLY
1393
3909
  },
1394
3910
  async ({ since, provider, groupBy, includeReconciliation }) => {
@@ -1403,7 +3919,7 @@ function registerTools(server) {
1403
3919
  {
1404
3920
  title: "Inspect workflow",
1405
3921
  description: "Inspect the current Studio workflow and its version history for a job.",
1406
- inputSchema: z5.object({ jobId: uuid("The job ID") }).strict(),
3922
+ inputSchema: z8.object({ jobId: uuid("The job ID") }).strict(),
1407
3923
  annotations: READ_ONLY
1408
3924
  },
1409
3925
  async ({ jobId }) => runTool(
@@ -1452,7 +3968,10 @@ function registerTools(server) {
1452
3968
  {
1453
3969
  title: "Create video brief",
1454
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.",
1455
- inputSchema: createBriefShape,
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(),
1456
3975
  annotations: SAFE_WRITE
1457
3976
  },
1458
3977
  async (input) => runTool(() => apiPost(`/api/v1/jobs`, input))
@@ -1462,7 +3981,7 @@ function registerTools(server) {
1462
3981
  {
1463
3982
  title: "Edit video brief",
1464
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.",
1465
- inputSchema: editBriefShape,
3984
+ inputSchema: z8.object(editBriefShape).strict(),
1466
3985
  annotations: SAFE_WRITE
1467
3986
  },
1468
3987
  async (input) => {
@@ -1483,10 +4002,10 @@ function registerTools(server) {
1483
4002
  {
1484
4003
  title: "Draft workflow",
1485
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.",
1486
- inputSchema: z5.object({
4005
+ inputSchema: z8.object({
1487
4006
  jobId: uuid("The job ID"),
1488
- expectedVersion: z5.number().int().nonnegative().describe("Use 0 when no workflow exists yet"),
1489
- document: z5.unknown().describe("The complete Studio workflow 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")
1490
4009
  }).strict(),
1491
4010
  annotations: SAFE_WRITE
1492
4011
  },
@@ -1497,10 +4016,10 @@ function registerTools(server) {
1497
4016
  {
1498
4017
  title: "Patch workflow",
1499
4018
  description: "Create a new workflow version by recursively merging a patch into the current version.",
1500
- inputSchema: z5.object({
4019
+ inputSchema: z8.object({
1501
4020
  jobId: uuid("The job ID"),
1502
- expectedVersion: z5.number().int().positive(),
1503
- patch: z5.record(z5.string(), z5.unknown())
4021
+ expectedVersion: z8.number().int().positive(),
4022
+ patch: z8.record(z8.string(), z8.unknown())
1504
4023
  }).strict(),
1505
4024
  annotations: SAFE_WRITE
1506
4025
  },
@@ -1511,9 +4030,9 @@ function registerTools(server) {
1511
4030
  {
1512
4031
  title: "Validate workflow",
1513
4032
  description: "Validate and compile the inspected workflow version without rendering or publishing it.",
1514
- inputSchema: z5.object({
4033
+ inputSchema: z8.object({
1515
4034
  jobId: uuid("The job ID"),
1516
- expectedVersion: z5.number().int().positive()
4035
+ expectedVersion: z8.number().int().positive()
1517
4036
  }).strict(),
1518
4037
  annotations: SAFE_WRITE
1519
4038
  },
@@ -1524,7 +4043,7 @@ function registerTools(server) {
1524
4043
  {
1525
4044
  title: "Preview workflow",
1526
4045
  description: "Return the current workflow's compiled UniversalVideo brief for a safe pre-render preview.",
1527
- inputSchema: z5.object({ jobId: uuid("The job ID") }).strict(),
4046
+ inputSchema: z8.object({ jobId: uuid("The job ID") }).strict(),
1528
4047
  annotations: READ_ONLY
1529
4048
  },
1530
4049
  async ({ jobId }) => runTool(
@@ -1538,10 +4057,10 @@ function registerTools(server) {
1538
4057
  {
1539
4058
  title: "Compare workflow versions",
1540
4059
  description: "Compare two durable workflow versions field by field.",
1541
- inputSchema: z5.object({
4060
+ inputSchema: z8.object({
1542
4061
  jobId: uuid("The job ID"),
1543
- fromVersion: z5.number().int().positive(),
1544
- toVersion: z5.number().int().positive()
4062
+ fromVersion: z8.number().int().positive(),
4063
+ toVersion: z8.number().int().positive()
1545
4064
  }).strict(),
1546
4065
  annotations: READ_ONLY
1547
4066
  },
@@ -1565,9 +4084,9 @@ function registerTools(server) {
1565
4084
  {
1566
4085
  title: "Approve workflow",
1567
4086
  description: "Validate and approve the inspected workflow version. This does not approve or publish the video job.",
1568
- inputSchema: z5.object({
4087
+ inputSchema: z8.object({
1569
4088
  jobId: uuid("The job ID"),
1570
- expectedVersion: z5.number().int().positive()
4089
+ expectedVersion: z8.number().int().positive()
1571
4090
  }).strict(),
1572
4091
  annotations: {
1573
4092
  readOnlyHint: false,
@@ -1583,11 +4102,11 @@ function registerTools(server) {
1583
4102
  {
1584
4103
  title: "Retry video QA",
1585
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.",
1586
- inputSchema: {
4105
+ inputSchema: z8.object({
1587
4106
  jobId: uuid("The current job ID"),
1588
4107
  runId: uuid("The current render run ID"),
1589
4108
  videoArtifactId: uuid("The current frozen video artifact ID")
1590
- },
4109
+ }).strict(),
1591
4110
  annotations: SAFE_WRITE
1592
4111
  },
1593
4112
  async ({ jobId, runId, videoArtifactId }) => runTool(
@@ -1602,11 +4121,11 @@ function registerTools(server) {
1602
4121
  {
1603
4122
  title: "Audit video QA",
1604
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.",
1605
- inputSchema: {
4124
+ inputSchema: z8.object({
1606
4125
  jobId: uuid("The current job ID"),
1607
4126
  runId: uuid("The current render run ID"),
1608
4127
  videoArtifactId: uuid("The current frozen video artifact ID")
1609
- },
4128
+ }).strict(),
1610
4129
  annotations: SAFE_WRITE
1611
4130
  },
1612
4131
  async ({ jobId, runId, videoArtifactId }) => runTool(
@@ -1621,11 +4140,11 @@ function registerTools(server) {
1621
4140
  {
1622
4141
  title: "Regenerate narration",
1623
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.",
1624
- inputSchema: {
4143
+ inputSchema: z8.object({
1625
4144
  jobId: uuid("The current job ID"),
1626
4145
  sourceRunId: uuid("The current run ID"),
1627
- trigger: z5.enum(["user_requested", "verifier_recommendation"])
1628
- },
4146
+ trigger: z8.enum(["user_requested", "verifier_recommendation"])
4147
+ }).strict(),
1629
4148
  annotations: SAFE_WRITE
1630
4149
  },
1631
4150
  async ({ jobId, sourceRunId, trigger }) => runTool(
@@ -1640,10 +4159,11 @@ function registerTools(server) {
1640
4159
  {
1641
4160
  title: "Approve job",
1642
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.",
1643
- inputSchema: { jobId: uuid("The job ID to approve") },
4162
+ inputSchema: z8.object({ jobId: uuid("The job ID to approve") }).strict(),
1644
4163
  annotations: {
1645
4164
  readOnlyHint: false,
1646
- destructiveHint: false,
4165
+ // Approval revokes active review links, so this is not purely additive.
4166
+ destructiveHint: true,
1647
4167
  // Approving an already-approved job does not change state further.
1648
4168
  idempotentHint: true,
1649
4169
  openWorldHint: false
@@ -1658,18 +4178,30 @@ function registerTools(server) {
1658
4178
  {
1659
4179
  title: "Publish job",
1660
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.",
1661
- inputSchema: {
4181
+ inputSchema: z8.object({
1662
4182
  jobId: uuid("The approved job to publish"),
1663
- youtubePrivacyStatus: z5.enum(["private", "unlisted", "public"]).optional().describe("YouTube only. Use 'private' for safe dogfood runs."),
1664
- tiktokPostMode: z5.enum(["direct_publish", "draft_upload"]).optional().describe("TikTok only. Use 'draft_upload' for safe dogfood runs."),
1665
- connectedAccountDestinationId: z5.string().uuid().optional().describe(
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(
4190
+ "True only after the user confirms the X automation policy."
4191
+ ),
4192
+ confirmXOriginalContent: z8.boolean().optional().describe(
4193
+ "True only after the user confirms ownership or publishing rights."
4194
+ ),
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(
1666
4198
  "Specific destination when the org has multiple accounts for the provider."
1667
4199
  ),
1668
- confirmSyntheticDisclosure: z5.boolean().optional().describe(
4200
+ confirmSyntheticDisclosure: z8.boolean().optional().describe(
1669
4201
  "Set true when the job requires an AI synthetic-media disclosure."
1670
4202
  ),
1671
- confirmQaOverride: z5.boolean().optional().describe("Set true to override a non-passing QA gate.")
1672
- },
4203
+ confirmQaOverride: z8.boolean().optional().describe("Set true to override a non-passing QA gate.")
4204
+ }).strict(),
1673
4205
  annotations: {
1674
4206
  readOnlyHint: false,
1675
4207
  // A live post on an external platform cannot be recalled by the API.
@@ -1690,8 +4222,8 @@ function registerTools(server) {
1690
4222
  {
1691
4223
  title: "Connect social account",
1692
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'.",
1693
- inputSchema: {
1694
- provider: z5.enum([
4225
+ inputSchema: z8.object({
4226
+ provider: z8.enum([
1695
4227
  "youtube",
1696
4228
  "x",
1697
4229
  "tiktok",
@@ -1700,7 +4232,7 @@ function registerTools(server) {
1700
4232
  "instagram",
1701
4233
  "threads"
1702
4234
  ]).describe("The social platform to connect")
1703
- },
4235
+ }).strict(),
1704
4236
  annotations: {
1705
4237
  readOnlyHint: false,
1706
4238
  destructiveHint: false,
@@ -1716,9 +4248,9 @@ function registerTools(server) {
1716
4248
  {
1717
4249
  title: "Check account connection",
1718
4250
  description: "Poll a pending account connection started by connect_account. Returns status 'pending' | 'connected' | 'expired', plus the connected account label once connected.",
1719
- inputSchema: {
4251
+ inputSchema: z8.object({
1720
4252
  pairingId: uuid("The pairingId returned by connect_account")
1721
- },
4253
+ }).strict(),
1722
4254
  annotations: READ_ONLY
1723
4255
  },
1724
4256
  async ({ pairingId }) => runTool(
@@ -1740,9 +4272,9 @@ function registerConfiguredTools(server, apiKey = process.env.INSTAVAR_API_KEY)
1740
4272
  // package.json
1741
4273
  var package_default = {
1742
4274
  name: "@instavar/mcp-server",
1743
- version: "0.5.0",
4275
+ version: "0.7.0",
1744
4276
  mcpName: "com.instavar/mcp-server",
1745
- description: "Make local videos with Instavar Remotion templates for free, then add an API key for hosted Studio workflows.",
4277
+ description: "Make local videos with Instavar templates for Remotion or HyperFrames for free, then add an API key for hosted Studio workflows.",
1746
4278
  type: "module",
1747
4279
  license: "MIT",
1748
4280
  homepage: "https://instavar.com",
@@ -1758,6 +4290,8 @@ var package_default = {
1758
4290
  "model-context-protocol",
1759
4291
  "instavar",
1760
4292
  "video",
4293
+ "remotion",
4294
+ "hyperframes",
1761
4295
  "claude",
1762
4296
  "codex"
1763
4297
  ],
@@ -1817,7 +4351,7 @@ async function main() {
1817
4351
  const server = new McpServer(
1818
4352
  { name: "instavar", version: VERSION },
1819
4353
  {
1820
- 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."
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."
1821
4355
  }
1822
4356
  );
1823
4357
  registerConfiguredTools(server);