@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.
- package/dist/beats.d.ts +47 -0
- package/dist/beats.js +9 -0
- package/dist/capture.d.ts +116 -0
- package/dist/capture.js +148 -0
- package/dist/config.d.ts +58 -0
- package/dist/config.js +130 -0
- package/dist/paths.d.ts +20 -0
- package/dist/paths.js +57 -0
- package/dist/scenes/pngSize.d.ts +11 -0
- package/dist/scenes/pngSize.js +75 -0
- package/dist/scenes/templates.d.ts +42 -0
- package/dist/scenes/templates.js +468 -0
- package/dist/server.d.ts +16 -0
- package/dist/server.js +37 -0
- package/dist/stdio.d.ts +2 -0
- package/dist/stdio.js +33 -0
- package/dist/tools/captureScreenRecording.d.ts +28 -0
- package/dist/tools/captureScreenRecording.js +181 -0
- package/dist/tools/captureScreenshot.d.ts +34 -0
- package/dist/tools/captureScreenshot.js +148 -0
- package/dist/tools/contactSheet.d.ts +36 -0
- package/dist/tools/contactSheet.js +116 -0
- package/dist/tools/generateNarration.d.ts +54 -0
- package/dist/tools/generateNarration.js +185 -0
- package/dist/tools/importHiggsfieldClip.d.ts +41 -0
- package/dist/tools/importHiggsfieldClip.js +108 -0
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.js +47 -0
- package/dist/tools/initProject.d.ts +37 -0
- package/dist/tools/initProject.js +166 -0
- package/dist/tools/mcp.d.ts +33 -0
- package/dist/tools/mcp.js +36 -0
- package/dist/tools/planBeats.d.ts +40 -0
- package/dist/tools/planBeats.js +143 -0
- package/dist/tools/preflight.d.ts +30 -0
- package/dist/tools/preflight.js +170 -0
- package/dist/tools/qcExtractFrames.d.ts +25 -0
- package/dist/tools/qcExtractFrames.js +129 -0
- package/dist/tools/renderVideo.d.ts +43 -0
- package/dist/tools/renderVideo.js +120 -0
- package/dist/tools/scaffoldScene.d.ts +23 -0
- package/dist/tools/scaffoldScene.js +147 -0
- package/dist/tools/stitchComposition.d.ts +24 -0
- package/dist/tools/stitchComposition.js +230 -0
- package/dist/tools/validateBeats.d.ts +14 -0
- package/dist/tools/validateBeats.js +164 -0
- package/dist/tools/validateScenes.d.ts +31 -0
- package/dist/tools/validateScenes.js +191 -0
- package/dist/tools/writeBeatsFile.d.ts +20 -0
- package/dist/tools/writeBeatsFile.js +81 -0
- package/dist/util.d.ts +43 -0
- package/dist/util.js +163 -0
- package/docs/API.md +344 -0
- package/docs/CAPTURE.md +106 -0
- package/docs/HIGGSFIELD.md +118 -0
- package/docs/NARRATION.md +111 -0
- package/docs/OVERVIEW.md +112 -0
- package/docs/PIPELINE.md +173 -0
- package/docs/PLANNING.md +206 -0
- package/docs/SCRIPT.md +50 -0
- package/docs/STYLE.md +88 -0
- package/package.json +54 -0
- package/templates/default/CLAUDE.md +63 -0
- package/templates/default/openvidstudio-core/package.json +51 -0
- package/templates/default/openvidstudio-core/scripts/gen-sfx.sh +74 -0
- package/templates/default/openvidstudio-core/src/ChecklistPanel.tsx +97 -0
- package/templates/default/openvidstudio-core/src/CinematicScene.tsx +53 -0
- package/templates/default/openvidstudio-core/src/CodePanel.tsx +122 -0
- package/templates/default/openvidstudio-core/src/CursorActor.tsx +90 -0
- package/templates/default/openvidstudio-core/src/DeviceFrame.tsx +74 -0
- package/templates/default/openvidstudio-core/src/KineticText.tsx +142 -0
- package/templates/default/openvidstudio-core/src/RepoCta.tsx +128 -0
- package/templates/default/openvidstudio-core/src/TerminalReplay.tsx +263 -0
- package/templates/default/openvidstudio-core/src/camera.tsx +141 -0
- package/templates/default/openvidstudio-core/src/index.ts +60 -0
- package/templates/default/openvidstudio-core/src/look.tsx +90 -0
- package/templates/default/openvidstudio-core/src/motion.ts +50 -0
- package/templates/default/openvidstudio-core/src/sfx.tsx +106 -0
- package/templates/default/openvidstudio-core/src/tokens.ts +41 -0
- package/templates/default/package.json +31 -0
- package/templates/default/public/sfx/bell.wav +0 -0
- package/templates/default/public/sfx/blip.wav +0 -0
- package/templates/default/public/sfx/click.wav +0 -0
- package/templates/default/public/sfx/key_a.wav +0 -0
- package/templates/default/public/sfx/key_b.wav +0 -0
- package/templates/default/public/sfx/key_c.wav +0 -0
- package/templates/default/public/sfx/key_enter.wav +0 -0
- package/templates/default/public/sfx/music-bed.mp3 +0 -0
- package/templates/default/public/sfx/success.wav +0 -0
- package/templates/default/public/sfx/whoosh.wav +0 -0
- package/templates/default/remotion.config.ts +13 -0
- package/templates/default/src/Root.tsx +9 -0
- package/templates/default/src/index.ts +4 -0
- 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
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/stdio.d.ts
ADDED
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;
|