@openvidstudio/mcp-server 0.1.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 (94) hide show
  1. package/dist/beats.d.ts +47 -0
  2. package/dist/beats.js +9 -0
  3. package/dist/capture.d.ts +116 -0
  4. package/dist/capture.js +148 -0
  5. package/dist/config.d.ts +58 -0
  6. package/dist/config.js +130 -0
  7. package/dist/paths.d.ts +20 -0
  8. package/dist/paths.js +57 -0
  9. package/dist/scenes/pngSize.d.ts +11 -0
  10. package/dist/scenes/pngSize.js +75 -0
  11. package/dist/scenes/templates.d.ts +42 -0
  12. package/dist/scenes/templates.js +468 -0
  13. package/dist/server.d.ts +16 -0
  14. package/dist/server.js +37 -0
  15. package/dist/stdio.d.ts +2 -0
  16. package/dist/stdio.js +33 -0
  17. package/dist/tools/captureScreenRecording.d.ts +28 -0
  18. package/dist/tools/captureScreenRecording.js +181 -0
  19. package/dist/tools/captureScreenshot.d.ts +34 -0
  20. package/dist/tools/captureScreenshot.js +148 -0
  21. package/dist/tools/contactSheet.d.ts +36 -0
  22. package/dist/tools/contactSheet.js +116 -0
  23. package/dist/tools/generateNarration.d.ts +54 -0
  24. package/dist/tools/generateNarration.js +185 -0
  25. package/dist/tools/importHiggsfieldClip.d.ts +41 -0
  26. package/dist/tools/importHiggsfieldClip.js +108 -0
  27. package/dist/tools/index.d.ts +13 -0
  28. package/dist/tools/index.js +47 -0
  29. package/dist/tools/initProject.d.ts +37 -0
  30. package/dist/tools/initProject.js +166 -0
  31. package/dist/tools/mcp.d.ts +33 -0
  32. package/dist/tools/mcp.js +36 -0
  33. package/dist/tools/planBeats.d.ts +40 -0
  34. package/dist/tools/planBeats.js +143 -0
  35. package/dist/tools/preflight.d.ts +30 -0
  36. package/dist/tools/preflight.js +170 -0
  37. package/dist/tools/qcExtractFrames.d.ts +25 -0
  38. package/dist/tools/qcExtractFrames.js +129 -0
  39. package/dist/tools/renderVideo.d.ts +43 -0
  40. package/dist/tools/renderVideo.js +120 -0
  41. package/dist/tools/scaffoldScene.d.ts +23 -0
  42. package/dist/tools/scaffoldScene.js +147 -0
  43. package/dist/tools/stitchComposition.d.ts +24 -0
  44. package/dist/tools/stitchComposition.js +230 -0
  45. package/dist/tools/validateBeats.d.ts +14 -0
  46. package/dist/tools/validateBeats.js +164 -0
  47. package/dist/tools/validateScenes.d.ts +31 -0
  48. package/dist/tools/validateScenes.js +191 -0
  49. package/dist/tools/writeBeatsFile.d.ts +20 -0
  50. package/dist/tools/writeBeatsFile.js +81 -0
  51. package/dist/util.d.ts +43 -0
  52. package/dist/util.js +163 -0
  53. package/docs/API.md +344 -0
  54. package/docs/CAPTURE.md +106 -0
  55. package/docs/HIGGSFIELD.md +118 -0
  56. package/docs/NARRATION.md +111 -0
  57. package/docs/OVERVIEW.md +112 -0
  58. package/docs/PIPELINE.md +173 -0
  59. package/docs/PLANNING.md +206 -0
  60. package/docs/SCRIPT.md +50 -0
  61. package/docs/STYLE.md +88 -0
  62. package/package.json +54 -0
  63. package/templates/default/CLAUDE.md +63 -0
  64. package/templates/default/openvidstudio-core/package.json +51 -0
  65. package/templates/default/openvidstudio-core/scripts/gen-sfx.sh +74 -0
  66. package/templates/default/openvidstudio-core/src/ChecklistPanel.tsx +97 -0
  67. package/templates/default/openvidstudio-core/src/CinematicScene.tsx +53 -0
  68. package/templates/default/openvidstudio-core/src/CodePanel.tsx +122 -0
  69. package/templates/default/openvidstudio-core/src/CursorActor.tsx +90 -0
  70. package/templates/default/openvidstudio-core/src/DeviceFrame.tsx +74 -0
  71. package/templates/default/openvidstudio-core/src/KineticText.tsx +142 -0
  72. package/templates/default/openvidstudio-core/src/RepoCta.tsx +128 -0
  73. package/templates/default/openvidstudio-core/src/TerminalReplay.tsx +263 -0
  74. package/templates/default/openvidstudio-core/src/camera.tsx +141 -0
  75. package/templates/default/openvidstudio-core/src/index.ts +60 -0
  76. package/templates/default/openvidstudio-core/src/look.tsx +90 -0
  77. package/templates/default/openvidstudio-core/src/motion.ts +50 -0
  78. package/templates/default/openvidstudio-core/src/sfx.tsx +106 -0
  79. package/templates/default/openvidstudio-core/src/tokens.ts +41 -0
  80. package/templates/default/package.json +31 -0
  81. package/templates/default/public/sfx/bell.wav +0 -0
  82. package/templates/default/public/sfx/blip.wav +0 -0
  83. package/templates/default/public/sfx/click.wav +0 -0
  84. package/templates/default/public/sfx/key_a.wav +0 -0
  85. package/templates/default/public/sfx/key_b.wav +0 -0
  86. package/templates/default/public/sfx/key_c.wav +0 -0
  87. package/templates/default/public/sfx/key_enter.wav +0 -0
  88. package/templates/default/public/sfx/music-bed.mp3 +0 -0
  89. package/templates/default/public/sfx/success.wav +0 -0
  90. package/templates/default/public/sfx/whoosh.wav +0 -0
  91. package/templates/default/remotion.config.ts +13 -0
  92. package/templates/default/src/Root.tsx +9 -0
  93. package/templates/default/src/index.ts +4 -0
  94. package/templates/default/tsconfig.json +16 -0
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Scene templates.
3
+ *
4
+ * scaffold_scene used to emit a TODO comment and an empty fragment for dom-demo
5
+ * beats, which meant the calling agent had to invent a scene from primitives for
6
+ * every beat in the video. Building one real 20 beat video that way took over two
7
+ * thousand lines of hand written React. Agents edit a working thing far better than
8
+ * they compose one from nothing, so every template here renders on its own with no
9
+ * edits, and the agent's job becomes changing content rather than building structure.
10
+ *
11
+ * Templates only use what `@openvidstudio/core` exports, because that is all a
12
+ * scaffolded project has.
13
+ *
14
+ * Two things encoded here that are easy to get wrong and expensive to discover:
15
+ *
16
+ * 1. Camera scale. The visible area is the stage divided by the camera scale, so at
17
+ * 1.4 only 1371x771 of the 1920x1080 stage is on screen. Panels built at stage
18
+ * size get cropped at the frame edge with no warning. Constructed scenes
19
+ * therefore use a gentle push (1.04 to 1.15) and keep content inside a safe box.
20
+ * STYLE.md's 120-250% rule is about not showing flat full screen screenshots,
21
+ * which is a capture-beat concern, and capture templates do use it.
22
+ *
23
+ * 2. Real captures get their frame size from the file on disk, not from a guess.
24
+ */
25
+ export type SceneKind = "real-screenshot" | "real-recording" | "dom-demo" | "higgsfield-clip";
26
+ export type TemplateName = "browser-capture" | "recording" | "terminal" | "split-panel" | "checklist" | "stat" | "code" | "comparison" | "cta" | "title";
27
+ export interface SceneContext {
28
+ beatId: string;
29
+ componentName: string;
30
+ /** The beat's `visual.description`, used as on-screen copy so the first render says something. */
31
+ description: string;
32
+ /** The beat's narration line, used for the caption. */
33
+ vo: string;
34
+ durationFrames: number;
35
+ /** For capture templates. */
36
+ url?: string;
37
+ captureWidth?: number;
38
+ captureHeight?: number;
39
+ }
40
+ /** Pick a template from the beat's capture method and what its description talks about. */
41
+ export declare function pickTemplate(kind: SceneKind, description: string): TemplateName;
42
+ export declare function renderTemplate(template: TemplateName, ctx: SceneContext, kind: SceneKind): string;
@@ -0,0 +1,468 @@
1
+ "use strict";
2
+ /**
3
+ * Scene templates.
4
+ *
5
+ * scaffold_scene used to emit a TODO comment and an empty fragment for dom-demo
6
+ * beats, which meant the calling agent had to invent a scene from primitives for
7
+ * every beat in the video. Building one real 20 beat video that way took over two
8
+ * thousand lines of hand written React. Agents edit a working thing far better than
9
+ * they compose one from nothing, so every template here renders on its own with no
10
+ * edits, and the agent's job becomes changing content rather than building structure.
11
+ *
12
+ * Templates only use what `@openvidstudio/core` exports, because that is all a
13
+ * scaffolded project has.
14
+ *
15
+ * Two things encoded here that are easy to get wrong and expensive to discover:
16
+ *
17
+ * 1. Camera scale. The visible area is the stage divided by the camera scale, so at
18
+ * 1.4 only 1371x771 of the 1920x1080 stage is on screen. Panels built at stage
19
+ * size get cropped at the frame edge with no warning. Constructed scenes
20
+ * therefore use a gentle push (1.04 to 1.15) and keep content inside a safe box.
21
+ * STYLE.md's 120-250% rule is about not showing flat full screen screenshots,
22
+ * which is a capture-beat concern, and capture templates do use it.
23
+ *
24
+ * 2. Real captures get their frame size from the file on disk, not from a guess.
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.pickTemplate = pickTemplate;
28
+ exports.renderTemplate = renderTemplate;
29
+ /** Content wider or taller than this gets cropped once the camera pushes in. */
30
+ const SAFE_W = 1560;
31
+ const HEADER = (beatId, template, direction = "") => `// Scene for beat "${beatId}", generated by scaffold_scene from the "${template}" template.
32
+ //
33
+ // This renders as-is. Edit the copy and the timings; the structure, camera and
34
+ // safe-area sizing are already correct.${direction ? `
35
+ //
36
+ // The beat asks for: ${direction.replace(/\s+/g, " ").slice(0, 240)}` : ""}`;
37
+ /**
38
+ * Heading text.
39
+ *
40
+ * Deliberately not `visual.description`. That field is a stage direction written for
41
+ * whoever builds the scene ("two labelled panels built from this project's tokens"),
42
+ * and putting it on screen reads as instructions leaking into the video. The beat id
43
+ * is short, already names the beat's subject, and title-cases into a usable heading.
44
+ */
45
+ function headline(ctx) {
46
+ const words = ctx.beatId.replace(/[-_]+/g, " ").trim().split(/\s+/);
47
+ const text = words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
48
+ return esc(text || "Untitled beat");
49
+ }
50
+ /** First sentence of the narration, for supporting copy under a heading. */
51
+ function leadLine(ctx, max = 150) {
52
+ const first = (ctx.vo || "").split(/(?<=[.!?])\s/)[0]?.trim() ?? "";
53
+ const text = first || ctx.description || "";
54
+ return esc(text.length > max ? `${text.slice(0, max - 3).trimEnd()}...` : text);
55
+ }
56
+ function esc(s) {
57
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, " ");
58
+ }
59
+ /** Caption timing: in after the opening settle, out before the cut. */
60
+ function captionWindow(frames) {
61
+ return { at: Math.round(frames * 0.18), out: Math.max(24, Math.round(frames * 0.82)) };
62
+ }
63
+ /* ------------------------------------------------------------------ capture */
64
+ function browserCapture(ctx) {
65
+ const w = ctx.captureWidth ?? 1440;
66
+ const h = ctx.captureHeight ?? 900;
67
+ // BrowserFrame adds a chrome bar above the page.
68
+ const frameW = Math.min(1600, w + 80);
69
+ const frameH = Math.round(frameW * (h / w)) + 56;
70
+ const mid = Math.round(ctx.durationFrames * 0.55);
71
+ const cap = captionWindow(ctx.durationFrames);
72
+ return `${HEADER(ctx.beatId, "browser-capture", ctx.description)}
73
+ //
74
+ // Frame size comes from the real capture on disk (${w}x${h}), not a guess, so the
75
+ // page is not letterboxed or stretched. Camera sits inside the UI and keeps moving.
76
+
77
+ import React from "react";
78
+ import { Img, staticFile } from "remotion";
79
+ import { CinematicScene, Layer, BrowserFrame, Caption, E } from "@openvidstudio/core";
80
+
81
+ const FRAME_W = ${frameW};
82
+ const FRAME_H = ${frameH};
83
+ const LEFT = (1920 - FRAME_W) / 2;
84
+ const TOP = (1080 - FRAME_H) / 2;
85
+
86
+ export const ${ctx.componentName}: React.FC = () => {
87
+ return (
88
+ <CinematicScene
89
+ camera={[
90
+ { frame: 0, x: 960, y: 470, scale: 1.35, rotZ: -0.5 },
91
+ { frame: ${mid}, x: 960, y: 560, scale: 1.62, rotZ: 0.4, easing: E.cinematic },
92
+ { frame: ${ctx.durationFrames}, x: 985, y: 620, scale: 1.78, rotZ: 0.8, easing: E.drift },
93
+ ]}
94
+ overlay={<Caption text="${esc(ctx.vo).slice(0, 70)}" at={${cap.at}} out={${cap.out}} fontSize={30} />}
95
+ >
96
+ <Layer depth={0}>
97
+ <div style={{ position: "absolute", left: LEFT, top: TOP }}>
98
+ <BrowserFrame url="${esc(ctx.url ?? "localhost:3000")}" width={FRAME_W} height={FRAME_H}>
99
+ <Img
100
+ src={staticFile("images/${ctx.beatId}.png")}
101
+ style={{ width: "100%", height: "100%", objectFit: "cover", objectPosition: "top" }}
102
+ />
103
+ </BrowserFrame>
104
+ </div>
105
+ </Layer>
106
+ </CinematicScene>
107
+ );
108
+ };
109
+ `;
110
+ }
111
+ function recording(ctx, higgsfield) {
112
+ const note = higgsfield
113
+ ? `// The asset comes from import_higgsfield_clip. Composited as a bare Layer and
114
+ // OffthreadVideo, never a BrowserFrame: an AI generated clip must not carry the
115
+ // visual signature of real captured UI (STYLE.md's provenance rule).`
116
+ : `// Composited as a bare Layer and OffthreadVideo, per STYLE.md's convention for
117
+ // recordings. The camera moves over the recording exactly as it would over a still.`;
118
+ const cap = captionWindow(ctx.durationFrames);
119
+ return `${HEADER(ctx.beatId, "recording", ctx.description)}
120
+ //
121
+ ${note}
122
+
123
+ import React from "react";
124
+ import { OffthreadVideo, staticFile } from "remotion";
125
+ import { CinematicScene, Layer, Caption, E } from "@openvidstudio/core";
126
+
127
+ export const ${ctx.componentName}: React.FC = () => {
128
+ return (
129
+ <CinematicScene
130
+ camera={[
131
+ { frame: 0, x: 960, y: 520, scale: 1.28, rotZ: 0.4 },
132
+ { frame: ${ctx.durationFrames}, x: 960, y: 560, scale: 1.5, rotZ: -0.3, easing: E.drift },
133
+ ]}
134
+ overlay={<Caption text="${esc(ctx.vo).slice(0, 70)}" at={${cap.at}} out={${cap.out}} fontSize={30} />}
135
+ >
136
+ <Layer depth={0}>
137
+ <OffthreadVideo
138
+ src={staticFile("video/${ctx.beatId}.mp4")}
139
+ style={{ width: "100%", height: "100%", objectFit: "cover" }}
140
+ />
141
+ </Layer>
142
+ </CinematicScene>
143
+ );
144
+ };
145
+ `;
146
+ }
147
+ /* ------------------------------------------------------------- constructed */
148
+ const SAFE_WRAP = (inner) => ` <Layer depth={0}>
149
+ <div
150
+ style={{
151
+ position: "absolute",
152
+ inset: 0,
153
+ display: "flex",
154
+ alignItems: "center",
155
+ justifyContent: "center",
156
+ padding: "0 ${(1920 - SAFE_W) / 2}px",
157
+ }}
158
+ >
159
+ ${inner}
160
+ </div>
161
+ </Layer>`;
162
+ const GENTLE_CAMERA = (frames, a = 1.04, b = 1.15) => ` camera={[
163
+ { frame: 0, x: 960, y: 540, scale: ${a}, rotZ: -0.4 },
164
+ { frame: ${frames}, x: 960, y: 548, scale: ${b}, rotZ: 0.35, easing: E.drift },
165
+ ]}`;
166
+ function terminal(ctx) {
167
+ return `${HEADER(ctx.beatId, "terminal", ctx.description)}
168
+
169
+ import React from "react";
170
+ import { CinematicScene, Layer, TerminalReplay, color, E } from "@openvidstudio/core";
171
+
172
+ export const ${ctx.componentName}: React.FC = () => {
173
+ return (
174
+ <CinematicScene
175
+ ${GENTLE_CAMERA(ctx.durationFrames, 1.05, 1.16)}
176
+ >
177
+ ${SAFE_WRAP(` <TerminalReplay
178
+ title="Project"
179
+ width={1400}
180
+ fontSize={27}
181
+ cps={24}
182
+ steps={[
183
+ { type: "cmd", text: "echo replace me with the real command" },
184
+ {
185
+ type: "out",
186
+ lines: [
187
+ { text: "replace with the real output", color: color.textSecondary },
188
+ { text: "done", color: color.success, glow: true },
189
+ ],
190
+ },
191
+ ]}
192
+ />`)}
193
+ </CinematicScene>
194
+ );
195
+ };
196
+ `;
197
+ }
198
+ function splitPanel(ctx) {
199
+ return `${HEADER(ctx.beatId, "split-panel", ctx.description)}
200
+
201
+ import React from "react";
202
+ import { CinematicScene, Layer, color, font, radius, panelShadow, E } from "@openvidstudio/core";
203
+
204
+ const MONO = \`"\${font.mono}", ui-monospace, monospace\`;
205
+ const UI = \`"\${font.ui}", system-ui, sans-serif\`;
206
+
207
+ export const ${ctx.componentName}: React.FC = () => {
208
+ return (
209
+ <CinematicScene
210
+ ${GENTLE_CAMERA(ctx.durationFrames)}
211
+ >
212
+ ${SAFE_WRAP(` <div style={{ display: "flex", alignItems: "center", gap: 72, width: "100%" }}>
213
+ <div style={{ display: "flex", flexDirection: "column", gap: 20, flex: "0 0 560px" }}>
214
+ <span style={{ fontFamily: MONO, fontSize: 20, letterSpacing: 4, textTransform: "uppercase", color: color.accent }}>
215
+ ${esc(ctx.beatId.replace(/[-_]/g, " "))}
216
+ </span>
217
+ <h2 style={{ margin: 0, fontFamily: MONO, fontSize: 60, fontWeight: 700, lineHeight: 1.1, color: color.textPrimary }}>
218
+ ${headline(ctx)}
219
+ </h2>
220
+ <p style={{ margin: 0, fontFamily: UI, fontSize: 26, lineHeight: 1.5, color: color.textSecondary }}>
221
+ ${leadLine(ctx, 150)}
222
+ </p>
223
+ </div>
224
+ <div
225
+ style={{
226
+ flex: 1,
227
+ minHeight: 460,
228
+ borderRadius: radius.window,
229
+ background: "#10141D",
230
+ border: \`1px solid \${color.panelBorder}\`,
231
+ boxShadow: panelShadow(true),
232
+ padding: 30,
233
+ fontFamily: UI,
234
+ fontSize: 24,
235
+ color: color.textSecondary,
236
+ }}
237
+ >
238
+ ${leadLine(ctx, 180)}
239
+ </div>
240
+ </div>`)}
241
+ </CinematicScene>
242
+ );
243
+ };
244
+ `;
245
+ }
246
+ function checklist(ctx) {
247
+ const step = Math.max(20, Math.floor(ctx.durationFrames / 6));
248
+ return `${HEADER(ctx.beatId, "checklist", ctx.description)}
249
+
250
+ import React from "react";
251
+ import { CinematicScene, Layer, ChecklistPanel, E } from "@openvidstudio/core";
252
+
253
+ export const ${ctx.componentName}: React.FC = () => {
254
+ return (
255
+ <CinematicScene
256
+ ${GENTLE_CAMERA(ctx.durationFrames)}
257
+ >
258
+ ${SAFE_WRAP(` <ChecklistPanel
259
+ title="${headline(ctx)}"
260
+ width={860}
261
+ fontSize={27}
262
+ items={[
263
+ { label: "First item", flipAt: ${step} },
264
+ { label: "Second item", flipAt: ${step * 2} },
265
+ { label: "Third item", flipAt: ${step * 3} },
266
+ { label: "Fourth item", flipAt: ${step * 4} },
267
+ ]}
268
+ />`)}
269
+ </CinematicScene>
270
+ );
271
+ };
272
+ `;
273
+ }
274
+ function stat(ctx) {
275
+ return `${HEADER(ctx.beatId, "stat", ctx.description)}
276
+
277
+ import React from "react";
278
+ import { CinematicScene, Layer, color, font, glow, E } from "@openvidstudio/core";
279
+
280
+ const MONO = \`"\${font.mono}", ui-monospace, monospace\`;
281
+
282
+ export const ${ctx.componentName}: React.FC = () => {
283
+ return (
284
+ <CinematicScene
285
+ ${GENTLE_CAMERA(ctx.durationFrames, 1.06, 1.18)}
286
+ >
287
+ ${SAFE_WRAP(` <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 24 }}>
288
+ <span style={{ fontFamily: MONO, fontSize: 190, fontWeight: 700, lineHeight: 1, color: color.accent, ...glow(color.accent, 0.8) }}>
289
+ 0
290
+ </span>
291
+ <span style={{ fontFamily: MONO, fontSize: 34, color: color.textSecondary, textAlign: "center", maxWidth: 900 }}>
292
+ ${headline(ctx)}
293
+ </span>
294
+ </div>`)}
295
+ </CinematicScene>
296
+ );
297
+ };
298
+ `;
299
+ }
300
+ function code(ctx) {
301
+ return `${HEADER(ctx.beatId, "code", ctx.description)}
302
+
303
+ import React from "react";
304
+ import { CinematicScene, Layer, CodePanel, E } from "@openvidstudio/core";
305
+
306
+ export const ${ctx.componentName}: React.FC = () => {
307
+ return (
308
+ <CinematicScene
309
+ ${GENTLE_CAMERA(ctx.durationFrames)}
310
+ >
311
+ ${SAFE_WRAP(` <CodePanel
312
+ title="${esc(ctx.beatId)}"
313
+ width={1000}
314
+ height={560}
315
+ fontSize={24}
316
+ highlight={[1]}
317
+ lines={[
318
+ [{ t: "{", c: "plain" }],
319
+ [{ t: " \\"replace\\"", c: "str" }, { t: ": ", c: "plain" }, { t: "\\"me\\"", c: "str" }],
320
+ [{ t: "}", c: "plain" }],
321
+ ]}
322
+ />`)}
323
+ </CinematicScene>
324
+ );
325
+ };
326
+ `;
327
+ }
328
+ function comparison(ctx) {
329
+ return `${HEADER(ctx.beatId, "comparison", ctx.description)}
330
+
331
+ import React from "react";
332
+ import { CinematicScene, Layer, color, font, radius, panelShadow, E } from "@openvidstudio/core";
333
+
334
+ const MONO = \`"\${font.mono}", ui-monospace, monospace\`;
335
+ const UI = \`"\${font.ui}", system-ui, sans-serif\`;
336
+
337
+ const Side: React.FC<{ tag: string; title: string; accent: string; body: string }> = ({
338
+ tag, title, accent, body,
339
+ }) => (
340
+ <div
341
+ style={{
342
+ flex: 1,
343
+ borderRadius: radius.window,
344
+ background: "#10141D",
345
+ border: \`1px solid \${accent}55\`,
346
+ boxShadow: panelShadow(true),
347
+ padding: 34,
348
+ display: "flex",
349
+ flexDirection: "column",
350
+ gap: 16,
351
+ }}
352
+ >
353
+ <span style={{ fontFamily: MONO, fontSize: 20, color: accent }}>{tag}</span>
354
+ <span style={{ fontFamily: MONO, fontSize: 34, color: color.textPrimary }}>{title}</span>
355
+ <span style={{ fontFamily: UI, fontSize: 24, lineHeight: 1.45, color: color.textSecondary }}>{body}</span>
356
+ </div>
357
+ );
358
+
359
+ export const ${ctx.componentName}: React.FC = () => {
360
+ return (
361
+ <CinematicScene
362
+ ${GENTLE_CAMERA(ctx.durationFrames)}
363
+ >
364
+ ${SAFE_WRAP(` <div style={{ display: "flex", gap: 44, width: "100%", alignItems: "stretch" }}>
365
+ <Side tag="option one" title="Replace me" accent={color.danger} body="Replace with the first option" />
366
+ <Side tag="option two" title="Replace me" accent={color.accent} body="${leadLine(ctx, 110)}" />
367
+ </div>`)}
368
+ </CinematicScene>
369
+ );
370
+ };
371
+ `;
372
+ }
373
+ function cta(ctx) {
374
+ return `${HEADER(ctx.beatId, "cta", ctx.description)}
375
+ //
376
+ // RepoCta ships without a star action by default, per OVERVIEW.md.
377
+
378
+ import React from "react";
379
+ import { RepoCta } from "@openvidstudio/core";
380
+
381
+ export const ${ctx.componentName}: React.FC = () => {
382
+ return (
383
+ <RepoCta
384
+ owner="your-org"
385
+ name="your-repo"
386
+ description="${leadLine(ctx, 110)}"
387
+ chips={["open source", "MIT"]}
388
+ titleText="${headline(ctx)}"
389
+ durationInFrames={${ctx.durationFrames}}
390
+ />
391
+ );
392
+ };
393
+ `;
394
+ }
395
+ function title(ctx) {
396
+ return `${HEADER(ctx.beatId, "title", ctx.description)}
397
+
398
+ import React from "react";
399
+ import { CinematicScene, Layer, color, font, E } from "@openvidstudio/core";
400
+
401
+ const MONO = \`"\${font.mono}", ui-monospace, monospace\`;
402
+ const UI = \`"\${font.ui}", system-ui, sans-serif\`;
403
+
404
+ export const ${ctx.componentName}: React.FC = () => {
405
+ return (
406
+ <CinematicScene
407
+ ${GENTLE_CAMERA(ctx.durationFrames, 1.05, 1.17)}
408
+ >
409
+ ${SAFE_WRAP(` <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 28, textAlign: "center" }}>
410
+ <h2 style={{ margin: 0, fontFamily: MONO, fontSize: 72, fontWeight: 700, lineHeight: 1.1, color: color.textPrimary, maxWidth: 1300 }}>
411
+ ${headline(ctx)}
412
+ </h2>
413
+ <p style={{ margin: 0, fontFamily: UI, fontSize: 30, lineHeight: 1.5, color: color.textSecondary, maxWidth: 1000 }}>
414
+ ${leadLine(ctx, 160)}
415
+ </p>
416
+ </div>`)}
417
+ </CinematicScene>
418
+ );
419
+ };
420
+ `;
421
+ }
422
+ /* -------------------------------------------------------------- selection */
423
+ const KEYWORDS = [
424
+ [/\bterminal|command|shell|cli\b|npm |npx |install|clone|build\b/i, "terminal"],
425
+ [/\bchecklist|requirement|prereq|steps?\b|tick|verify\b/i, "checklist"],
426
+ [/\bconfig|json|code panel|snippet|mcp\.json|package\.json\b/i, "code"],
427
+ [/\bversus|vs\.?\b|compare|comparison|two (options|panels|tiers)|either\b/i, "comparison"],
428
+ [/\bcta|call to action|repo|github|star\b/i, "cta"],
429
+ [/\bstat|number|percent|count|figure|\$\d/i, "stat"],
430
+ [/\bpanel|split|beside|alongside|left|right\b/i, "split-panel"],
431
+ ];
432
+ /** Pick a template from the beat's capture method and what its description talks about. */
433
+ function pickTemplate(kind, description) {
434
+ if (kind === "real-screenshot")
435
+ return "browser-capture";
436
+ if (kind === "real-recording" || kind === "higgsfield-clip")
437
+ return "recording";
438
+ for (const [re, name] of KEYWORDS) {
439
+ if (re.test(description))
440
+ return name;
441
+ }
442
+ return "title";
443
+ }
444
+ function renderTemplate(template, ctx, kind) {
445
+ switch (template) {
446
+ case "browser-capture":
447
+ return browserCapture(ctx);
448
+ case "recording":
449
+ return recording(ctx, kind === "higgsfield-clip");
450
+ case "terminal":
451
+ return terminal(ctx);
452
+ case "split-panel":
453
+ return splitPanel(ctx);
454
+ case "checklist":
455
+ return checklist(ctx);
456
+ case "stat":
457
+ return stat(ctx);
458
+ case "code":
459
+ return code(ctx);
460
+ case "comparison":
461
+ return comparison(ctx);
462
+ case "cta":
463
+ return cta(ctx);
464
+ case "title":
465
+ default:
466
+ return title(ctx);
467
+ }
468
+ }
@@ -0,0 +1,16 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { OpenvidstudioConfig } from "./config";
3
+ export declare const SERVER_NAME = "openvidstudio";
4
+ export declare const SERVER_VERSION = "0.1.0";
5
+ /**
6
+ * Assembly only, mirrors pepiros/mcp/server.ts's shape: transport lives in the entry
7
+ * point (stdio.ts), not here, so the same server instance could later be served over a
8
+ * different transport without touching the tool layer.
9
+ *
10
+ * Unlike pepiros, there is no auth/token layer -- this is a local process a dev's
11
+ * coding agent spawns directly (like Playwright MCP or context7's MCP), operating on
12
+ * the local filesystem and local processes (remotion CLI, ffmpeg). `config` (loaded by
13
+ * stdio.ts from process.cwd() at startup) is threaded through to registerTools, whose
14
+ * only use of it is gating import_higgsfield_clip's registration on hasHiggsfield.
15
+ */
16
+ export declare function createMcpServer(config?: OpenvidstudioConfig): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SERVER_VERSION = exports.SERVER_NAME = void 0;
4
+ exports.createMcpServer = createMcpServer;
5
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
6
+ const tools_1 = require("./tools");
7
+ exports.SERVER_NAME = "openvidstudio";
8
+ exports.SERVER_VERSION = "0.1.0";
9
+ /**
10
+ * Assembly only, mirrors pepiros/mcp/server.ts's shape: transport lives in the entry
11
+ * point (stdio.ts), not here, so the same server instance could later be served over a
12
+ * different transport without touching the tool layer.
13
+ *
14
+ * Unlike pepiros, there is no auth/token layer -- this is a local process a dev's
15
+ * coding agent spawns directly (like Playwright MCP or context7's MCP), operating on
16
+ * the local filesystem and local processes (remotion CLI, ffmpeg). `config` (loaded by
17
+ * stdio.ts from process.cwd() at startup) is threaded through to registerTools, whose
18
+ * only use of it is gating import_higgsfield_clip's registration on hasHiggsfield.
19
+ */
20
+ function createMcpServer(config) {
21
+ const server = new mcp_js_1.McpServer({ name: exports.SERVER_NAME, version: exports.SERVER_VERSION }, {
22
+ instructions: [
23
+ "openvidstudio exposes the mechanical half of the video pipeline: project scaffolding, beats.json " +
24
+ "validation and writes, scene stubs, composition stitching, rendering, and QC frame extraction.",
25
+ "",
26
+ "This server does not call an LLM and does not draft content. Reasoning (PLANNING.md's intake " +
27
+ "questions, beat wording, scene JSX content, and the mandatory human-approval gate before " +
28
+ "write_beats_file) stays with you, the calling agent.",
29
+ "",
30
+ "Read PLANNING.md and PIPELINE.md before drafting a beats.json: in this monorepo that's " +
31
+ "packages/docs/; in a scaffolded project it's the project's own docs/ directory, which " +
32
+ "init_project copies in from this package's bundled docs.",
33
+ ].join("\n"),
34
+ });
35
+ (0, tools_1.registerTools)(server, config);
36
+ return server;
37
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/stdio.js ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
5
+ const server_1 = require("./server");
6
+ const config_1 = require("./config");
7
+ /**
8
+ * stdio entry point -- `openvidstudio-mcp` once installed, or wired directly into an
9
+ * agent's MCP config as a command.
10
+ *
11
+ * Nothing may be written to stdout except protocol frames: stdout IS the transport.
12
+ * Any diagnostic goes to stderr, or it corrupts the JSON-RPC stream and the client
13
+ * disconnects with a parse error. This is the single most important constraint in this
14
+ * package -- violate it anywhere (a stray console.log, a library that logs to stdout
15
+ * by default) and the whole server breaks silently from the calling agent's perspective.
16
+ *
17
+ * Loads openvidstudio.config.json from process.cwd() (the calling agent's project root,
18
+ * same default resolveProjectRoot uses everywhere else in this package) once, here, at
19
+ * startup: this is what gives registerTools' hasHiggsfield gate something to read.
20
+ * loadConfig fills in defaults for a missing file, so an un-initialized project still
21
+ * starts cleanly with import_higgsfield_clip simply absent.
22
+ */
23
+ async function main() {
24
+ const config = (0, config_1.loadConfig)(process.cwd());
25
+ const server = (0, server_1.createMcpServer)(config);
26
+ const transport = new stdio_js_1.StdioServerTransport();
27
+ await server.connect(transport);
28
+ process.stderr.write("openvidstudio-mcp: connected over stdio\n");
29
+ }
30
+ main().catch((err) => {
31
+ process.stderr.write(`openvidstudio-mcp: fatal: ${err instanceof Error ? err.stack : String(err)}\n`);
32
+ process.exit(1);
33
+ });
@@ -0,0 +1,28 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { type Interaction, type Viewport } from "../capture";
3
+ export interface CaptureScreenRecordingInput {
4
+ projectRoot?: string;
5
+ beatId: string;
6
+ url: string;
7
+ viewport?: Viewport;
8
+ interactions?: Interaction[];
9
+ outPath?: string;
10
+ }
11
+ export interface CaptureScreenRecordingResult {
12
+ outPath: string;
13
+ width: number;
14
+ height: number;
15
+ zoom: number;
16
+ durationMs?: number;
17
+ }
18
+ /**
19
+ * Pure argv builder, unit-testable without spawning ffmpeg. Remotion's
20
+ * OffthreadVideo needs a seekable format, so Playwright's webm output gets
21
+ * transcoded to mp4 (h264/yuv420p, +faststart) -- same spawn/argv-array
22
+ * discipline as render_video/qc_extract_frames.
23
+ */
24
+ export declare function buildFfmpegTranscodeArgs(inputPath: string, outputPath: string): string[];
25
+ /** Pure argv builder for the best-effort ffprobe duration lookup. */
26
+ export declare function buildFfprobeDurationArgs(filePath: string): string[];
27
+ export declare function runCaptureScreenRecording(input: CaptureScreenRecordingInput): Promise<CaptureScreenRecordingResult>;
28
+ export declare function registerCaptureScreenRecording(server: McpServer): void;