@instavar/mcp-server 0.6.0 → 0.7.0

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