@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,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.buildRenderCommand = buildRenderCommand;
|
|
37
|
+
exports.runRenderVideo = runRenderVideo;
|
|
38
|
+
exports.registerRenderVideo = registerRenderVideo;
|
|
39
|
+
const fs = __importStar(require("node:fs"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const zod_1 = require("zod");
|
|
42
|
+
const util_1 = require("../util");
|
|
43
|
+
const mcp_1 = require("./mcp");
|
|
44
|
+
const COMPOSITION_ID_RE = /^[A-Za-z0-9_-]+$/;
|
|
45
|
+
/**
|
|
46
|
+
* Pure argv builder, unit-testable without spawning a real process. On Windows, npx is
|
|
47
|
+
* a .cmd shim -- Node's spawn() can only execute that via an internal cmd.exe hop even
|
|
48
|
+
* with shell:false, which is why compositionId/outPath both get sanitized against
|
|
49
|
+
* cmd.exe metacharacters before ever reaching this function (see util.ts).
|
|
50
|
+
*/
|
|
51
|
+
function buildRenderCommand(compositionId, outPath, opts = {}) {
|
|
52
|
+
const args = ["remotion", "render", compositionId, outPath];
|
|
53
|
+
if (opts.draft) {
|
|
54
|
+
// Half resolution and a fast x264 preset. Enough to judge framing, motion and
|
|
55
|
+
// timing; not enough to judge final text crispness.
|
|
56
|
+
args.push("--scale=0.5", "--crf=32", "--x264-preset=veryfast");
|
|
57
|
+
}
|
|
58
|
+
if (opts.concurrency && opts.concurrency > 0) {
|
|
59
|
+
args.push(`--concurrency=${Math.floor(opts.concurrency)}`);
|
|
60
|
+
}
|
|
61
|
+
// Remotion suppresses its progress bar when stdout is not a TTY, which is always
|
|
62
|
+
// the case here, so a piped render writes an empty log for its whole run and an
|
|
63
|
+
// agent watching it cannot tell progress from a hang.
|
|
64
|
+
args.push("--log=verbose");
|
|
65
|
+
return {
|
|
66
|
+
command: process.platform === "win32" ? "npx.cmd" : "npx",
|
|
67
|
+
args,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async function runRenderVideo(input) {
|
|
71
|
+
const videoName = (0, util_1.sanitizeSegment)(input.videoName, "videoName");
|
|
72
|
+
const projectRoot = (0, util_1.resolveProjectRoot)(input.projectRoot);
|
|
73
|
+
const compositionId = input.compositionId ?? `${(0, util_1.pascalCase)(videoName)}Demo`;
|
|
74
|
+
if (!COMPOSITION_ID_RE.test(compositionId)) {
|
|
75
|
+
throw new Error(`compositionId "${compositionId}" is not safe (letters, numbers, "-", "_" only).`);
|
|
76
|
+
}
|
|
77
|
+
const outPathRel = input.outPath ?? path.join("out", `${videoName}.mp4`);
|
|
78
|
+
(0, util_1.sanitizeRelativeOutPath)(projectRoot, outPathRel, "outPath");
|
|
79
|
+
fs.mkdirSync(path.dirname(path.join(projectRoot, outPathRel)), { recursive: true });
|
|
80
|
+
const started = Date.now();
|
|
81
|
+
const { command, args } = buildRenderCommand(compositionId, outPathRel, {
|
|
82
|
+
draft: input.draft,
|
|
83
|
+
concurrency: input.concurrency,
|
|
84
|
+
});
|
|
85
|
+
const result = await (0, util_1.spawnCapture)(command, args, projectRoot);
|
|
86
|
+
const elapsedSeconds = Math.round((Date.now() - started) / 1000);
|
|
87
|
+
return {
|
|
88
|
+
success: result.code === 0,
|
|
89
|
+
outPath: path.join(projectRoot, outPathRel),
|
|
90
|
+
draft: Boolean(input.draft),
|
|
91
|
+
elapsedSeconds,
|
|
92
|
+
stdout: result.stdout,
|
|
93
|
+
stderr: result.stderr,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function registerRenderVideo(server) {
|
|
97
|
+
server.registerTool("render_video", {
|
|
98
|
+
title: "Render a composition with the Remotion CLI",
|
|
99
|
+
description: "Thin wrapper around `npx remotion render <compositionId> <outPath>`, run in projectRoot via " +
|
|
100
|
+
"child_process.spawn with an argv array (never exec/execSync with a shell string), so a " +
|
|
101
|
+
"user-controlled videoName/outPath can't inject shell syntax. Default compositionId is the video's " +
|
|
102
|
+
"PascalCase name + \"Demo\" (matching stitch_composition's naming), default outPath is " +
|
|
103
|
+
"out/<videoName>.mp4. Pass draft for a half resolution, fast encode pass: a five minute video is " +
|
|
104
|
+
"9000 frames and tens of minutes at full quality, which is far too slow a loop for checking whether " +
|
|
105
|
+
"the video is right, and draft is usually enough to judge framing, motion and timing. Pass " +
|
|
106
|
+
"concurrency to cap the worker count; left unset Remotion takes as much of the machine as it can, " +
|
|
107
|
+
"and one render here spawned 25 Chrome workers and starved a second pipeline step running " +
|
|
108
|
+
"alongside it. Renders with verbose logging, because Remotion hides its progress bar when stdout " +
|
|
109
|
+
"is not a TTY and a piped render otherwise writes an empty log for its entire run, leaving no way " +
|
|
110
|
+
"to tell progress from a hang. Returns the render's captured stdout/stderr and how long it took.",
|
|
111
|
+
inputSchema: {
|
|
112
|
+
projectRoot: zod_1.z.string().optional(),
|
|
113
|
+
videoName: zod_1.z.string().min(1),
|
|
114
|
+
compositionId: zod_1.z.string().optional(),
|
|
115
|
+
outPath: zod_1.z.string().optional(),
|
|
116
|
+
draft: zod_1.z.boolean().optional(),
|
|
117
|
+
concurrency: zod_1.z.number().positive().optional(),
|
|
118
|
+
},
|
|
119
|
+
}, async (input) => (0, mcp_1.runTool)("render_video", () => runRenderVideo(input)));
|
|
120
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { type SceneKind, type TemplateName } from "../scenes/templates";
|
|
3
|
+
export type { SceneKind };
|
|
4
|
+
export interface ScaffoldSceneInput {
|
|
5
|
+
projectRoot?: string;
|
|
6
|
+
videoName: string;
|
|
7
|
+
beatId: string;
|
|
8
|
+
kind: SceneKind;
|
|
9
|
+
overwrite?: boolean;
|
|
10
|
+
/** Override the automatic template choice. */
|
|
11
|
+
template?: TemplateName;
|
|
12
|
+
}
|
|
13
|
+
export interface ScaffoldSceneResult {
|
|
14
|
+
written: true;
|
|
15
|
+
path: string;
|
|
16
|
+
componentName: string;
|
|
17
|
+
template: TemplateName;
|
|
18
|
+
/** True when the beat was found in beats.json and its content was used. */
|
|
19
|
+
usedBeat: boolean;
|
|
20
|
+
notes: string[];
|
|
21
|
+
}
|
|
22
|
+
export declare function runScaffoldScene(input: ScaffoldSceneInput): ScaffoldSceneResult;
|
|
23
|
+
export declare function registerScaffoldScene(server: McpServer): void;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runScaffoldScene = runScaffoldScene;
|
|
37
|
+
exports.registerScaffoldScene = registerScaffoldScene;
|
|
38
|
+
const fs = __importStar(require("node:fs"));
|
|
39
|
+
const path = __importStar(require("node:path"));
|
|
40
|
+
const zod_1 = require("zod");
|
|
41
|
+
const util_1 = require("../util");
|
|
42
|
+
const mcp_1 = require("./mcp");
|
|
43
|
+
const templates_1 = require("../scenes/templates");
|
|
44
|
+
const pngSize_1 = require("../scenes/pngSize");
|
|
45
|
+
/**
|
|
46
|
+
* Read the beat out of the project's beats.json so the scaffolded scene can carry
|
|
47
|
+
* that beat's own copy and duration. Without this the template has nothing real to
|
|
48
|
+
* put on screen and the first render says nothing.
|
|
49
|
+
*/
|
|
50
|
+
function readBeat(projectRoot, videoName, beatId) {
|
|
51
|
+
const file = path.join(projectRoot, "src", "videos", videoName, "beats.json");
|
|
52
|
+
try {
|
|
53
|
+
const doc = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
54
|
+
return doc.beats?.find((b) => b.id === beatId) ?? null;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function runScaffoldScene(input) {
|
|
61
|
+
const videoName = (0, util_1.sanitizeSegment)(input.videoName, "videoName");
|
|
62
|
+
const beatId = (0, util_1.sanitizeSegment)(input.beatId, "beatId");
|
|
63
|
+
const projectRoot = (0, util_1.resolveProjectRoot)(input.projectRoot);
|
|
64
|
+
const componentName = (0, util_1.pascalCase)(beatId);
|
|
65
|
+
const scenesDir = path.join(projectRoot, "src", "videos", videoName, "scenes");
|
|
66
|
+
const filePath = path.join(scenesDir, `${componentName}.tsx`);
|
|
67
|
+
const notes = [];
|
|
68
|
+
if (fs.existsSync(filePath) && !input.overwrite) {
|
|
69
|
+
throw new Error(`${filePath} already exists. Pass overwrite: true to replace a scene the calling agent or dev has ` +
|
|
70
|
+
`already started editing.`);
|
|
71
|
+
}
|
|
72
|
+
const beat = readBeat(projectRoot, videoName, beatId);
|
|
73
|
+
if (!beat) {
|
|
74
|
+
notes.push(`No beat "${beatId}" found in beats.json, so the scene was scaffolded with placeholder copy. ` +
|
|
75
|
+
`Run write_beats_file first to get a scene that carries this beat's own content.`);
|
|
76
|
+
}
|
|
77
|
+
const description = beat?.visual?.description ?? "";
|
|
78
|
+
const ctx = {
|
|
79
|
+
beatId,
|
|
80
|
+
componentName,
|
|
81
|
+
description,
|
|
82
|
+
vo: beat?.vo ?? "",
|
|
83
|
+
durationFrames: beat?.duration ?? 300,
|
|
84
|
+
url: beat?.visual?.url,
|
|
85
|
+
};
|
|
86
|
+
if (input.kind === "real-screenshot") {
|
|
87
|
+
const png = path.join(projectRoot, "public", "images", `${beatId}.png`);
|
|
88
|
+
const size = (0, pngSize_1.pngSize)(png);
|
|
89
|
+
if (size) {
|
|
90
|
+
ctx.captureWidth = size.width;
|
|
91
|
+
ctx.captureHeight = size.height;
|
|
92
|
+
notes.push(`Frame sized from the real capture: ${size.width}x${size.height}.`);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
notes.push(`No capture found at public/images/${beatId}.png, so the frame uses a 1440x900 default. ` +
|
|
96
|
+
`Re-run scaffold_scene after capture_screenshot to size the frame from the real file.`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const template = input.template ?? (0, templates_1.pickTemplate)(input.kind, description);
|
|
100
|
+
const source = (0, templates_1.renderTemplate)(template, ctx, input.kind);
|
|
101
|
+
fs.mkdirSync(scenesDir, { recursive: true });
|
|
102
|
+
fs.writeFileSync(filePath, source, "utf8");
|
|
103
|
+
return {
|
|
104
|
+
written: true,
|
|
105
|
+
path: filePath,
|
|
106
|
+
componentName,
|
|
107
|
+
template,
|
|
108
|
+
usedBeat: Boolean(beat),
|
|
109
|
+
notes,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function registerScaffoldScene(server) {
|
|
113
|
+
server.registerTool("scaffold_scene", {
|
|
114
|
+
title: "Scaffold a working scene for one beat",
|
|
115
|
+
description: "Writes src/videos/<videoName>/scenes/<BeatId>.tsx as a scene that renders immediately, with no " +
|
|
116
|
+
"TODOs to fill in. It reads the beat out of beats.json and uses that beat's own description, " +
|
|
117
|
+
"narration and duration for the on-screen copy and timings, so the first render already says " +
|
|
118
|
+
"something. Pick the shape with `template`, or let it choose from the capture method and what the " +
|
|
119
|
+
"beat's description talks about. Templates: browser-capture and recording (for real captures), " +
|
|
120
|
+
"terminal, split-panel, checklist, stat, code, comparison, cta, title. For real-screenshot beats " +
|
|
121
|
+
"the frame is sized from the actual PNG on disk rather than guessed, so the page is not letterboxed. " +
|
|
122
|
+
"Constructed scenes use a gentle camera push and a safe content width, because the visible area is " +
|
|
123
|
+
"the stage divided by the camera scale and content built at full stage size gets cropped at the " +
|
|
124
|
+
"frame edge. Refuses to overwrite an existing scene unless overwrite: true.",
|
|
125
|
+
inputSchema: {
|
|
126
|
+
projectRoot: zod_1.z.string().optional(),
|
|
127
|
+
videoName: zod_1.z.string().min(1),
|
|
128
|
+
beatId: zod_1.z.string().min(1),
|
|
129
|
+
kind: zod_1.z.enum(["real-screenshot", "real-recording", "dom-demo", "higgsfield-clip"]),
|
|
130
|
+
overwrite: zod_1.z.boolean().optional(),
|
|
131
|
+
template: zod_1.z
|
|
132
|
+
.enum([
|
|
133
|
+
"browser-capture",
|
|
134
|
+
"recording",
|
|
135
|
+
"terminal",
|
|
136
|
+
"split-panel",
|
|
137
|
+
"checklist",
|
|
138
|
+
"stat",
|
|
139
|
+
"code",
|
|
140
|
+
"comparison",
|
|
141
|
+
"cta",
|
|
142
|
+
"title",
|
|
143
|
+
])
|
|
144
|
+
.optional(),
|
|
145
|
+
},
|
|
146
|
+
}, async (input) => (0, mcp_1.runTool)("scaffold_scene", () => runScaffoldScene(input)));
|
|
147
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
export interface StitchCompositionInput {
|
|
3
|
+
projectRoot?: string;
|
|
4
|
+
videoName: string;
|
|
5
|
+
}
|
|
6
|
+
export interface StitchCompositionResult {
|
|
7
|
+
written: true;
|
|
8
|
+
compositionId: string;
|
|
9
|
+
demoPath: string;
|
|
10
|
+
rootPath: string;
|
|
11
|
+
durationInFrames: number;
|
|
12
|
+
voBeatsFound: string[];
|
|
13
|
+
musicBedFound: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Reads src/videos/<videoName>/beats.json and writes the composition + updates
|
|
17
|
+
* src/Root.tsx (via the registry above). VO convention: public/audio/vo/<beatId>.mp3,
|
|
18
|
+
* included only if the file actually exists on disk (mirrors PepirosDemo.tsx, where
|
|
19
|
+
* the cta beat has no VO entry because none was recorded). Music bed convention:
|
|
20
|
+
* public/audio/music-bed.mp3, included as a full-duration looping layer only if it
|
|
21
|
+
* exists.
|
|
22
|
+
*/
|
|
23
|
+
export declare function runStitchComposition(input: StitchCompositionInput): StitchCompositionResult;
|
|
24
|
+
export declare function registerStitchComposition(server: McpServer): void;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runStitchComposition = runStitchComposition;
|
|
37
|
+
exports.registerStitchComposition = registerStitchComposition;
|
|
38
|
+
const fs = __importStar(require("node:fs"));
|
|
39
|
+
const path = __importStar(require("node:path"));
|
|
40
|
+
const zod_1 = require("zod");
|
|
41
|
+
const config_1 = require("../config");
|
|
42
|
+
const util_1 = require("../util");
|
|
43
|
+
const mcp_1 = require("./mcp");
|
|
44
|
+
/**
|
|
45
|
+
* Registry of every video this project has stitched, one entry per stitch_composition
|
|
46
|
+
* call, at src/videos/.registry.json. Root.tsx is fully regenerated from this file on
|
|
47
|
+
* every call rather than round-tripped via regex/JSX parsing, which is what makes it
|
|
48
|
+
* safe to never clobber a composition a prior call registered for a different video:
|
|
49
|
+
* the registry (not Root.tsx itself) is the actual source of truth. Deliberately not
|
|
50
|
+
* part of openvidstudio.config.json: that shape is pinned for Task 5 to depend on,
|
|
51
|
+
* this is internal bookkeeping local to this one tool.
|
|
52
|
+
*/
|
|
53
|
+
function registryPath(projectRoot) {
|
|
54
|
+
return path.join(projectRoot, "src", "videos", ".registry.json");
|
|
55
|
+
}
|
|
56
|
+
function readRegistry(projectRoot) {
|
|
57
|
+
const p = registryPath(projectRoot);
|
|
58
|
+
if (!fs.existsSync(p))
|
|
59
|
+
return [];
|
|
60
|
+
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
61
|
+
}
|
|
62
|
+
function writeRegistry(projectRoot, entries) {
|
|
63
|
+
const p = registryPath(projectRoot);
|
|
64
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
65
|
+
fs.writeFileSync(p, JSON.stringify(entries, null, 2) + "\n", "utf8");
|
|
66
|
+
}
|
|
67
|
+
function renderRootTsx(entries) {
|
|
68
|
+
const imports = entries.map((e) => `import { ${e.componentName} } from "${e.importPath}";`).join("\n");
|
|
69
|
+
const comps = entries
|
|
70
|
+
.map((e) => ` <Composition\n id="${e.id}"\n component={${e.componentName}}\n` +
|
|
71
|
+
` durationInFrames={${e.durationInFrames}}\n fps={${e.fps}}\n` +
|
|
72
|
+
` width={${e.width}}\n height={${e.height}}\n />`)
|
|
73
|
+
.join("\n");
|
|
74
|
+
return `// AUTO-GENERATED by openvidstudio's stitch_composition MCP tool.
|
|
75
|
+
// Source of truth is src/videos/.registry.json -- hand edits to this file are lost the
|
|
76
|
+
// next time stitch_composition runs for ANY video in this project, not just the one you
|
|
77
|
+
// edited. Add a video by calling stitch_composition, not by editing this directly.
|
|
78
|
+
|
|
79
|
+
import React from "react";
|
|
80
|
+
import { Composition } from "remotion";
|
|
81
|
+
${imports}
|
|
82
|
+
|
|
83
|
+
export const RemotionRoot: React.FC = () => {
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
${comps}
|
|
87
|
+
</>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
`;
|
|
91
|
+
}
|
|
92
|
+
function renderDemoTsx(opts) {
|
|
93
|
+
const { componentName, beats, voMap, musicBedFound } = opts;
|
|
94
|
+
const sceneImports = beats
|
|
95
|
+
.map((b) => `import { ${(0, util_1.pascalCase)(b.id)} } from "./scenes/${(0, util_1.pascalCase)(b.id)}";`)
|
|
96
|
+
.join("\n");
|
|
97
|
+
const sceneMapLines = beats.map((b) => ` ${JSON.stringify(b.id)}: ${(0, util_1.pascalCase)(b.id)},`).join("\n");
|
|
98
|
+
const voLines = Object.entries(voMap)
|
|
99
|
+
.map(([id, rel]) => ` ${JSON.stringify(id)}: ${JSON.stringify(rel)},`)
|
|
100
|
+
.join("\n");
|
|
101
|
+
const musicBedBlock = musicBedFound
|
|
102
|
+
? ` <Sequence from={0} durationInFrames={DURATION_IN_FRAMES} layout="none">
|
|
103
|
+
<Audio src={staticFile("audio/music-bed.mp3")} volume={0.18} loop />
|
|
104
|
+
</Sequence>
|
|
105
|
+
`
|
|
106
|
+
: "";
|
|
107
|
+
return `// AUTO-GENERATED by openvidstudio's stitch_composition MCP tool from beats.json.
|
|
108
|
+
// Re-run stitch_composition after editing beats.json rather than hand-editing this file;
|
|
109
|
+
// it will be overwritten.
|
|
110
|
+
|
|
111
|
+
import React from "react";
|
|
112
|
+
import { AbsoluteFill, Audio, Sequence, Series, staticFile } from "remotion";
|
|
113
|
+
import beats from "./beats.json";
|
|
114
|
+
${sceneImports}
|
|
115
|
+
|
|
116
|
+
const SCENES: Record<string, React.FC> = {
|
|
117
|
+
${sceneMapLines}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const VO_FILES: Record<string, string> = {
|
|
121
|
+
${voLines}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const last = beats.beats[beats.beats.length - 1];
|
|
125
|
+
export const DURATION_IN_FRAMES = last.start + last.duration;
|
|
126
|
+
|
|
127
|
+
export const ${componentName}: React.FC = () => {
|
|
128
|
+
return (
|
|
129
|
+
<AbsoluteFill>
|
|
130
|
+
<Series>
|
|
131
|
+
{beats.beats.map((b) => {
|
|
132
|
+
const Scene = SCENES[b.id];
|
|
133
|
+
return (
|
|
134
|
+
<Series.Sequence key={b.id} durationInFrames={b.duration} name={b.id}>
|
|
135
|
+
<Scene />
|
|
136
|
+
</Series.Sequence>
|
|
137
|
+
);
|
|
138
|
+
})}
|
|
139
|
+
</Series>
|
|
140
|
+
{beats.beats
|
|
141
|
+
.filter((b) => VO_FILES[b.id])
|
|
142
|
+
.map((b) => (
|
|
143
|
+
<Sequence key={\`vo-\${b.id}\`} from={b.start} durationInFrames={b.duration} layout="none">
|
|
144
|
+
<Audio src={staticFile(VO_FILES[b.id])} volume={0.9} />
|
|
145
|
+
</Sequence>
|
|
146
|
+
))}
|
|
147
|
+
${musicBedBlock} </AbsoluteFill>
|
|
148
|
+
);
|
|
149
|
+
};
|
|
150
|
+
`;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Reads src/videos/<videoName>/beats.json and writes the composition + updates
|
|
154
|
+
* src/Root.tsx (via the registry above). VO convention: public/audio/vo/<beatId>.mp3,
|
|
155
|
+
* included only if the file actually exists on disk (mirrors PepirosDemo.tsx, where
|
|
156
|
+
* the cta beat has no VO entry because none was recorded). Music bed convention:
|
|
157
|
+
* public/audio/music-bed.mp3, included as a full-duration looping layer only if it
|
|
158
|
+
* exists.
|
|
159
|
+
*/
|
|
160
|
+
function runStitchComposition(input) {
|
|
161
|
+
const videoName = (0, util_1.sanitizeSegment)(input.videoName, "videoName");
|
|
162
|
+
const projectRoot = (0, util_1.resolveProjectRoot)(input.projectRoot);
|
|
163
|
+
const videoDir = path.join(projectRoot, "src", "videos", videoName);
|
|
164
|
+
const beatsPath = path.join(videoDir, "beats.json");
|
|
165
|
+
if (!fs.existsSync(beatsPath)) {
|
|
166
|
+
throw new Error(`${beatsPath} does not exist. Call write_beats_file for "${videoName}" first.`);
|
|
167
|
+
}
|
|
168
|
+
const beatsData = JSON.parse(fs.readFileSync(beatsPath, "utf8"));
|
|
169
|
+
const beats = beatsData.beats;
|
|
170
|
+
if (!Array.isArray(beats) || beats.length === 0) {
|
|
171
|
+
throw new Error(`${beatsPath} has no beats.`);
|
|
172
|
+
}
|
|
173
|
+
const config = (0, config_1.loadConfig)(projectRoot);
|
|
174
|
+
const fps = beatsData.fps ?? config.videoConfig.fps;
|
|
175
|
+
const last = beats[beats.length - 1];
|
|
176
|
+
const durationInFrames = last.start + last.duration;
|
|
177
|
+
const voMap = {};
|
|
178
|
+
for (const b of beats) {
|
|
179
|
+
const voFile = path.join(projectRoot, "public", "audio", "vo", `${b.id}.mp3`);
|
|
180
|
+
if (fs.existsSync(voFile)) {
|
|
181
|
+
voMap[b.id] = `audio/vo/${b.id}.mp3`;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const musicBedFound = fs.existsSync(path.join(projectRoot, "public", "audio", "music-bed.mp3"));
|
|
185
|
+
const componentName = `${(0, util_1.pascalCase)(videoName)}Demo`;
|
|
186
|
+
const demoTsx = renderDemoTsx({ componentName, beats, voMap, musicBedFound });
|
|
187
|
+
const demoPath = path.join(videoDir, `${componentName}.tsx`);
|
|
188
|
+
fs.writeFileSync(demoPath, demoTsx, "utf8");
|
|
189
|
+
const entries = readRegistry(projectRoot).filter((e) => e.videoName !== videoName);
|
|
190
|
+
entries.push({
|
|
191
|
+
id: componentName,
|
|
192
|
+
videoName,
|
|
193
|
+
componentName,
|
|
194
|
+
importPath: `./videos/${videoName}/${componentName}`,
|
|
195
|
+
durationInFrames,
|
|
196
|
+
fps,
|
|
197
|
+
width: config.videoConfig.width,
|
|
198
|
+
height: config.videoConfig.height,
|
|
199
|
+
});
|
|
200
|
+
writeRegistry(projectRoot, entries);
|
|
201
|
+
const rootPath = path.join(projectRoot, "src", "Root.tsx");
|
|
202
|
+
fs.writeFileSync(rootPath, renderRootTsx(entries), "utf8");
|
|
203
|
+
return {
|
|
204
|
+
written: true,
|
|
205
|
+
compositionId: componentName,
|
|
206
|
+
demoPath,
|
|
207
|
+
rootPath,
|
|
208
|
+
durationInFrames,
|
|
209
|
+
voBeatsFound: Object.keys(voMap),
|
|
210
|
+
musicBedFound,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function registerStitchComposition(server) {
|
|
214
|
+
server.registerTool("stitch_composition", {
|
|
215
|
+
title: "Stitch a video's beats into a composition",
|
|
216
|
+
description: "Reads src/videos/<videoName>/beats.json and writes src/videos/<videoName>/<PascalName>Demo.tsx: a " +
|
|
217
|
+
"SCENES map from beat id to its scaffold_scene component (imported from ./scenes/<BeatId>, the same " +
|
|
218
|
+
"PascalCase convention scaffold_scene uses -- beats without a scaffolded scene file will fail to " +
|
|
219
|
+
"type-check, scaffold every beat first), a <Series> of <Series.Sequence> per beat, a VO Audio layer " +
|
|
220
|
+
"per beat only where public/audio/vo/<beatId>.mp3 actually exists on disk, and a looping " +
|
|
221
|
+
"full-duration music bed layer if public/audio/music-bed.mp3 exists. Also (re)registers the " +
|
|
222
|
+
"composition in src/Root.tsx (via an internal src/videos/.registry.json this tool owns), without " +
|
|
223
|
+
"clobbering compositions a prior stitch_composition call registered for a different video in the " +
|
|
224
|
+
"same project.",
|
|
225
|
+
inputSchema: {
|
|
226
|
+
projectRoot: zod_1.z.string().optional(),
|
|
227
|
+
videoName: zod_1.z.string().min(1),
|
|
228
|
+
},
|
|
229
|
+
}, async (input) => (0, mcp_1.runTool)("stitch_composition", () => runStitchComposition(input)));
|
|
230
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
export interface ValidateBeatsResult {
|
|
3
|
+
valid: boolean;
|
|
4
|
+
errors: string[];
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Mechanically enforces packages/docs/PLANNING.md §4 (captureMethod field spec) and
|
|
8
|
+
* packages/docs/SCRIPT.md (VO pacing, no em dashes). Takes the drafted beats.json
|
|
9
|
+
* content directly (not yet written to disk, not assumed to already be valid), and
|
|
10
|
+
* returns every failure found rather than stopping at the first -- this is the tool's
|
|
11
|
+
* normal "no" answer, never a thrown exception.
|
|
12
|
+
*/
|
|
13
|
+
export declare function validateBeatsLogic(beatsJson: unknown): ValidateBeatsResult;
|
|
14
|
+
export declare function registerValidateBeats(server: McpServer): void;
|