@instavar/mcp-server 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -18
- package/dist/index.js +1121 -219
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,13 +1,378 @@
|
|
|
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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[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 registerPairedAnnouncement(server) {
|
|
240
|
+
server.registerTool(
|
|
241
|
+
"prepare_announcement",
|
|
242
|
+
{
|
|
243
|
+
title: "Prepare an announcement in Remotion or HyperFrames",
|
|
244
|
+
description: "Prepare one paired announcement-card-v1 template, continue its caller-held project, edit copy or switch frameworks. Returns source files for local rendering. " + PAIRED_ANNOUNCEMENT_INSTRUCTIONS,
|
|
245
|
+
inputSchema: pairedAnnouncementInput,
|
|
246
|
+
annotations: {
|
|
247
|
+
readOnlyHint: true,
|
|
248
|
+
destructiveHint: false,
|
|
249
|
+
openWorldHint: false
|
|
250
|
+
},
|
|
251
|
+
_meta: { securitySchemes: [{ type: "noauth" }] }
|
|
252
|
+
},
|
|
253
|
+
async (args) => {
|
|
254
|
+
try {
|
|
255
|
+
const result2 = prepareAnnouncement(args);
|
|
256
|
+
return {
|
|
257
|
+
structuredContent: result2,
|
|
258
|
+
content: [{ type: "text", text: JSON.stringify(result2) }]
|
|
259
|
+
};
|
|
260
|
+
} catch (error) {
|
|
261
|
+
return {
|
|
262
|
+
isError: true,
|
|
263
|
+
content: [
|
|
264
|
+
{
|
|
265
|
+
type: "text",
|
|
266
|
+
text: error instanceof Error ? error.message : "Invalid announcement input"
|
|
267
|
+
}
|
|
268
|
+
]
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
3
275
|
// src/index.ts
|
|
4
276
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
277
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
278
|
|
|
279
|
+
// src/creative-guides.ts
|
|
280
|
+
import { z as z2 } from "zod";
|
|
281
|
+
|
|
282
|
+
// src/creative-guides.generated.ts
|
|
283
|
+
var CREATIVE_GUIDES = [
|
|
284
|
+
{
|
|
285
|
+
"id": "visual-copy",
|
|
286
|
+
"title": "Concise plain and poetic visual copy",
|
|
287
|
+
"uri": "instavar://creative-guides/visual-copy",
|
|
288
|
+
"text": "# Write concise text for visual stories\n\nStatus: practical guidance, adapted from documented writing methods and bounded human feedback. This is not a measured guarantee of comprehension or engagement.\n\nUse for carousel headlines, supporting text, captions and video scripts. Begin with plain language. Add poetic expression when the brief asks for an experience or mood; keep instructions, factual claims and interface states literal.\n\n## Start with the viewer and the evidence\n\nName the audience, what they already know, the one idea they should understand, and the action or feeling the piece invites. List supplied facts and necessary qualifications separately from imagined details. Inspect the accompanying image before describing it. If it is unavailable, ask for context or mark the visual description as a proposal.\n\nDecide what the image demonstrates and what the words must add. A caption can rely on an accompanying picture only when the intended viewer receives that picture. Provide a self-contained alternative description for accessibility and text-only reuse.\n\n## Expand, select, connect, compress\n\n1. Expand enough to understand the full thought. Find the concrete objects, actions, relationships, evidence and limits.\n2. Select what must survive. In an explanation, preserve the causal link, units, conditions and uncertainty. In poetic copy, preserve the scene, voice and details that carry the experience.\n3. Connect the ideas in the order a new viewer needs them. Each slide or scene should advance the explanation or experience.\n4. Compress by removing repeated meaning, vague labels and redundant explanations. Read it again against the source. Shorter wording must not make the claim stronger.\n\nTreat brevity as revision pressure, not a word-count target. If a condition matters to the decision, retain it close to that decision. A tiny footnote or a later slide may be missed when the current slide is shared alone.\n\n## Use poetic language selectively\n\nLet a concrete action or sensory detail suggest emotion before naming the emotion. Choose details that belong to this scene. Movement, posture, pressure, temperature, hesitation and small corrections can matter as much as sight or sound. Do not force every sense into a passage.\n\nMetaphor, personification and rhythm should serve one governing idea. Remove mixed metaphors and phrases that could describe any product. Leave room for interpretation in expressive copy, but do not leave factual instructions ambiguous. Do not invent an observed event, testimonial, feeling or product result. Sensuality is an optional requested mode, not a default meaning of poetic.\n\n## Draft examples\n\nThese are newly authored teaching examples, not approved campaign copy.\n\n| Purpose | Weak draft | More concrete draft | What must remain |\n| ------------------- | --------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------- |\n| Product instruction | Experience seamless creative transformation. | Choose a clip. Add your caption. Preview the result. | Name only actions the product actually supports. |\n| Quiet atmosphere | Discover the magic of a new morning. | The kettle clicks. Light reaches the table. | Use only supplied or explicitly imagined scene details. |\n| Evidence summary | This format makes everyone understand faster. | In this review, two readers found the revised chart easier to follow. | Counts, population and evidence source must be real. |\n\n## Deliver copy the creator can use\n\nReturn the title and caption separately. For a carousel or video, give one row per slide or scene: purpose, on-screen text, visual evidence, optional narration, and necessary qualification. Keep narration separate from on-screen copy so the screen need not repeat every spoken sentence.\n\nUse line breaks to shape meaningful groups. A single newline can keep short actions together; a blank line can mark a change of scene or thought. Follow the requested channel formatting. Neither spacing rule is universal.\n\nReview the actual render at the intended viewing size. Read the headline sequence alone, then the complete piece. Ask an unfamiliar viewer what happened, why, and what they would do next. A correct schema or short sentence does not establish reader understanding.\n\n## Reusable request\n\n\u201CUsing the supplied facts and visuals, draft concise copy for [audience] in [carousel/video format]. The viewer should understand [idea]. Use [plain/poetic] language. Give each scene a clear job, keep necessary conditions and evidence, and separate on-screen text from narration. Mark missing facts rather than inventing them. Return title, scene copy, caption and a short source-preservation check.\u201D\n\nSee the companion carousel, design and human-review guides. Source provenance and scope are recorded in the repository's September 2026 creative-playbook transfer report.\n"
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
"id": "carousel-stories",
|
|
292
|
+
"title": "Carousel story structures and transfer limits",
|
|
293
|
+
"uri": "instavar://creative-guides/carousel-stories",
|
|
294
|
+
"text": "# Choose a carousel story structure\n\nStatus: planning recipes adapted from the Eclat carousel programme. Recipes are not newly ported renderers or proof of audience performance.\n\nChoose a structure for the relationship being explained. A topic is not a template: circular motion, collisions and half-life can share a questioning structure while needing different diagrams, conditions and notation.\n\n## Existing Instavar templates\n\nThe HTTP template MCP currently scaffolds `listicle`, `manifesto`, `before-after` and `value-first-reveal`. Use `list_carousel_templates` and `get_carousel_schema` for the executable contract. The npm connector's video tools are a separate catalogue; these recipes do not imply npm carousel rendering support.\n\n- Listicle: parallel points that belong together.\n- Manifesto: one idea per slide in a connected progression.\n- Before/after: a genuine two-state contrast. This builder uses the first and last point; do not put essential intermediate reasoning into points that will be omitted.\n- Value-first reveal: useful information followed by evidence and a restrained product reveal. Do not invent product proof to fill a slot.\n\n## Newer educational recipes\n\n| Recipe | Reading path | Use when | Preserve |\n| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------- |\n| Question, evidence, explanation, check | Ask a specific question; show the relevant objects; explain the relationship; compare a case; ask a final check with its answer | A plausible misconception can be resolved visually | The conditions that make the explanation true; a complete answer |\n| Observation to conclusion | Show an observation; separate it from inference; compare explanations; state what can be concluded | The viewer must reason from visible evidence | Uncertainty and alternative explanations |\n| Worked comparison | Establish objects and quantities; show the symbolic relationship; substitute values; compare outcomes; conclude | A calculation needs to stay connected to the physical situation | Units, signs, definitions and unchanged variables |\n| Action and correction | Show the setup; show an action; show a small correction; show the result | A practical action or sensory moment carries the story | Safe technique, sequence and a truthful visible result |\n\nThese can be drafted with one idea per slide using the manifesto skeleton, then reviewed and edited. Diagrams, equation blocks, editable placeholders and scientific apparatus require an appropriate construction layer; the text scaffold does not create or verify them. Never flatten a necessary diagram into decorative background artwork.\n\n## Lessons from the newer drafts\n\nKeep the relevant objects beside their equations so the viewer can connect symbols to the situation. State a useful symbolic relationship before numerical substitution when that helps the intended reader. Typeset fractions and symbols rather than using ambiguous slash text.\n\nGroup related words with their diagram. Give each calculation slide one main job. Remove duplicate results when the chart already carries them, but retain necessary assumptions. Make supporting text readable at delivery size. Recheck the whole deck after a local change.\n\nThe September 2026 Eclat batch included photoelectric threshold, circular motion, terminal velocity, sticking collision, wave amplitude, SHM turning points, standing waves, single-slit diffraction, field versus potential and half-life. A later six-deck revision applied earlier feedback to circular motion, SHM, standing waves, diffraction, field/potential and half-life. Those revised drafts were awaiting human review in the inspected record. This topic inventory is provenance, not a claim of ten new Instavar templates.\n\n## Delivery check\n\nReview full masters and every intended crop. A centred 4:5 crop of a 9:16 master was an Eclat production choice, not a universal platform requirement. Keep essential text, diagrams and qualifications inside the actual delivery region; verify current platform constraints separately before publication.\n\nTransfer the story method. Choose branding, type, texture, colour and imagery for the current creator. Eclat logos, source images, specialist answers and approval labels do not transfer automatically.\n"
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
"id": "visual-design",
|
|
298
|
+
"title": "UI, graphic design and video principles",
|
|
299
|
+
"uri": "instavar://creative-guides/visual-design",
|
|
300
|
+
"text": "# Make meaning visible across slides, video and interfaces\n\nStatus: practical design and review guidance. Context-specific aesthetic and outcome claims still need audience evidence.\n\n## Give attention a useful path\n\nStart with the audience's task. Identify what should be noticed first, what it means, where attention moves next, and what evidence supports the conclusion. Use hierarchy to express that path. The first meaningful text should usually carry the main idea; a small label above it earns its place only when it changes interpretation.\n\nMatch form to relationship: sequence for steps, side-by-side comparison for alternatives, a diagram for a mechanism, and an explicit state change for an action. Do not force every relationship into the same card layout. Make the heading sequence tell a connected story while the full reading supplies proof and limits.\n\n## Graphic design\n\nUse proximity, alignment, spacing, scale, weight and contrast to show grouping and priority. Keep type and colour roles consistent. Test text against its actual background and at its final size; do not assume a colour token alone guarantees contrast. Keep non-colour cues for important distinctions.\n\nTypography and colour can suggest tone, but their meanings depend on audience and context. A serif does not prove quality and a particular hue does not guarantee trust. Choose an expressive treatment only when the core content remains legible.\n\nReview local details, the whole image and the collection separately. A sharper object or stronger accent may weaken overall harmony. Compare against the verified source image; a file labelled original can still be an edited derivative. Retaining the original is a valid outcome.\n\n## Video and motion\n\nPlan what changes in the viewer's understanding across time. Show the setup before an action that requires it. Keep spatial and causal continuity when the viewer must follow a procedure. Use cuts, movement and sound to reveal a relationship or shift attention, not simply to add activity.\n\nGive viewers enough time to read the actual text and inspect the evidence. Test at normal playback speed. Treat muted review, picture review, narration review and the combined sound mix as separate checks. Time visible contact and its sound together when synchrony matters. Keep necessary information available when audio is absent.\n\nDo not import film pacing into interactive controls blindly. A film controls its timeline; an interface must preserve user agency, orientation and recovery.\n\n## UI and editing workflows\n\nMake the current object, possible action, saved state and consequence clear. Keep frequent controls visible at the intended viewport. Distinguish selection, dragging and text editing. Provide undo and understandable conflict handling for editable work. A preview and its export should reflect the same saved document.\n\nThese are review questions, not claims that the Eclat prototype's editor has been integrated into Instavar. Prototype gestures, exact button layouts and crop choices require fresh implementation and testing in the receiving product.\n\n## Verify the artifact\n\nInspect final-size text, crops, diagram labels, symbols, contrast, grouping and the overall sequence. For interfaces, also exercise keyboard use, focus, narrow viewports, saving, errors and recovery. For video, review timing and sound in playback rather than relying only on contact sheets.\n\nAutomated checks establish their measured properties, such as dimensions, parsing or bounds. They do not establish human preference, comprehension or campaign performance. Ask a new viewer to explain the idea or perform the intended task before making those stronger claims.\n"
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
"id": "human-review",
|
|
304
|
+
"title": "Human-guided copy, visual and media review",
|
|
305
|
+
"uri": "instavar://creative-guides/human-review",
|
|
306
|
+
"text": "# Preserve human feedback without inventing approval\n\nStatus: repository adaptation of the shared copy, UI/UX and media recording protocols. This guide is read-only; it does not store feedback or publish media.\n\nRecord a meaningful transition when a person reviews an identifiable draft, gives concrete feedback and a later revision responds. Preserve the exact draft and context, the feedback, the revision and the follow-up judgment. Do not treat every autonomous adjustment as human-guided work.\n\n## Keep one owning record\n\nUse an existing workstream report when available. Otherwise use `reports/copy/<workstream>/` for wording, `reports/ui-ux/` for layout and controls, or `reports/media/<workstream>/` for rendered picture, pacing, narration and sound. Link overlapping records instead of duplicating their histories. Keep old evidence at its existing paths.\n\nFor copy, preserve exact text, punctuation and line breaks. Record title and caption separately. For visual work, identify slide, route, state, viewport and before/after render. For media, also identify seconds or frames and playback conditions, including whether sound was on. If evidence is unavailable or a revision is source-only, say so.\n\n## Record template\n\n- Artifact and audience:\n- Before version, exact text or safe render reference:\n- Reviewed context, slide/range, viewport or playback conditions:\n- Human feedback, exact wording when safe:\n- Perceived problem and requested change:\n- Edit hypothesis:\n- Revision and after version:\n- Kept stable:\n- Follow-up judgment: not_judged / accepted_in_scope / rejected / mixed / superseded\n- Acceptance scope:\n- Publication: not_requested / requested / authorized / sent / verified, with evidence\n- Audience outcome: not_measured, or the actual observation and its limits\n- Source provenance, rights status and safe evidence references:\n- New issue and successor reference, if any:\n\nUse superseded only with a successor and keep the earlier judgment. A new complaint begins a new transition; chronology alone does not prove the previous change caused it.\n\n## What transfers\n\nKeep the problem-solving principle distinct from the particular remedy. \u201CThese calculation slides look cluttered\u201D can motivate grouping and removing repeated results. It does not justify removing assumptions from all science slides. A preference for one background, font size or line break applies to the reviewed context until further evidence supports transfer.\n\nApproval of a local detail is not approval of a whole image. Approval of a silent video does not approve the mix. Delivery is not approval, and approval is not evidence of audience impact. Silence remains not_judged.\n\nUse only public, owned or cleared examples in shared resources. Do not copy private screenshots, raw conversations, source photographs or unrelated account details to complete a record. A file hash identifies bytes; it does not establish rights, accuracy or quality. Recording feedback does not authorize publication or admission to a training or taste-critic corpus.\n"
|
|
307
|
+
}
|
|
308
|
+
];
|
|
309
|
+
|
|
310
|
+
// src/creative-guides.ts
|
|
311
|
+
var creativeGuideIdSchema = z2.enum([
|
|
312
|
+
"visual-copy",
|
|
313
|
+
"carousel-stories",
|
|
314
|
+
"visual-design",
|
|
315
|
+
"human-review"
|
|
316
|
+
]);
|
|
317
|
+
var CREATIVE_GUIDE_INDEX = CREATIVE_GUIDES.map(
|
|
318
|
+
({ id: id2, title, uri }) => ({
|
|
319
|
+
id: id2,
|
|
320
|
+
title,
|
|
321
|
+
uri
|
|
322
|
+
})
|
|
323
|
+
);
|
|
324
|
+
function registerCreativeGuides(server) {
|
|
325
|
+
for (const guide of CREATIVE_GUIDES) {
|
|
326
|
+
server.registerResource(
|
|
327
|
+
guide.id,
|
|
328
|
+
guide.uri,
|
|
329
|
+
{
|
|
330
|
+
title: guide.title,
|
|
331
|
+
description: guide.title,
|
|
332
|
+
mimeType: "text/markdown"
|
|
333
|
+
},
|
|
334
|
+
async () => ({
|
|
335
|
+
contents: [
|
|
336
|
+
{ uri: guide.uri, mimeType: "text/markdown", text: guide.text }
|
|
337
|
+
]
|
|
338
|
+
})
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
server.registerTool(
|
|
342
|
+
"get_creative_guide",
|
|
343
|
+
{
|
|
344
|
+
title: "Get a visual communication guide",
|
|
345
|
+
description: "Read practical guidance before drafting concise plain or poetic copy, choosing carousel stories, designing visuals or recording human feedback. Returns guidance for the calling assistant to apply, not generated copy or saved feedback. Omit guideId to list guides.",
|
|
346
|
+
inputSchema: { guideId: creativeGuideIdSchema.optional() },
|
|
347
|
+
annotations: {
|
|
348
|
+
readOnlyHint: true,
|
|
349
|
+
destructiveHint: false,
|
|
350
|
+
openWorldHint: false
|
|
351
|
+
},
|
|
352
|
+
_meta: { securitySchemes: [{ type: "noauth" }] }
|
|
353
|
+
},
|
|
354
|
+
async ({ guideId }) => {
|
|
355
|
+
const guide = CREATIVE_GUIDES.find((entry) => entry.id === guideId);
|
|
356
|
+
return {
|
|
357
|
+
structuredContent: guide ? { guide } : { guides: CREATIVE_GUIDE_INDEX },
|
|
358
|
+
content: [
|
|
359
|
+
{
|
|
360
|
+
type: "text",
|
|
361
|
+
text: guide ? guide.text : CREATIVE_GUIDE_INDEX.map(
|
|
362
|
+
(entry) => `${entry.id}: ${entry.title} (${entry.uri})`
|
|
363
|
+
).join("\n")
|
|
364
|
+
}
|
|
365
|
+
]
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
7
371
|
// src/public-templates.ts
|
|
8
|
-
import { z } from "zod";
|
|
372
|
+
import { z as z3 } from "zod";
|
|
9
373
|
var TEMPLATE_REPOSITORY = "https://github.com/instavar/remotion-templates";
|
|
10
374
|
var TEMPLATE_ENDPOINT = "https://instavar.com/api/mcp/templates";
|
|
375
|
+
var VOICE_STUDIO_URL = "https://instavar.com/voice";
|
|
11
376
|
var templateIds = [
|
|
12
377
|
"qa-ad",
|
|
13
378
|
"proof-walkthrough",
|
|
@@ -15,8 +380,8 @@ var templateIds = [
|
|
|
15
380
|
"social-remix",
|
|
16
381
|
"finance-brief"
|
|
17
382
|
];
|
|
18
|
-
var templateIdSchema =
|
|
19
|
-
var aspectSchema =
|
|
383
|
+
var templateIdSchema = z3.enum(templateIds);
|
|
384
|
+
var aspectSchema = z3.enum(["9:16", "4:5", "1:1"]);
|
|
20
385
|
var templates = [
|
|
21
386
|
{
|
|
22
387
|
id: "qa-ad",
|
|
@@ -86,6 +451,42 @@ var videoSpecSchema = {
|
|
|
86
451
|
fixedDurationSec: { type: "number", exclusiveMinimum: 0 }
|
|
87
452
|
}
|
|
88
453
|
},
|
|
454
|
+
audio: {
|
|
455
|
+
type: "object",
|
|
456
|
+
properties: {
|
|
457
|
+
mode: { enum: ["auto", "narration", "music", "silent"] },
|
|
458
|
+
narrationText: { type: "string" },
|
|
459
|
+
narrationSrc: { type: "string" },
|
|
460
|
+
narrationVolume: { type: "number", minimum: 0, maximum: 1 },
|
|
461
|
+
musicSrc: { type: "string" },
|
|
462
|
+
musicVolume: { type: "number", minimum: 0, maximum: 1 },
|
|
463
|
+
ducking: { type: "boolean" },
|
|
464
|
+
narrationTiming: {
|
|
465
|
+
type: "object",
|
|
466
|
+
required: ["schemaVersion", "source", "durationMs", "segments"],
|
|
467
|
+
properties: {
|
|
468
|
+
schemaVersion: { const: "1.0" },
|
|
469
|
+
source: { const: "segmented-synthesis" },
|
|
470
|
+
durationMs: { type: "integer", minimum: 1 },
|
|
471
|
+
segments: {
|
|
472
|
+
type: "array",
|
|
473
|
+
minItems: 1,
|
|
474
|
+
maxItems: 24,
|
|
475
|
+
items: {
|
|
476
|
+
type: "object",
|
|
477
|
+
required: ["sceneId", "startMs", "endMs", "durationMs"],
|
|
478
|
+
properties: {
|
|
479
|
+
sceneId: { type: "string", minLength: 1 },
|
|
480
|
+
startMs: { type: "integer", minimum: 0 },
|
|
481
|
+
endMs: { type: "integer", minimum: 1 },
|
|
482
|
+
durationMs: { type: "integer", minimum: 1 }
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
},
|
|
89
490
|
scenes: {
|
|
90
491
|
type: "array",
|
|
91
492
|
minItems: 1,
|
|
@@ -97,6 +498,8 @@ var videoSpecSchema = {
|
|
|
97
498
|
kind: {
|
|
98
499
|
enum: [
|
|
99
500
|
"hero",
|
|
501
|
+
"statement",
|
|
502
|
+
"mechanism",
|
|
100
503
|
"points",
|
|
101
504
|
"equation",
|
|
102
505
|
"video-window",
|
|
@@ -108,6 +511,17 @@ var videoSpecSchema = {
|
|
|
108
511
|
timing: { type: "object" }
|
|
109
512
|
}
|
|
110
513
|
}
|
|
514
|
+
},
|
|
515
|
+
style: {
|
|
516
|
+
type: "object",
|
|
517
|
+
properties: {
|
|
518
|
+
theme: {
|
|
519
|
+
enum: ["default", "editorial-light", "editorial-dark", "clinical"]
|
|
520
|
+
},
|
|
521
|
+
variant: { type: "string" },
|
|
522
|
+
safeAreaProfile: { enum: ["baseline", "metaSafe"] },
|
|
523
|
+
showSceneLabels: { type: "boolean", default: false }
|
|
524
|
+
}
|
|
111
525
|
}
|
|
112
526
|
}
|
|
113
527
|
};
|
|
@@ -134,35 +548,77 @@ function scaffoldVideoSpec(args) {
|
|
|
134
548
|
content: {
|
|
135
549
|
title: args.title,
|
|
136
550
|
subtitle: "Replace with a concise promise"
|
|
137
|
-
}
|
|
551
|
+
},
|
|
552
|
+
timing: { durationMs: 4e3 }
|
|
138
553
|
}
|
|
139
554
|
];
|
|
140
555
|
if (args.templateId === "qa-ad" || args.templateId === "social-remix") {
|
|
141
556
|
scenes.push({
|
|
142
557
|
id: "source-video",
|
|
143
558
|
kind: "video-window",
|
|
144
|
-
content: {
|
|
559
|
+
content: {
|
|
560
|
+
title: "See it in context",
|
|
561
|
+
src: args.mediaSrc ?? "public/replace-with-your-video.mp4"
|
|
562
|
+
},
|
|
563
|
+
timing: { durationMs: 7e3 }
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
if (args.templateId === "announcement-brief") {
|
|
567
|
+
scenes.push(
|
|
568
|
+
{
|
|
569
|
+
id: "context",
|
|
570
|
+
kind: "statement",
|
|
571
|
+
content: {
|
|
572
|
+
title: "Name the change",
|
|
573
|
+
subtitle: "Explain why it matters in one sentence"
|
|
574
|
+
},
|
|
575
|
+
timing: { durationMs: 6e3 }
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
id: "mechanism",
|
|
579
|
+
kind: "mechanism",
|
|
580
|
+
content: {
|
|
581
|
+
title: "Show how the change happens",
|
|
582
|
+
before: "Before",
|
|
583
|
+
after: "After"
|
|
584
|
+
},
|
|
585
|
+
timing: { durationMs: 7e3 }
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
id: "outcomes",
|
|
589
|
+
kind: "points",
|
|
590
|
+
content: {
|
|
591
|
+
title: "What changes",
|
|
592
|
+
points: ["First outcome", "Second outcome", "Third outcome"]
|
|
593
|
+
},
|
|
594
|
+
timing: { durationMs: 7e3 }
|
|
595
|
+
}
|
|
596
|
+
);
|
|
597
|
+
} else {
|
|
598
|
+
scenes.push({
|
|
599
|
+
id: args.templateId === "proof-walkthrough" ? "working" : "key-points",
|
|
600
|
+
kind: args.templateId === "proof-walkthrough" ? "equation" : "points",
|
|
601
|
+
content: args.templateId === "proof-walkthrough" ? {
|
|
602
|
+
title: "Work through the idea",
|
|
603
|
+
latexBlocks: ["x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}"]
|
|
604
|
+
} : {
|
|
605
|
+
title: "Key points",
|
|
606
|
+
points: [
|
|
607
|
+
"Replace with evidence",
|
|
608
|
+
"Replace with explanation",
|
|
609
|
+
"Replace with takeaway"
|
|
610
|
+
]
|
|
611
|
+
},
|
|
612
|
+
timing: { durationMs: 8e3 }
|
|
145
613
|
});
|
|
146
614
|
}
|
|
147
|
-
scenes.push({
|
|
148
|
-
id: args.templateId === "proof-walkthrough" ? "working" : "key-points",
|
|
149
|
-
kind: args.templateId === "proof-walkthrough" ? "equation" : "points",
|
|
150
|
-
content: args.templateId === "proof-walkthrough" ? {
|
|
151
|
-
title: "Work through the idea",
|
|
152
|
-
latexBlocks: ["x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}"]
|
|
153
|
-
} : {
|
|
154
|
-
title: "Key points",
|
|
155
|
-
points: [
|
|
156
|
-
"Replace with evidence",
|
|
157
|
-
"Replace with explanation",
|
|
158
|
-
"Replace with takeaway"
|
|
159
|
-
]
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
615
|
scenes.push({
|
|
163
616
|
id: "cta",
|
|
164
617
|
kind: "cta",
|
|
165
|
-
content: { line1: "Replace with the next action" }
|
|
618
|
+
content: { line1: "Replace with the next action" },
|
|
619
|
+
timing: {
|
|
620
|
+
durationMs: args.templateId === "announcement-brief" ? 6e3 : 5e3
|
|
621
|
+
}
|
|
166
622
|
});
|
|
167
623
|
return {
|
|
168
624
|
schemaVersion: "1.0",
|
|
@@ -171,16 +627,25 @@ function scaffoldVideoSpec(args) {
|
|
|
171
627
|
meta: { title: args.title, tags: [] },
|
|
172
628
|
target: { aspect: args.aspect, fps: 30, durationMode: "auto" },
|
|
173
629
|
style: {
|
|
174
|
-
theme: "default",
|
|
630
|
+
theme: args.templateId === "announcement-brief" ? "editorial-light" : "default",
|
|
175
631
|
variant: "default",
|
|
176
|
-
safeAreaProfile: "metaSafe"
|
|
632
|
+
safeAreaProfile: "metaSafe",
|
|
633
|
+
showSceneLabels: false
|
|
634
|
+
},
|
|
635
|
+
audio: {
|
|
636
|
+
mode: "silent",
|
|
637
|
+
narrationText: "",
|
|
638
|
+
narrationVolume: 1,
|
|
639
|
+
musicVolume: 0.16,
|
|
640
|
+
ducking: true
|
|
177
641
|
},
|
|
178
|
-
audio: { ducking: true },
|
|
179
642
|
assets: {},
|
|
180
643
|
scenes
|
|
181
644
|
};
|
|
182
645
|
}
|
|
183
646
|
function registerPublicTemplateTools(server) {
|
|
647
|
+
registerCreativeGuides(server);
|
|
648
|
+
registerPairedAnnouncement(server);
|
|
184
649
|
server.registerTool(
|
|
185
650
|
"get_capabilities",
|
|
186
651
|
{
|
|
@@ -195,13 +660,24 @@ function registerPublicTemplateTools(server) {
|
|
|
195
660
|
apiKeyConfigured: Boolean(process.env.INSTAVAR_API_KEY?.trim()),
|
|
196
661
|
repository: TEMPLATE_REPOSITORY,
|
|
197
662
|
endpoint: TEMPLATE_ENDPOINT,
|
|
663
|
+
creativeGuides: CREATIVE_GUIDE_INDEX,
|
|
664
|
+
pairedAnnouncement: PAIRED_ANNOUNCEMENT_CAPABILITY,
|
|
198
665
|
localFeatures: [
|
|
199
666
|
"template discovery",
|
|
200
667
|
"VideoSpec schema and examples",
|
|
201
668
|
"deterministic scaffolding",
|
|
202
669
|
"local render instructions",
|
|
203
|
-
"render error diagnosis"
|
|
670
|
+
"render error diagnosis",
|
|
671
|
+
"keyless on-device narration with scene timing",
|
|
672
|
+
"every-scene preview frames",
|
|
673
|
+
"post-render audible-audio verification"
|
|
204
674
|
],
|
|
675
|
+
narration: {
|
|
676
|
+
url: VOICE_STUDIO_URL,
|
|
677
|
+
cost: "free",
|
|
678
|
+
apiKeyRequired: false,
|
|
679
|
+
guidance: "Write one paragraph per scene. Download the WAV and timing manifest, then apply both before rendering."
|
|
680
|
+
},
|
|
205
681
|
accountFeatures: [
|
|
206
682
|
"managed rendering and storage",
|
|
207
683
|
"workflow review and approval",
|
|
@@ -284,9 +760,9 @@ ${template?.description ?? ""}`
|
|
|
284
760
|
description: "Return a deterministic VideoSpec skeleton without writing files.",
|
|
285
761
|
inputSchema: {
|
|
286
762
|
templateId: templateIdSchema,
|
|
287
|
-
title:
|
|
763
|
+
title: z3.string().min(1).max(160),
|
|
288
764
|
aspect: aspectSchema.default("9:16"),
|
|
289
|
-
mediaSrc:
|
|
765
|
+
mediaSrc: z3.string().min(1).max(500).optional()
|
|
290
766
|
},
|
|
291
767
|
annotations
|
|
292
768
|
},
|
|
@@ -304,7 +780,7 @@ ${template?.description ?? ""}`
|
|
|
304
780
|
title: "Get local render instructions",
|
|
305
781
|
description: "Return clone, install, validate, preview, and render commands.",
|
|
306
782
|
inputSchema: {
|
|
307
|
-
client:
|
|
783
|
+
client: z3.enum(["codex", "claude-code", "manual"]).default("manual")
|
|
308
784
|
},
|
|
309
785
|
annotations
|
|
310
786
|
},
|
|
@@ -313,9 +789,12 @@ ${template?.description ?? ""}`
|
|
|
313
789
|
`git clone ${TEMPLATE_REPOSITORY}.git`,
|
|
314
790
|
"cd remotion-templates",
|
|
315
791
|
"npm install",
|
|
792
|
+
`# Generate one narration paragraph per scene at ${VOICE_STUDIO_URL}`,
|
|
793
|
+
"npm run video:timing -- path/to/video.video.json --audio public/generated/narration.wav --manifest path/to/narration.timing.json",
|
|
316
794
|
"npm run video:validate -- path/to/video.video.json",
|
|
317
795
|
"npm run video:preview -- path/to/video.video.json",
|
|
318
|
-
"npm run video:render -- path/to/video.video.json"
|
|
796
|
+
"npm run video:render -- path/to/video.video.json",
|
|
797
|
+
"npm run video:qa -- path/to/video.video.json path/to/video.mp4"
|
|
319
798
|
];
|
|
320
799
|
return result(
|
|
321
800
|
{ repository: TEMPLATE_REPOSITORY, client, commands },
|
|
@@ -328,12 +807,12 @@ ${template?.description ?? ""}`
|
|
|
328
807
|
{
|
|
329
808
|
title: "Diagnose render error",
|
|
330
809
|
description: "Classify a local validation or render error.",
|
|
331
|
-
inputSchema: { errorText:
|
|
810
|
+
inputSchema: { errorText: z3.string().min(1).max(1e4) },
|
|
332
811
|
annotations
|
|
333
812
|
},
|
|
334
813
|
async ({ errorText }) => {
|
|
335
|
-
const
|
|
336
|
-
const code =
|
|
814
|
+
const text2 = errorText.toLowerCase();
|
|
815
|
+
const code = text2.includes("enoent") || text2.includes("not found") ? "missing-file" : text2.includes("ffmpeg") || text2.includes("codec") ? "media-codec" : text2.includes("schema") || text2.includes("invalid") || text2.includes("zod") ? "invalid-video-spec" : text2.includes("chrome") || text2.includes("browser") ? "browser-runtime" : "unknown-render-error";
|
|
337
816
|
return result(
|
|
338
817
|
{
|
|
339
818
|
code,
|
|
@@ -346,7 +825,7 @@ ${template?.description ?? ""}`
|
|
|
346
825
|
}
|
|
347
826
|
|
|
348
827
|
// src/tools.ts
|
|
349
|
-
import { z as
|
|
828
|
+
import { z as z6 } from "zod";
|
|
350
829
|
|
|
351
830
|
// src/http.ts
|
|
352
831
|
import { randomUUID } from "crypto";
|
|
@@ -392,11 +871,11 @@ function requestFailure(error) {
|
|
|
392
871
|
return error instanceof Error ? error.message : String(error);
|
|
393
872
|
}
|
|
394
873
|
async function parse(res) {
|
|
395
|
-
const
|
|
874
|
+
const text2 = await res.text();
|
|
396
875
|
try {
|
|
397
|
-
return JSON.parse(
|
|
876
|
+
return JSON.parse(text2);
|
|
398
877
|
} catch {
|
|
399
|
-
return
|
|
878
|
+
return text2;
|
|
400
879
|
}
|
|
401
880
|
}
|
|
402
881
|
async function apiGet(path) {
|
|
@@ -420,7 +899,9 @@ async function apiPost(path, payload) {
|
|
|
420
899
|
method: "POST",
|
|
421
900
|
headers: {
|
|
422
901
|
[API_KEY_HEADER]: getApiKey(),
|
|
423
|
-
"content-type": "application/json"
|
|
902
|
+
"content-type": "application/json",
|
|
903
|
+
// Labels person-consent declarations made through MCP (surface only).
|
|
904
|
+
"x-instavar-client": "mcp"
|
|
424
905
|
},
|
|
425
906
|
body: JSON.stringify(clean),
|
|
426
907
|
signal: AbortSignal.timeout(API_REQUEST_TIMEOUT_MS)
|
|
@@ -470,7 +951,7 @@ async function runTool(fn) {
|
|
|
470
951
|
}
|
|
471
952
|
|
|
472
953
|
// src/brief-schema.ts
|
|
473
|
-
import { z as
|
|
954
|
+
import { z as z4 } from "zod";
|
|
474
955
|
var PUBLISH_TARGETS = [
|
|
475
956
|
"linkedin",
|
|
476
957
|
"x",
|
|
@@ -502,16 +983,16 @@ var REFERENCE_ROLES = [
|
|
|
502
983
|
"environment",
|
|
503
984
|
"style"
|
|
504
985
|
];
|
|
505
|
-
var optionalDirectionText =
|
|
506
|
-
var visualDirectionSchema =
|
|
986
|
+
var optionalDirectionText = z4.string().trim().min(1).max(500).optional();
|
|
987
|
+
var visualDirectionSchema = z4.object({
|
|
507
988
|
communicationJob: optionalDirectionText,
|
|
508
989
|
emotionalTarget: optionalDirectionText,
|
|
509
|
-
attentionPath:
|
|
990
|
+
attentionPath: z4.object({
|
|
510
991
|
first: optionalDirectionText,
|
|
511
992
|
next: optionalDirectionText,
|
|
512
993
|
proof: optionalDirectionText
|
|
513
994
|
}).optional(),
|
|
514
|
-
visualLanguage:
|
|
995
|
+
visualLanguage: z4.object({
|
|
515
996
|
space: optionalDirectionText,
|
|
516
997
|
line: optionalDirectionText,
|
|
517
998
|
shape: optionalDirectionText,
|
|
@@ -520,24 +1001,24 @@ var visualDirectionSchema = z2.object({
|
|
|
520
1001
|
movement: optionalDirectionText,
|
|
521
1002
|
rhythm: optionalDirectionText
|
|
522
1003
|
}).optional(),
|
|
523
|
-
camera:
|
|
1004
|
+
camera: z4.object({
|
|
524
1005
|
shotSize: optionalDirectionText,
|
|
525
1006
|
angle: optionalDirectionText,
|
|
526
1007
|
movement: optionalDirectionText,
|
|
527
1008
|
lens: optionalDirectionText
|
|
528
1009
|
}).optional()
|
|
529
1010
|
});
|
|
530
|
-
var generationContractSchema =
|
|
531
|
-
globalInvariants:
|
|
532
|
-
shotDelta:
|
|
533
|
-
referenceBindings:
|
|
534
|
-
|
|
535
|
-
assetId:
|
|
536
|
-
roles:
|
|
1011
|
+
var generationContractSchema = z4.object({
|
|
1012
|
+
globalInvariants: z4.array(z4.string().trim().min(1).max(300)).max(20).optional(),
|
|
1013
|
+
shotDelta: z4.string().trim().min(1).max(500).optional(),
|
|
1014
|
+
referenceBindings: z4.array(
|
|
1015
|
+
z4.object({
|
|
1016
|
+
assetId: z4.string().trim().min(1).max(200),
|
|
1017
|
+
roles: z4.array(z4.enum(REFERENCE_ROLES)).min(1).max(7)
|
|
537
1018
|
})
|
|
538
1019
|
).max(20).optional(),
|
|
539
|
-
prohibitedChanges:
|
|
540
|
-
reviewCriteria:
|
|
1020
|
+
prohibitedChanges: z4.array(z4.string().trim().min(1).max(300)).max(20).optional(),
|
|
1021
|
+
reviewCriteria: z4.array(z4.string().trim().min(1).max(300)).max(20).optional()
|
|
541
1022
|
});
|
|
542
1023
|
var REGION_NAMES = [
|
|
543
1024
|
"background",
|
|
@@ -554,47 +1035,92 @@ var ASSET_ROLES = [
|
|
|
554
1035
|
"lower-third",
|
|
555
1036
|
"diagram-node"
|
|
556
1037
|
];
|
|
557
|
-
var assetSlotStyleSchema =
|
|
558
|
-
opacity:
|
|
559
|
-
fit:
|
|
560
|
-
z:
|
|
1038
|
+
var assetSlotStyleSchema = z4.object({
|
|
1039
|
+
opacity: z4.number().min(0).max(1).optional(),
|
|
1040
|
+
fit: z4.enum(["cover", "contain"]).optional(),
|
|
1041
|
+
z: z4.number().optional()
|
|
561
1042
|
}).strict();
|
|
562
|
-
var assetSlotSchema =
|
|
563
|
-
id:
|
|
564
|
-
sceneId:
|
|
565
|
-
region:
|
|
566
|
-
role:
|
|
567
|
-
kind:
|
|
568
|
-
source:
|
|
569
|
-
intent:
|
|
1043
|
+
var assetSlotSchema = z4.object({
|
|
1044
|
+
id: z4.string().min(1),
|
|
1045
|
+
sceneId: z4.string().min(1).optional(),
|
|
1046
|
+
region: z4.enum(REGION_NAMES),
|
|
1047
|
+
role: z4.enum(ASSET_ROLES),
|
|
1048
|
+
kind: z4.enum(["image", "video"]),
|
|
1049
|
+
source: z4.enum(["auto", "stock", "generate"]),
|
|
1050
|
+
intent: z4.string(),
|
|
570
1051
|
style: assetSlotStyleSchema.optional(),
|
|
571
|
-
timing:
|
|
572
|
-
fromPct:
|
|
573
|
-
toPct:
|
|
1052
|
+
timing: z4.object({
|
|
1053
|
+
fromPct: z4.number().min(0).max(1).optional(),
|
|
1054
|
+
toPct: z4.number().min(0).max(1).optional()
|
|
574
1055
|
}).strict().optional(),
|
|
575
|
-
url:
|
|
1056
|
+
url: z4.string().url().optional()
|
|
576
1057
|
}).strict();
|
|
577
|
-
var briefStepSchema =
|
|
578
|
-
label:
|
|
579
|
-
latex:
|
|
580
|
-
explanation:
|
|
1058
|
+
var briefStepSchema = z4.object({
|
|
1059
|
+
label: z4.string(),
|
|
1060
|
+
latex: z4.string().optional(),
|
|
1061
|
+
explanation: z4.string().optional()
|
|
581
1062
|
});
|
|
582
|
-
var briefAudioLayerSchema =
|
|
583
|
-
id:
|
|
584
|
-
src:
|
|
585
|
-
startFrame:
|
|
586
|
-
durationInFrames:
|
|
587
|
-
volume:
|
|
588
|
-
fadeFrames:
|
|
589
|
-
role:
|
|
1063
|
+
var briefAudioLayerSchema = z4.object({
|
|
1064
|
+
id: z4.string().min(1),
|
|
1065
|
+
src: z4.string().url(),
|
|
1066
|
+
startFrame: z4.number().int().nonnegative().default(0),
|
|
1067
|
+
durationInFrames: z4.number().int().positive().optional(),
|
|
1068
|
+
volume: z4.number().min(0).max(1).default(1),
|
|
1069
|
+
fadeFrames: z4.number().int().nonnegative().default(0),
|
|
1070
|
+
role: z4.enum(["music", "foley", "transition", "effect"])
|
|
590
1071
|
});
|
|
591
|
-
var
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
1072
|
+
var protectedRegionEffectSchema = z4.object({
|
|
1073
|
+
id: z4.string().min(1).max(80),
|
|
1074
|
+
kind: z4.literal("border-glint"),
|
|
1075
|
+
coordinateSpace: z4.literal("output-frame-v1").default("output-frame-v1"),
|
|
1076
|
+
region: z4.object({
|
|
1077
|
+
leftPercent: z4.number().min(0).max(100),
|
|
1078
|
+
topPercent: z4.number().min(0).max(100),
|
|
1079
|
+
widthPercent: z4.number().positive().max(100),
|
|
1080
|
+
heightPercent: z4.number().positive().max(100)
|
|
1081
|
+
}),
|
|
1082
|
+
startFrame: z4.number().int().nonnegative().default(0),
|
|
1083
|
+
endFrame: z4.number().int().positive(),
|
|
1084
|
+
speedUpFrame: z4.number().int().nonnegative().optional(),
|
|
1085
|
+
color: z4.string().max(64).default("#ffffff"),
|
|
1086
|
+
thicknessPercent: z4.number().positive().max(20).default(3)
|
|
1087
|
+
}).superRefine((effect2, context) => {
|
|
1088
|
+
if (effect2.region.leftPercent + effect2.region.widthPercent > 100) {
|
|
1089
|
+
context.addIssue({
|
|
1090
|
+
code: "custom",
|
|
1091
|
+
message: "Protected region exceeds the output frame width.",
|
|
1092
|
+
path: ["region", "widthPercent"]
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
if (effect2.region.topPercent + effect2.region.heightPercent > 100) {
|
|
1096
|
+
context.addIssue({
|
|
1097
|
+
code: "custom",
|
|
1098
|
+
message: "Protected region exceeds the output frame height.",
|
|
1099
|
+
path: ["region", "heightPercent"]
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
if (effect2.endFrame <= effect2.startFrame) {
|
|
1103
|
+
context.addIssue({
|
|
1104
|
+
code: "custom",
|
|
1105
|
+
message: "Effect endFrame must be after startFrame.",
|
|
1106
|
+
path: ["endFrame"]
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
if (effect2.speedUpFrame !== void 0 && (effect2.speedUpFrame < effect2.startFrame || effect2.speedUpFrame >= effect2.endFrame)) {
|
|
1110
|
+
context.addIssue({
|
|
1111
|
+
code: "custom",
|
|
1112
|
+
message: "speedUpFrame must fall inside the effect window.",
|
|
1113
|
+
path: ["speedUpFrame"]
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
var beatVisualRequestSchema = z4.object({
|
|
1118
|
+
intent: z4.string().trim().min(1).optional(),
|
|
1119
|
+
media: z4.enum(["still", "motion"]).default("still"),
|
|
1120
|
+
source: z4.enum(["auto", "stock", "generate"]).default("auto")
|
|
595
1121
|
}).strict();
|
|
596
|
-
var briefSceneSchema =
|
|
597
|
-
type:
|
|
1122
|
+
var briefSceneSchema = z4.object({
|
|
1123
|
+
type: z4.enum([
|
|
598
1124
|
"hero",
|
|
599
1125
|
"points",
|
|
600
1126
|
"equation",
|
|
@@ -617,154 +1143,460 @@ var briefSceneSchema = z2.object({
|
|
|
617
1143
|
"image-window",
|
|
618
1144
|
"custom"
|
|
619
1145
|
]),
|
|
620
|
-
|
|
621
|
-
|
|
1146
|
+
layoutPreset: z4.literal("dark-cafe-announcement").optional().describe(
|
|
1147
|
+
"Built-in portrait coffee illustration with title, subtitle body and text CTA. Use only on a custom scene after the user selects this design. No generated assets."
|
|
1148
|
+
),
|
|
1149
|
+
durationInFrames: z4.number().optional(),
|
|
1150
|
+
narrationText: z4.string().optional(),
|
|
622
1151
|
visualResolution: beatVisualRequestSchema.optional(),
|
|
623
|
-
title:
|
|
624
|
-
subtitle:
|
|
625
|
-
text:
|
|
626
|
-
points:
|
|
627
|
-
latex:
|
|
628
|
-
value:
|
|
629
|
-
max:
|
|
630
|
-
label:
|
|
631
|
-
unit:
|
|
632
|
-
prefix:
|
|
633
|
-
suffix:
|
|
634
|
-
decimals:
|
|
635
|
-
color:
|
|
636
|
-
tone:
|
|
637
|
-
iconKey:
|
|
638
|
-
stepNumber:
|
|
639
|
-
totalSteps:
|
|
640
|
-
items:
|
|
641
|
-
src:
|
|
642
|
-
caption:
|
|
1152
|
+
title: z4.string().optional(),
|
|
1153
|
+
subtitle: z4.string().optional(),
|
|
1154
|
+
text: z4.string().optional(),
|
|
1155
|
+
points: z4.array(z4.string()).optional(),
|
|
1156
|
+
latex: z4.string().optional(),
|
|
1157
|
+
value: z4.union([z4.string(), z4.number()]).optional(),
|
|
1158
|
+
max: z4.number().optional(),
|
|
1159
|
+
label: z4.string().optional(),
|
|
1160
|
+
unit: z4.string().optional(),
|
|
1161
|
+
prefix: z4.string().optional(),
|
|
1162
|
+
suffix: z4.string().optional(),
|
|
1163
|
+
decimals: z4.number().optional(),
|
|
1164
|
+
color: z4.string().optional(),
|
|
1165
|
+
tone: z4.string().optional(),
|
|
1166
|
+
iconKey: z4.string().optional(),
|
|
1167
|
+
stepNumber: z4.number().optional(),
|
|
1168
|
+
totalSteps: z4.number().optional(),
|
|
1169
|
+
items: z4.array(z4.object({ label: z4.string(), value: z4.number() })).optional(),
|
|
1170
|
+
src: z4.string().optional(),
|
|
1171
|
+
caption: z4.string().optional(),
|
|
643
1172
|
// A-roll generation (video-window scenes): source "generate" triggers
|
|
644
1173
|
// flag-gated WaveSpeed image/video gen server-side from generatePrompt;
|
|
645
1174
|
// "url" (default) uses src as-is. Kept in sync with the app's
|
|
646
1175
|
// briefSceneSchema - see the parity test in the app suite.
|
|
647
|
-
source:
|
|
648
|
-
generatePrompt:
|
|
1176
|
+
source: z4.enum(["url", "generate"]).optional(),
|
|
1177
|
+
generatePrompt: z4.string().optional(),
|
|
649
1178
|
generationContract: generationContractSchema.optional(),
|
|
650
|
-
trimStartSeconds:
|
|
651
|
-
trimEndSeconds:
|
|
652
|
-
playbackRate:
|
|
653
|
-
objectFit:
|
|
654
|
-
objectPosition:
|
|
655
|
-
scaleFrom:
|
|
656
|
-
scaleTo:
|
|
657
|
-
panFromX:
|
|
658
|
-
panToX:
|
|
659
|
-
panFromY:
|
|
660
|
-
panToY:
|
|
661
|
-
routeFrom:
|
|
662
|
-
routeTo:
|
|
663
|
-
routeLabel:
|
|
664
|
-
overlayKind:
|
|
665
|
-
overlayPosition:
|
|
666
|
-
accentColor:
|
|
667
|
-
overlayBackgroundColor:
|
|
668
|
-
cutFlash:
|
|
669
|
-
muted:
|
|
1179
|
+
trimStartSeconds: z4.number().nonnegative().optional(),
|
|
1180
|
+
trimEndSeconds: z4.number().positive().optional(),
|
|
1181
|
+
playbackRate: z4.number().min(0.25).max(8).optional(),
|
|
1182
|
+
objectFit: z4.enum(["cover", "contain"]).optional(),
|
|
1183
|
+
objectPosition: z4.string().optional(),
|
|
1184
|
+
scaleFrom: z4.number().min(0.5).max(2).optional(),
|
|
1185
|
+
scaleTo: z4.number().min(0.5).max(2).optional(),
|
|
1186
|
+
panFromX: z4.number().min(-50).max(50).optional(),
|
|
1187
|
+
panToX: z4.number().min(-50).max(50).optional(),
|
|
1188
|
+
panFromY: z4.number().min(-50).max(50).optional(),
|
|
1189
|
+
panToY: z4.number().min(-50).max(50).optional(),
|
|
1190
|
+
routeFrom: z4.string().optional(),
|
|
1191
|
+
routeTo: z4.string().optional(),
|
|
1192
|
+
routeLabel: z4.string().optional(),
|
|
1193
|
+
overlayKind: z4.enum(["none", "hook", "stage", "end-card"]).optional(),
|
|
1194
|
+
overlayPosition: z4.enum(["center", "top", "bottom"]).optional(),
|
|
1195
|
+
accentColor: z4.string().optional(),
|
|
1196
|
+
overlayBackgroundColor: z4.string().optional(),
|
|
1197
|
+
cutFlash: z4.boolean().optional(),
|
|
1198
|
+
muted: z4.boolean().optional(),
|
|
1199
|
+
protectedRegionEffects: z4.array(protectedRegionEffectSchema).max(4).optional(),
|
|
1200
|
+
flashAtFrame: z4.number().int().nonnegative().optional(),
|
|
1201
|
+
flashDurationFrames: z4.number().int().positive().max(15).optional()
|
|
670
1202
|
});
|
|
671
|
-
var directionsBriefSchema =
|
|
672
|
-
subtitle:
|
|
673
|
-
unitLabel:
|
|
674
|
-
distanceTimeChip:
|
|
675
|
-
clips:
|
|
676
|
-
|
|
677
|
-
src:
|
|
678
|
-
trimStartSeconds:
|
|
679
|
-
trimEndSeconds:
|
|
680
|
-
speed:
|
|
681
|
-
mirrorFlip:
|
|
1203
|
+
var directionsBriefSchema = z4.object({
|
|
1204
|
+
subtitle: z4.string().optional(),
|
|
1205
|
+
unitLabel: z4.string().optional(),
|
|
1206
|
+
distanceTimeChip: z4.string().optional(),
|
|
1207
|
+
clips: z4.array(
|
|
1208
|
+
z4.object({
|
|
1209
|
+
src: z4.string(),
|
|
1210
|
+
trimStartSeconds: z4.number().optional(),
|
|
1211
|
+
trimEndSeconds: z4.number(),
|
|
1212
|
+
speed: z4.number().optional(),
|
|
1213
|
+
mirrorFlip: z4.boolean().optional()
|
|
682
1214
|
})
|
|
683
1215
|
).min(1),
|
|
684
|
-
cues:
|
|
685
|
-
|
|
686
|
-
atSeconds:
|
|
687
|
-
text:
|
|
688
|
-
arrow:
|
|
689
|
-
step:
|
|
1216
|
+
cues: z4.array(
|
|
1217
|
+
z4.object({
|
|
1218
|
+
atSeconds: z4.number(),
|
|
1219
|
+
text: z4.string(),
|
|
1220
|
+
arrow: z4.enum(["left", "right", "forward"]).optional(),
|
|
1221
|
+
step: z4.number().optional()
|
|
690
1222
|
})
|
|
691
1223
|
),
|
|
692
|
-
outro:
|
|
693
|
-
heading:
|
|
694
|
-
body:
|
|
695
|
-
seconds:
|
|
1224
|
+
outro: z4.object({
|
|
1225
|
+
heading: z4.string().optional(),
|
|
1226
|
+
body: z4.array(z4.string()).optional(),
|
|
1227
|
+
seconds: z4.number().optional()
|
|
696
1228
|
}).optional(),
|
|
697
|
-
music:
|
|
698
|
-
perSegment:
|
|
699
|
-
|
|
700
|
-
src:
|
|
701
|
-
clipIndexStart:
|
|
702
|
-
hotOffsetSeconds:
|
|
703
|
-
volume:
|
|
1229
|
+
music: z4.object({
|
|
1230
|
+
perSegment: z4.array(
|
|
1231
|
+
z4.object({
|
|
1232
|
+
src: z4.string(),
|
|
1233
|
+
clipIndexStart: z4.number().optional(),
|
|
1234
|
+
hotOffsetSeconds: z4.number().optional(),
|
|
1235
|
+
volume: z4.number().optional()
|
|
704
1236
|
})
|
|
705
1237
|
).optional(),
|
|
706
|
-
crossfadeSeconds:
|
|
1238
|
+
crossfadeSeconds: z4.number().optional()
|
|
707
1239
|
}).optional()
|
|
708
1240
|
});
|
|
1241
|
+
var PERSON_CONSENT_WORDING_VERSION = "2026-10-19.1";
|
|
1242
|
+
var PERSON_CONSENT_WORDING = "I have permission from everyone shown, and no one shown is under 18 unless I am their parent or guardian.";
|
|
1243
|
+
var personConsentSchema = z4.object({
|
|
1244
|
+
confirmed: z4.literal(true).describe("Must be true: the caller confirms the declaration below."),
|
|
1245
|
+
wordingVersion: z4.string().describe(
|
|
1246
|
+
`Version of the declaration the caller confirmed. Current: ${PERSON_CONSENT_WORDING_VERSION}.`
|
|
1247
|
+
)
|
|
1248
|
+
}).strict().describe(
|
|
1249
|
+
`Required when the brief uses an uploaded image or video. Ask the user to confirm: "${PERSON_CONSENT_WORDING}"`
|
|
1250
|
+
);
|
|
1251
|
+
var LIKENESS_WARNING_WORDING_VERSION = "2026-10-19.1";
|
|
1252
|
+
var LIKENESS_WARNING_WORDING = "This photo seems to show someone other than you. Only continue if you have their permission.";
|
|
1253
|
+
var LIKENESS_MINOR_WARNING_WORDING_VERSION = "2026-10-19.1";
|
|
1254
|
+
var LIKENESS_MINOR_WARNING_WORDING = "This may show someone under 18. Continue only if you are their parent or guardian, or have their parent or guardian's permission.";
|
|
1255
|
+
var likenessAcknowledgementSchema = z4.object({
|
|
1256
|
+
confirmed: z4.literal(true).describe("Must be true: the user accepted the warning below."),
|
|
1257
|
+
kind: z4.enum(["other_person", "guardian"]).optional().describe(
|
|
1258
|
+
`Which warning was accepted. "other_person" (default): "${LIKENESS_WARNING_WORDING}" "guardian": "${LIKENESS_MINOR_WARNING_WORDING}"`
|
|
1259
|
+
),
|
|
1260
|
+
wordingVersion: z4.string().describe(
|
|
1261
|
+
`Version of the warning accepted. Current: other_person ${LIKENESS_WARNING_WORDING_VERSION}, guardian ${LIKENESS_MINOR_WARNING_WORDING_VERSION}.`
|
|
1262
|
+
)
|
|
1263
|
+
}).strict().describe(
|
|
1264
|
+
"Optional. Send only after the user has seen and accepted the warning. Without it, a job that animates an uploaded photo of someone other than the account holder stops with likeness_acknowledgement_required, or likeness_guardian_confirmation_required when the person may be under 18."
|
|
1265
|
+
);
|
|
709
1266
|
var createBriefShape = {
|
|
710
|
-
title:
|
|
711
|
-
script:
|
|
712
|
-
caption:
|
|
713
|
-
publishTarget:
|
|
714
|
-
|
|
715
|
-
|
|
1267
|
+
title: z4.string().min(1).max(120).describe("Short title (max 120 chars)"),
|
|
1268
|
+
script: z4.string().min(1).describe("Full narration script (sent to TTS)"),
|
|
1269
|
+
caption: z4.string().min(1).describe("Social caption with hooks, hashtags, CTA"),
|
|
1270
|
+
publishTarget: z4.enum(PUBLISH_TARGETS).describe("Target social platform"),
|
|
1271
|
+
publishTargets: z4.array(z4.enum(PUBLISH_TARGETS)).max(9).optional().describe("All allowed social destinations for this video"),
|
|
1272
|
+
objective: z4.enum(BRIEF_OBJECTIVES).describe("Content objective (drives composition routing)"),
|
|
1273
|
+
targetDurationSeconds: z4.number().int().min(5).max(600).optional().describe("Requested final video duration in seconds"),
|
|
1274
|
+
narrationMode: z4.enum(["tts", "off"]).optional().describe("Disable synthesized narration for source-audio or silent work"),
|
|
1275
|
+
lessonTitle: z4.string().optional().describe("Display title rendered inside the video"),
|
|
716
1276
|
visualDirection: visualDirectionSchema.optional().describe(
|
|
717
1277
|
"Whole-video communication, attention, visual-language, and camera direction."
|
|
718
1278
|
),
|
|
719
|
-
steps:
|
|
720
|
-
scenes:
|
|
1279
|
+
steps: z4.array(briefStepSchema).optional().describe("Structured steps for educational content (LaTeX)"),
|
|
1280
|
+
scenes: z4.array(briefSceneSchema).optional().describe("Scene graph for the UniversalVideo composition"),
|
|
721
1281
|
// No zod .default() here: a default would materialize into the parsed tool
|
|
722
1282
|
// arguments and relay to /api/v1/jobs as if the caller stated it, corrupting
|
|
723
1283
|
// the resolution provenance the API echoes back. Unset must stay absent;
|
|
724
1284
|
// the API re-applies 9:16 server-side.
|
|
725
|
-
aspectRatio:
|
|
726
|
-
compositionId:
|
|
727
|
-
voiceId:
|
|
1285
|
+
aspectRatio: z4.enum(ASPECT_RATIOS).optional().describe("Aspect ratio (defaults to 9:16 when omitted)"),
|
|
1286
|
+
compositionId: z4.string().optional().describe("Remotion composition ID override (advanced)"),
|
|
1287
|
+
voiceId: z4.string().optional().describe(
|
|
728
1288
|
"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."
|
|
729
1289
|
),
|
|
730
1290
|
directions: directionsBriefSchema.optional().describe(
|
|
731
1291
|
"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')."
|
|
732
1292
|
),
|
|
733
|
-
assetSlots:
|
|
1293
|
+
assetSlots: z4.array(assetSlotSchema).optional().describe(
|
|
734
1294
|
"Optional storyboard asset slots: region-placed image/video assets or placeholders attached to the whole video or a specific scene."
|
|
735
1295
|
),
|
|
736
|
-
audioLayers:
|
|
1296
|
+
audioLayers: z4.array(briefAudioLayerSchema).optional().describe("Optional typed music, Foley, transition, and effect layers."),
|
|
1297
|
+
personConsent: personConsentSchema.optional(),
|
|
1298
|
+
likenessAcknowledgement: likenessAcknowledgementSchema.optional()
|
|
737
1299
|
};
|
|
738
1300
|
var editBriefShape = {
|
|
739
|
-
jobId:
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
1301
|
+
jobId: z4.string().uuid().describe("The job to edit"),
|
|
1302
|
+
personConsent: personConsentSchema.optional(),
|
|
1303
|
+
likenessAcknowledgement: likenessAcknowledgementSchema.optional(),
|
|
1304
|
+
title: z4.string().optional().describe("Updated title"),
|
|
1305
|
+
script: z4.string().optional().describe("Updated narration script"),
|
|
1306
|
+
caption: z4.string().optional().describe("Updated social caption"),
|
|
1307
|
+
lessonTitle: z4.string().optional().describe("Updated lesson title"),
|
|
1308
|
+
narrationMode: z4.enum(["tts", "off"]).optional().describe("Updated synthesized narration mode"),
|
|
744
1309
|
visualDirection: visualDirectionSchema.optional().describe("Updated whole-video visual direction"),
|
|
745
|
-
steps:
|
|
746
|
-
scenes:
|
|
747
|
-
aspectRatio:
|
|
748
|
-
objective:
|
|
1310
|
+
steps: z4.array(briefStepSchema).optional().describe("Updated steps (replaces the array)"),
|
|
1311
|
+
scenes: z4.array(briefSceneSchema).optional().describe("Updated scenes (replaces the array)"),
|
|
1312
|
+
aspectRatio: z4.enum(ASPECT_RATIOS).optional().describe("Updated aspect ratio"),
|
|
1313
|
+
objective: z4.enum(BRIEF_OBJECTIVES).optional().describe(
|
|
749
1314
|
"Updated content objective. Re-routes the composition and triggers a re-render."
|
|
750
1315
|
),
|
|
751
|
-
publishTarget:
|
|
1316
|
+
publishTarget: z4.enum(PUBLISH_TARGETS).optional().describe(
|
|
752
1317
|
"Updated target social platform. Distribution metadata only - does not trigger a re-render."
|
|
753
1318
|
),
|
|
754
|
-
|
|
1319
|
+
publishTargets: z4.array(z4.enum(PUBLISH_TARGETS)).max(9).optional().describe("Replacement allowed social destination list"),
|
|
1320
|
+
targetDurationSeconds: z4.number().int().min(5).max(600).optional().describe("Updated final video duration in seconds"),
|
|
1321
|
+
voiceId: z4.string().optional().describe(
|
|
755
1322
|
"Updated TTS narrator voice ID. Triggers a re-render with the new voice."
|
|
756
1323
|
),
|
|
757
|
-
assetSlots:
|
|
1324
|
+
assetSlots: z4.array(assetSlotSchema).optional().describe(
|
|
758
1325
|
"Replacement storyboard asset slots. Omit to preserve existing slots; pass [] to clear them."
|
|
759
1326
|
),
|
|
760
|
-
audioLayers:
|
|
1327
|
+
audioLayers: z4.array(briefAudioLayerSchema).optional().describe(
|
|
761
1328
|
"Replacement typed audio layers. Omit to preserve existing layers; pass [] to clear them."
|
|
762
1329
|
)
|
|
763
1330
|
};
|
|
764
1331
|
|
|
1332
|
+
// src/render-diagnostics.ts
|
|
1333
|
+
function record(value) {
|
|
1334
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1335
|
+
}
|
|
1336
|
+
function rows(value) {
|
|
1337
|
+
return Array.isArray(value) ? value.map(record).filter((row) => !!row) : [];
|
|
1338
|
+
}
|
|
1339
|
+
function string(value) {
|
|
1340
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
1341
|
+
}
|
|
1342
|
+
function number(value) {
|
|
1343
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
1344
|
+
}
|
|
1345
|
+
function currentVideoArtifact(status) {
|
|
1346
|
+
return rows(status?.artifacts).find(
|
|
1347
|
+
(artifact) => artifact.artifactType === "video" && artifact.isCurrent === true
|
|
1348
|
+
) ?? null;
|
|
1349
|
+
}
|
|
1350
|
+
function verificationMatches(verifications, pattern) {
|
|
1351
|
+
return verifications.filter(
|
|
1352
|
+
(verification) => pattern.test(
|
|
1353
|
+
[
|
|
1354
|
+
verification.phase,
|
|
1355
|
+
verification.checkKey,
|
|
1356
|
+
verification.verifier,
|
|
1357
|
+
JSON.stringify(verification.detailsJson ?? null)
|
|
1358
|
+
].map((value) => String(value ?? "")).join(" ")
|
|
1359
|
+
)
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
function buildRenderDiagnosticSnapshot(params) {
|
|
1363
|
+
const failed = [params.jobStatus, params.videoState].find(
|
|
1364
|
+
(result2) => !result2.ok
|
|
1365
|
+
);
|
|
1366
|
+
if (failed) return failed;
|
|
1367
|
+
const status = record(params.jobStatus.body);
|
|
1368
|
+
const stateResponse = record(params.videoState.body);
|
|
1369
|
+
const state = record(stateResponse?.state);
|
|
1370
|
+
const runs = rows(status?.runs);
|
|
1371
|
+
const latestRunId = string(status?.latestRunId);
|
|
1372
|
+
const latestRun = runs.find((run) => string(run.id) === latestRunId) ?? runs[0] ?? null;
|
|
1373
|
+
const videoArtifact = currentVideoArtifact(status);
|
|
1374
|
+
const artifactMetadata = record(videoArtifact?.metadataJson);
|
|
1375
|
+
const verifications = rows(status?.verificationTimeline);
|
|
1376
|
+
const blankChecks = verificationMatches(verifications, /blank|black frame/i);
|
|
1377
|
+
const placeholderChecks = verificationMatches(
|
|
1378
|
+
verifications,
|
|
1379
|
+
/placeholder|source clip|source asset/i
|
|
1380
|
+
);
|
|
1381
|
+
const rightsChecks = verificationMatches(
|
|
1382
|
+
verifications,
|
|
1383
|
+
/rights|licen[cs]|clearance/i
|
|
1384
|
+
);
|
|
1385
|
+
const currentVideoArtifactId = string(videoArtifact?.id);
|
|
1386
|
+
const stateVideoArtifactId = string(state?.currentVideoArtifactId);
|
|
1387
|
+
const expectedDurationSeconds = number(state?.durationSeconds) ?? number(status?.durationSec);
|
|
1388
|
+
const actualDurationSeconds = number(videoArtifact?.durationSeconds) ?? number(artifactMetadata?.durationSeconds) ?? number(artifactMetadata?.durationSec) ?? number(artifactMetadata?.duration);
|
|
1389
|
+
return {
|
|
1390
|
+
ok: true,
|
|
1391
|
+
status: 200,
|
|
1392
|
+
body: {
|
|
1393
|
+
job: {
|
|
1394
|
+
id: string(status?.id),
|
|
1395
|
+
title: string(status?.title),
|
|
1396
|
+
status: string(status?.status),
|
|
1397
|
+
latestRunId
|
|
1398
|
+
},
|
|
1399
|
+
latestRun: latestRun ? {
|
|
1400
|
+
id: string(latestRun.id),
|
|
1401
|
+
runType: string(latestRun.runType),
|
|
1402
|
+
status: string(latestRun.status),
|
|
1403
|
+
errorCode: string(latestRun.errorCode),
|
|
1404
|
+
errorMessage: string(latestRun.errorMessage)
|
|
1405
|
+
} : null,
|
|
1406
|
+
artifactBinding: {
|
|
1407
|
+
jobCurrentVideoArtifactId: currentVideoArtifactId,
|
|
1408
|
+
videoStateCurrentArtifactId: stateVideoArtifactId,
|
|
1409
|
+
matches: currentVideoArtifactId && stateVideoArtifactId ? currentVideoArtifactId === stateVideoArtifactId : null,
|
|
1410
|
+
currentVideoUrl: string(status?.currentVideoUrl) ?? string(state?.currentVideoUrl)
|
|
1411
|
+
},
|
|
1412
|
+
duration: {
|
|
1413
|
+
expectedSeconds: expectedDurationSeconds,
|
|
1414
|
+
actualSeconds: actualDurationSeconds,
|
|
1415
|
+
deltaSeconds: expectedDurationSeconds !== null && actualDurationSeconds !== null ? Math.round(
|
|
1416
|
+
(actualDurationSeconds - expectedDurationSeconds) * 1e3
|
|
1417
|
+
) / 1e3 : null
|
|
1418
|
+
},
|
|
1419
|
+
retryRecommendations: Array.isArray(status?.retryRecommendations) ? status?.retryRecommendations : [],
|
|
1420
|
+
evidence: {
|
|
1421
|
+
rights: rightsChecks,
|
|
1422
|
+
blankFrames: blankChecks,
|
|
1423
|
+
placeholders: placeholderChecks,
|
|
1424
|
+
videoState: state
|
|
1425
|
+
},
|
|
1426
|
+
evidenceLimits: {
|
|
1427
|
+
visualContentInspection: blankChecks.length || placeholderChecks.length ? "verification_rows_available" : "not_evaluated",
|
|
1428
|
+
note: "No blank or placeholder verdict is inferred when the current verification timeline contains no matching check. Inspect the video artifact visually before deciding on an edit."
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// src/design-action-schema.ts
|
|
1435
|
+
import { z as z5 } from "zod";
|
|
1436
|
+
var id = z5.string().min(1).max(120);
|
|
1437
|
+
var point = z5.object({ x: z5.number().min(0).max(1), y: z5.number().min(0).max(1) }).strict();
|
|
1438
|
+
var polygonShape = z5.object({
|
|
1439
|
+
kind: z5.literal("polygon"),
|
|
1440
|
+
points: z5.array(point).min(3).max(512)
|
|
1441
|
+
}).strict();
|
|
1442
|
+
var rasterShape = z5.object({
|
|
1443
|
+
kind: z5.literal("raster"),
|
|
1444
|
+
source: z5.object({
|
|
1445
|
+
assetId: id,
|
|
1446
|
+
expectedSha256: z5.string().regex(/^[a-f0-9]{64}$/),
|
|
1447
|
+
sourceRange: z5.object({
|
|
1448
|
+
startFrame: z5.number().int().nonnegative(),
|
|
1449
|
+
endFrame: z5.number().int().positive()
|
|
1450
|
+
}).strict().optional()
|
|
1451
|
+
}).strict(),
|
|
1452
|
+
widthPx: z5.number().int().positive().max(8192),
|
|
1453
|
+
heightPx: z5.number().int().positive().max(8192)
|
|
1454
|
+
}).strict();
|
|
1455
|
+
var shape = z5.discriminatedUnion("kind", [polygonShape, rasterShape]);
|
|
1456
|
+
var mask = z5.object({
|
|
1457
|
+
id,
|
|
1458
|
+
shape,
|
|
1459
|
+
operation: z5.enum(["add", "subtract"]),
|
|
1460
|
+
inverted: z5.boolean().default(false),
|
|
1461
|
+
feather: z5.number().finite().nonnegative().max(0.25).default(0),
|
|
1462
|
+
allowedSupportMargin: z5.number().finite().nonnegative().max(0.25).default(0),
|
|
1463
|
+
reviewState: z5.enum(["draft", "approved", "rejected"]),
|
|
1464
|
+
provenance: z5.object({
|
|
1465
|
+
kind: z5.enum(["manual", "imported", "model-proposed"]),
|
|
1466
|
+
sourceAssetId: id.optional(),
|
|
1467
|
+
modelId: z5.string().min(1).max(160).optional()
|
|
1468
|
+
}).strict()
|
|
1469
|
+
}).strict();
|
|
1470
|
+
var transform = z5.object({
|
|
1471
|
+
x: z5.number().min(0).max(1),
|
|
1472
|
+
y: z5.number().min(0).max(1),
|
|
1473
|
+
width: z5.number().positive().max(1),
|
|
1474
|
+
height: z5.number().positive().max(1),
|
|
1475
|
+
rotationDegrees: z5.number().min(-360).max(360),
|
|
1476
|
+
opacity: z5.number().min(0).max(1)
|
|
1477
|
+
}).strict();
|
|
1478
|
+
var colourParameters = {
|
|
1479
|
+
exposure: z5.number().finite().min(-2).max(2).default(0),
|
|
1480
|
+
contrast: z5.number().finite().min(0.5).max(2).default(1),
|
|
1481
|
+
saturation: z5.number().finite().min(0).max(2).default(1),
|
|
1482
|
+
temperature: z5.number().finite().min(-1).max(1).default(0),
|
|
1483
|
+
tint: z5.number().finite().min(-1).max(1).default(0)
|
|
1484
|
+
};
|
|
1485
|
+
var effect = z5.object({
|
|
1486
|
+
id,
|
|
1487
|
+
kind: z5.literal("local-colour"),
|
|
1488
|
+
version: z5.literal(1),
|
|
1489
|
+
maskId: id,
|
|
1490
|
+
...colourParameters
|
|
1491
|
+
}).strict();
|
|
1492
|
+
var motionPath = z5.object({
|
|
1493
|
+
id,
|
|
1494
|
+
kind: z5.literal("cubic-bezier"),
|
|
1495
|
+
startFrame: z5.number().int().nonnegative(),
|
|
1496
|
+
endFrame: z5.number().int().positive(),
|
|
1497
|
+
start: point,
|
|
1498
|
+
control1: point,
|
|
1499
|
+
control2: point,
|
|
1500
|
+
end: point,
|
|
1501
|
+
easing: z5.enum(["linear", "ease-in", "ease-out", "ease-in-out"]),
|
|
1502
|
+
orientToPath: z5.boolean().default(false)
|
|
1503
|
+
}).strict();
|
|
1504
|
+
var region = z5.object({ id, shape, label: z5.string().min(1).max(160) }).strict();
|
|
1505
|
+
var base = { actionId: id, sceneId: id };
|
|
1506
|
+
var vendoredDesignActionSchema = z5.discriminatedUnion("kind", [
|
|
1507
|
+
z5.object({
|
|
1508
|
+
...base,
|
|
1509
|
+
kind: z5.literal("set-layer-transform"),
|
|
1510
|
+
layerId: id,
|
|
1511
|
+
transform
|
|
1512
|
+
}).strict(),
|
|
1513
|
+
z5.object({
|
|
1514
|
+
...base,
|
|
1515
|
+
kind: z5.literal("set-local-colour-effect"),
|
|
1516
|
+
effectId: id,
|
|
1517
|
+
parameters: z5.object(colourParameters).strict()
|
|
1518
|
+
}).strict(),
|
|
1519
|
+
z5.object({ ...base, kind: z5.literal("create-mask"), mask }).strict(),
|
|
1520
|
+
z5.object({ ...base, kind: z5.literal("set-mask"), maskId: id, mask }).strict(),
|
|
1521
|
+
z5.object({
|
|
1522
|
+
...base,
|
|
1523
|
+
kind: z5.literal("set-mask-review-state"),
|
|
1524
|
+
maskId: id,
|
|
1525
|
+
reviewState: z5.enum(["draft", "approved", "rejected"])
|
|
1526
|
+
}).strict(),
|
|
1527
|
+
z5.object({ ...base, kind: z5.literal("delete-mask"), maskId: id }).strict(),
|
|
1528
|
+
z5.object({
|
|
1529
|
+
...base,
|
|
1530
|
+
kind: z5.literal("attach-effect"),
|
|
1531
|
+
effect,
|
|
1532
|
+
layerIds: z5.array(id).min(1).max(50)
|
|
1533
|
+
}).strict(),
|
|
1534
|
+
z5.object({ ...base, kind: z5.literal("remove-effect"), effectId: id }).strict(),
|
|
1535
|
+
z5.object({
|
|
1536
|
+
...base,
|
|
1537
|
+
kind: z5.literal("set-layer-text"),
|
|
1538
|
+
layerId: id,
|
|
1539
|
+
text: z5.string().max(1e4)
|
|
1540
|
+
}).strict(),
|
|
1541
|
+
z5.object({
|
|
1542
|
+
...base,
|
|
1543
|
+
kind: z5.literal("set-layer-visibility"),
|
|
1544
|
+
layerId: id,
|
|
1545
|
+
visible: z5.boolean()
|
|
1546
|
+
}).strict(),
|
|
1547
|
+
z5.object({
|
|
1548
|
+
...base,
|
|
1549
|
+
kind: z5.literal("set-layer-locked"),
|
|
1550
|
+
layerId: id,
|
|
1551
|
+
locked: z5.boolean()
|
|
1552
|
+
}).strict(),
|
|
1553
|
+
z5.object({
|
|
1554
|
+
...base,
|
|
1555
|
+
kind: z5.literal("set-layer-mask-ids"),
|
|
1556
|
+
layerId: id,
|
|
1557
|
+
maskIds: z5.array(id).max(16)
|
|
1558
|
+
}).strict(),
|
|
1559
|
+
z5.object({
|
|
1560
|
+
...base,
|
|
1561
|
+
kind: z5.literal("set-layer-order"),
|
|
1562
|
+
orderedLayerIds: z5.array(id).min(1).max(500)
|
|
1563
|
+
}).strict(),
|
|
1564
|
+
z5.object({
|
|
1565
|
+
...base,
|
|
1566
|
+
kind: z5.literal("set-motion-path"),
|
|
1567
|
+
path: motionPath,
|
|
1568
|
+
layerIds: z5.array(id).max(50).default([])
|
|
1569
|
+
}).strict(),
|
|
1570
|
+
z5.object({ ...base, kind: z5.literal("remove-motion-path"), pathId: id }).strict(),
|
|
1571
|
+
z5.object({ ...base, kind: z5.literal("set-protected-region"), region }).strict(),
|
|
1572
|
+
z5.object({
|
|
1573
|
+
...base,
|
|
1574
|
+
kind: z5.literal("remove-protected-region"),
|
|
1575
|
+
regionId: id
|
|
1576
|
+
}).strict()
|
|
1577
|
+
]);
|
|
1578
|
+
var inspectDesignDocumentToolSchema = z5.object({
|
|
1579
|
+
projectId: z5.string().uuid(),
|
|
1580
|
+
documentId: z5.string().uuid(),
|
|
1581
|
+
includeVersions: z5.boolean().default(true)
|
|
1582
|
+
}).strict();
|
|
1583
|
+
var applyDesignActionsToolSchema = z5.object({
|
|
1584
|
+
projectId: z5.string().uuid(),
|
|
1585
|
+
documentId: z5.string().uuid(),
|
|
1586
|
+
actionGroupId: z5.string().min(1).max(120),
|
|
1587
|
+
expectedVersion: z5.number().int().positive(),
|
|
1588
|
+
actions: z5.array(vendoredDesignActionSchema).min(1).max(50),
|
|
1589
|
+
clientCreatedAt: z5.string().datetime()
|
|
1590
|
+
}).strict();
|
|
1591
|
+
|
|
765
1592
|
// src/tools.ts
|
|
766
|
-
var uuid = (desc) =>
|
|
767
|
-
var READ_ONLY = {
|
|
1593
|
+
var uuid = (desc) => z6.string().uuid().describe(desc);
|
|
1594
|
+
var READ_ONLY = {
|
|
1595
|
+
readOnlyHint: true,
|
|
1596
|
+
destructiveHint: false,
|
|
1597
|
+
idempotentHint: true,
|
|
1598
|
+
openWorldHint: false
|
|
1599
|
+
};
|
|
768
1600
|
var SAFE_WRITE = {
|
|
769
1601
|
readOnlyHint: false,
|
|
770
1602
|
destructiveHint: false,
|
|
@@ -778,8 +1610,8 @@ function registerTools(server) {
|
|
|
778
1610
|
title: "List jobs",
|
|
779
1611
|
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.",
|
|
780
1612
|
inputSchema: {
|
|
781
|
-
limit:
|
|
782
|
-
status:
|
|
1613
|
+
limit: z6.number().int().min(1).max(50).default(10).describe("Number of jobs to return (default 10, max 50)"),
|
|
1614
|
+
status: z6.enum([
|
|
783
1615
|
"queued",
|
|
784
1616
|
"rendering",
|
|
785
1617
|
"draft_ready",
|
|
@@ -820,6 +1652,23 @@ function registerTools(server) {
|
|
|
820
1652
|
() => apiGet(`/api/v1/jobs/${encodeURIComponent(jobId)}/video-state`)
|
|
821
1653
|
)
|
|
822
1654
|
);
|
|
1655
|
+
server.registerTool(
|
|
1656
|
+
"diagnose_render",
|
|
1657
|
+
{
|
|
1658
|
+
title: "Diagnose render",
|
|
1659
|
+
description: "Build one read-only troubleshooting snapshot from the current job status and video state. Reports run failures, artifact binding, duration evidence, retry recommendations, rights checks, and whether blank-frame or placeholder verification evidence exists. It does not visually inspect the video.",
|
|
1660
|
+
inputSchema: { jobId: uuid("The job ID to diagnose") },
|
|
1661
|
+
annotations: READ_ONLY
|
|
1662
|
+
},
|
|
1663
|
+
async ({ jobId }) => runTool(async () => {
|
|
1664
|
+
const encodedJobId = encodeURIComponent(jobId);
|
|
1665
|
+
const [jobStatus, videoState] = await Promise.all([
|
|
1666
|
+
apiGet(`/api/v1/jobs/${encodedJobId}`),
|
|
1667
|
+
apiGet(`/api/v1/jobs/${encodedJobId}/video-state`)
|
|
1668
|
+
]);
|
|
1669
|
+
return buildRenderDiagnosticSnapshot({ jobStatus, videoState });
|
|
1670
|
+
})
|
|
1671
|
+
);
|
|
823
1672
|
server.registerTool(
|
|
824
1673
|
"get_job_metrics",
|
|
825
1674
|
{
|
|
@@ -827,7 +1676,7 @@ function registerTools(server) {
|
|
|
827
1676
|
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.",
|
|
828
1677
|
inputSchema: {
|
|
829
1678
|
jobId: uuid("The job ID"),
|
|
830
|
-
limit:
|
|
1679
|
+
limit: z6.number().int().min(1).max(20).default(10).describe("Max metric snapshots (default 10, max 20)")
|
|
831
1680
|
},
|
|
832
1681
|
annotations: READ_ONLY
|
|
833
1682
|
},
|
|
@@ -843,10 +1692,10 @@ function registerTools(server) {
|
|
|
843
1692
|
title: "Get cost summary",
|
|
844
1693
|
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.",
|
|
845
1694
|
inputSchema: {
|
|
846
|
-
since:
|
|
847
|
-
provider:
|
|
848
|
-
groupBy:
|
|
849
|
-
includeReconciliation:
|
|
1695
|
+
since: z6.string().optional().describe("ISO date \u2014 only include events after this date"),
|
|
1696
|
+
provider: z6.enum(["runpod", "lambda", "r2", "wavespeed", "poyo", "all"]).default("all").describe("Filter by provider"),
|
|
1697
|
+
groupBy: z6.enum(["provider", "job", "day"]).default("provider").describe("How to aggregate the summary"),
|
|
1698
|
+
includeReconciliation: z6.boolean().default(false).describe("Include account-wide reconciliation events")
|
|
850
1699
|
},
|
|
851
1700
|
annotations: READ_ONLY
|
|
852
1701
|
},
|
|
@@ -862,13 +1711,50 @@ function registerTools(server) {
|
|
|
862
1711
|
{
|
|
863
1712
|
title: "Inspect workflow",
|
|
864
1713
|
description: "Inspect the current Studio workflow and its version history for a job.",
|
|
865
|
-
inputSchema:
|
|
1714
|
+
inputSchema: z6.object({ jobId: uuid("The job ID") }).strict(),
|
|
866
1715
|
annotations: READ_ONLY
|
|
867
1716
|
},
|
|
868
1717
|
async ({ jobId }) => runTool(
|
|
869
1718
|
() => apiGet(`/api/v1/jobs/${encodeURIComponent(jobId)}/workflow`)
|
|
870
1719
|
)
|
|
871
1720
|
);
|
|
1721
|
+
server.registerTool(
|
|
1722
|
+
"inspect_design_document",
|
|
1723
|
+
{
|
|
1724
|
+
title: "Inspect design document",
|
|
1725
|
+
description: "Inspect one versioned design document and its recent immutable version history. This tool never changes the document.",
|
|
1726
|
+
inputSchema: inspectDesignDocumentToolSchema,
|
|
1727
|
+
annotations: READ_ONLY
|
|
1728
|
+
},
|
|
1729
|
+
async ({ projectId, documentId, includeVersions }) => {
|
|
1730
|
+
const query = new URLSearchParams({
|
|
1731
|
+
documentId,
|
|
1732
|
+
includeVersions: includeVersions ? "1" : "0"
|
|
1733
|
+
});
|
|
1734
|
+
return runTool(
|
|
1735
|
+
() => apiGet(
|
|
1736
|
+
`/api/v1/projects/${encodeURIComponent(
|
|
1737
|
+
projectId
|
|
1738
|
+
)}/design?${query.toString()}`
|
|
1739
|
+
)
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
1742
|
+
);
|
|
1743
|
+
server.registerTool(
|
|
1744
|
+
"apply_design_actions",
|
|
1745
|
+
{
|
|
1746
|
+
title: "Apply design actions",
|
|
1747
|
+
description: "Apply a reviewed group of closed, invertible design actions to an expected document version. Inspect first. A stale version returns a readable conflict and does not overwrite customer work.",
|
|
1748
|
+
inputSchema: applyDesignActionsToolSchema,
|
|
1749
|
+
annotations: SAFE_WRITE
|
|
1750
|
+
},
|
|
1751
|
+
async (input) => runTool(
|
|
1752
|
+
() => apiPost(
|
|
1753
|
+
`/api/v1/projects/${encodeURIComponent(input.projectId)}/design`,
|
|
1754
|
+
input
|
|
1755
|
+
)
|
|
1756
|
+
)
|
|
1757
|
+
);
|
|
872
1758
|
server.registerTool(
|
|
873
1759
|
"create_video_brief",
|
|
874
1760
|
{
|
|
@@ -905,10 +1791,10 @@ function registerTools(server) {
|
|
|
905
1791
|
{
|
|
906
1792
|
title: "Draft workflow",
|
|
907
1793
|
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.",
|
|
908
|
-
inputSchema:
|
|
1794
|
+
inputSchema: z6.object({
|
|
909
1795
|
jobId: uuid("The job ID"),
|
|
910
|
-
expectedVersion:
|
|
911
|
-
document:
|
|
1796
|
+
expectedVersion: z6.number().int().nonnegative().describe("Use 0 when no workflow exists yet"),
|
|
1797
|
+
document: z6.unknown().describe("The complete Studio workflow document")
|
|
912
1798
|
}).strict(),
|
|
913
1799
|
annotations: SAFE_WRITE
|
|
914
1800
|
},
|
|
@@ -919,10 +1805,10 @@ function registerTools(server) {
|
|
|
919
1805
|
{
|
|
920
1806
|
title: "Patch workflow",
|
|
921
1807
|
description: "Create a new workflow version by recursively merging a patch into the current version.",
|
|
922
|
-
inputSchema:
|
|
1808
|
+
inputSchema: z6.object({
|
|
923
1809
|
jobId: uuid("The job ID"),
|
|
924
|
-
expectedVersion:
|
|
925
|
-
patch:
|
|
1810
|
+
expectedVersion: z6.number().int().positive(),
|
|
1811
|
+
patch: z6.record(z6.string(), z6.unknown())
|
|
926
1812
|
}).strict(),
|
|
927
1813
|
annotations: SAFE_WRITE
|
|
928
1814
|
},
|
|
@@ -933,9 +1819,9 @@ function registerTools(server) {
|
|
|
933
1819
|
{
|
|
934
1820
|
title: "Validate workflow",
|
|
935
1821
|
description: "Validate and compile the inspected workflow version without rendering or publishing it.",
|
|
936
|
-
inputSchema:
|
|
1822
|
+
inputSchema: z6.object({
|
|
937
1823
|
jobId: uuid("The job ID"),
|
|
938
|
-
expectedVersion:
|
|
1824
|
+
expectedVersion: z6.number().int().positive()
|
|
939
1825
|
}).strict(),
|
|
940
1826
|
annotations: SAFE_WRITE
|
|
941
1827
|
},
|
|
@@ -946,7 +1832,7 @@ function registerTools(server) {
|
|
|
946
1832
|
{
|
|
947
1833
|
title: "Preview workflow",
|
|
948
1834
|
description: "Return the current workflow's compiled UniversalVideo brief for a safe pre-render preview.",
|
|
949
|
-
inputSchema:
|
|
1835
|
+
inputSchema: z6.object({ jobId: uuid("The job ID") }).strict(),
|
|
950
1836
|
annotations: READ_ONLY
|
|
951
1837
|
},
|
|
952
1838
|
async ({ jobId }) => runTool(
|
|
@@ -960,10 +1846,10 @@ function registerTools(server) {
|
|
|
960
1846
|
{
|
|
961
1847
|
title: "Compare workflow versions",
|
|
962
1848
|
description: "Compare two durable workflow versions field by field.",
|
|
963
|
-
inputSchema:
|
|
1849
|
+
inputSchema: z6.object({
|
|
964
1850
|
jobId: uuid("The job ID"),
|
|
965
|
-
fromVersion:
|
|
966
|
-
toVersion:
|
|
1851
|
+
fromVersion: z6.number().int().positive(),
|
|
1852
|
+
toVersion: z6.number().int().positive()
|
|
967
1853
|
}).strict(),
|
|
968
1854
|
annotations: READ_ONLY
|
|
969
1855
|
},
|
|
@@ -987,9 +1873,9 @@ function registerTools(server) {
|
|
|
987
1873
|
{
|
|
988
1874
|
title: "Approve workflow",
|
|
989
1875
|
description: "Validate and approve the inspected workflow version. This does not approve or publish the video job.",
|
|
990
|
-
inputSchema:
|
|
1876
|
+
inputSchema: z6.object({
|
|
991
1877
|
jobId: uuid("The job ID"),
|
|
992
|
-
expectedVersion:
|
|
1878
|
+
expectedVersion: z6.number().int().positive()
|
|
993
1879
|
}).strict(),
|
|
994
1880
|
annotations: {
|
|
995
1881
|
readOnlyHint: false,
|
|
@@ -1046,7 +1932,7 @@ function registerTools(server) {
|
|
|
1046
1932
|
inputSchema: {
|
|
1047
1933
|
jobId: uuid("The current job ID"),
|
|
1048
1934
|
sourceRunId: uuid("The current run ID"),
|
|
1049
|
-
trigger:
|
|
1935
|
+
trigger: z6.enum(["user_requested", "verifier_recommendation"])
|
|
1050
1936
|
},
|
|
1051
1937
|
annotations: SAFE_WRITE
|
|
1052
1938
|
},
|
|
@@ -1065,7 +1951,8 @@ function registerTools(server) {
|
|
|
1065
1951
|
inputSchema: { jobId: uuid("The job ID to approve") },
|
|
1066
1952
|
annotations: {
|
|
1067
1953
|
readOnlyHint: false,
|
|
1068
|
-
|
|
1954
|
+
// Approval revokes active review links, so this is not purely additive.
|
|
1955
|
+
destructiveHint: true,
|
|
1069
1956
|
// Approving an already-approved job does not change state further.
|
|
1070
1957
|
idempotentHint: true,
|
|
1071
1958
|
openWorldHint: false
|
|
@@ -1082,15 +1969,25 @@ function registerTools(server) {
|
|
|
1082
1969
|
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.",
|
|
1083
1970
|
inputSchema: {
|
|
1084
1971
|
jobId: uuid("The approved job to publish"),
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1972
|
+
xPreviewArtifactId: z6.string().min(1).optional().describe("X only. Exact reviewed video artifact ID."),
|
|
1973
|
+
xCaption: z6.string().trim().min(1).max(2e3).optional().describe("X only. Exact post text approved by the user."),
|
|
1974
|
+
xMadeWithAi: z6.boolean().optional().describe("X only. Enable the Made with AI disclosure."),
|
|
1975
|
+
confirmXPublicPost: z6.boolean().optional().describe("True only after the user authorizes this public X post."),
|
|
1976
|
+
confirmXAutomationPolicy: z6.boolean().optional().describe(
|
|
1977
|
+
"True only after the user confirms the X automation policy."
|
|
1978
|
+
),
|
|
1979
|
+
confirmXOriginalContent: z6.boolean().optional().describe(
|
|
1980
|
+
"True only after the user confirms ownership or publishing rights."
|
|
1981
|
+
),
|
|
1982
|
+
youtubePrivacyStatus: z6.enum(["private", "unlisted", "public"]).optional().describe("YouTube only. Use 'private' for safe dogfood runs."),
|
|
1983
|
+
tiktokPostMode: z6.enum(["direct_publish", "draft_upload"]).optional().describe("TikTok only. Use 'draft_upload' for safe dogfood runs."),
|
|
1984
|
+
connectedAccountDestinationId: z6.string().uuid().optional().describe(
|
|
1088
1985
|
"Specific destination when the org has multiple accounts for the provider."
|
|
1089
1986
|
),
|
|
1090
|
-
confirmSyntheticDisclosure:
|
|
1987
|
+
confirmSyntheticDisclosure: z6.boolean().optional().describe(
|
|
1091
1988
|
"Set true when the job requires an AI synthetic-media disclosure."
|
|
1092
1989
|
),
|
|
1093
|
-
confirmQaOverride:
|
|
1990
|
+
confirmQaOverride: z6.boolean().optional().describe("Set true to override a non-passing QA gate.")
|
|
1094
1991
|
},
|
|
1095
1992
|
annotations: {
|
|
1096
1993
|
readOnlyHint: false,
|
|
@@ -1113,7 +2010,7 @@ function registerTools(server) {
|
|
|
1113
2010
|
title: "Connect social account",
|
|
1114
2011
|
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'.",
|
|
1115
2012
|
inputSchema: {
|
|
1116
|
-
provider:
|
|
2013
|
+
provider: z6.enum([
|
|
1117
2014
|
"youtube",
|
|
1118
2015
|
"x",
|
|
1119
2016
|
"tiktok",
|
|
@@ -1162,7 +2059,7 @@ function registerConfiguredTools(server, apiKey = process.env.INSTAVAR_API_KEY)
|
|
|
1162
2059
|
// package.json
|
|
1163
2060
|
var package_default = {
|
|
1164
2061
|
name: "@instavar/mcp-server",
|
|
1165
|
-
version: "0.
|
|
2062
|
+
version: "0.6.0",
|
|
1166
2063
|
mcpName: "com.instavar/mcp-server",
|
|
1167
2064
|
description: "Make local videos with Instavar Remotion templates for free, then add an API key for hosted Studio workflows.",
|
|
1168
2065
|
type: "module",
|
|
@@ -1236,7 +2133,12 @@ async function main() {
|
|
|
1236
2133
|
process.stdout.write(HELP);
|
|
1237
2134
|
return;
|
|
1238
2135
|
}
|
|
1239
|
-
const server = new McpServer(
|
|
2136
|
+
const server = new McpServer(
|
|
2137
|
+
{ name: "instavar", version: VERSION },
|
|
2138
|
+
{
|
|
2139
|
+
instructions: PAIRED_ANNOUNCEMENT_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."
|
|
2140
|
+
}
|
|
2141
|
+
);
|
|
1240
2142
|
registerConfiguredTools(server);
|
|
1241
2143
|
await server.connect(new StdioServerTransport());
|
|
1242
2144
|
}
|