@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,185 @@
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.runGenerateNarration = runGenerateNarration;
37
+ exports.registerGenerateNarration = registerGenerateNarration;
38
+ const node_child_process_1 = require("node:child_process");
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
+ /** Beyond this, a pitch-preserving stretch starts to sound processed. */
45
+ const TEMPO_FLOOR = 0.9;
46
+ const TEMPO_CEIL = 1.12;
47
+ const DEFAULT_VOICE = {
48
+ "edge-tts": "en-US-AndrewNeural",
49
+ say: "Alex",
50
+ espeak: "en-us",
51
+ };
52
+ function probeDuration(file) {
53
+ const r = (0, node_child_process_1.spawnSync)("ffprobe", ["-v", "error", "-show_entries", "format=duration", "-of", "default=nw=1:nk=1", file], { encoding: "utf8" });
54
+ const v = Number.parseFloat((r.stdout ?? "").trim());
55
+ return Number.isFinite(v) ? v : 0;
56
+ }
57
+ function synthesize(engine, voice, text, out) {
58
+ for (let attempt = 0; attempt < 3; attempt++) {
59
+ fs.rmSync(out, { force: true });
60
+ let r;
61
+ if (engine === "edge-tts") {
62
+ r = (0, node_child_process_1.spawnSync)("edge-tts", ["--voice", voice, "--text", text, "--write-media", out], {
63
+ encoding: "utf8",
64
+ });
65
+ if (r.error) {
66
+ // Not on PATH as a binary in every install; the module entrypoint always is.
67
+ r = (0, node_child_process_1.spawnSync)("python", ["-m", "edge_tts", "--voice", voice, "--text", text, "--write-media", out], { encoding: "utf8" });
68
+ }
69
+ }
70
+ else if (engine === "say") {
71
+ r = (0, node_child_process_1.spawnSync)("say", ["-v", voice, "-o", out, "--data-format=LEF32@22050", text], {
72
+ encoding: "utf8",
73
+ });
74
+ }
75
+ else {
76
+ r = (0, node_child_process_1.spawnSync)("espeak", ["-v", voice, "-w", out, text], { encoding: "utf8" });
77
+ }
78
+ // A size check alone is not enough: a truncated download can clear it and still
79
+ // be undecodable, so this validates by actually probing the file.
80
+ if (!r.error && fs.existsSync(out) && probeDuration(out) > 0.3)
81
+ return { ok: true };
82
+ }
83
+ return { ok: false, error: `no usable audio after 3 attempts` };
84
+ }
85
+ function runGenerateNarration(input) {
86
+ const videoName = (0, util_1.sanitizeSegment)(input.videoName, "videoName");
87
+ const projectRoot = (0, util_1.resolveProjectRoot)(input.projectRoot);
88
+ const engine = input.engine ?? "edge-tts";
89
+ const voice = input.voice ?? DEFAULT_VOICE[engine];
90
+ const pad = input.padSeconds ?? 0.45;
91
+ const beatsFile = path.join(projectRoot, "src", "videos", videoName, "beats.json");
92
+ if (!fs.existsSync(beatsFile)) {
93
+ throw new Error(`No beats.json at ${beatsFile}. Run write_beats_file first.`);
94
+ }
95
+ const doc = JSON.parse(fs.readFileSync(beatsFile, "utf8"));
96
+ const fps = doc.fps || 30;
97
+ const outDir = path.join(projectRoot, "public", "audio", "vo");
98
+ fs.mkdirSync(outDir, { recursive: true });
99
+ const workDir = path.join(projectRoot, ".openvidstudio", "narration");
100
+ fs.mkdirSync(workDir, { recursive: true });
101
+ const written = [];
102
+ const skipped = [];
103
+ const missing = [];
104
+ const warnings = [];
105
+ for (const beat of doc.beats) {
106
+ if (input.beatIds && !input.beatIds.includes(beat.id))
107
+ continue;
108
+ const finalPath = path.join(outDir, `${beat.id}.mp3`);
109
+ if (!beat.vo || !beat.vo.trim()) {
110
+ missing.push(beat.id);
111
+ continue;
112
+ }
113
+ if (fs.existsSync(finalPath) && !input.overwrite) {
114
+ skipped.push(beat.id);
115
+ continue;
116
+ }
117
+ const raw = path.join(workDir, `${beat.id}.raw`);
118
+ const res = synthesize(engine, voice, beat.vo, raw);
119
+ if (!res.ok) {
120
+ missing.push(beat.id);
121
+ warnings.push(`${beat.id}: ${res.error}`);
122
+ continue;
123
+ }
124
+ const spoken = probeDuration(raw);
125
+ const beatSeconds = beat.duration / fps;
126
+ const target = beatSeconds - pad;
127
+ const wanted = spoken / target;
128
+ const tempo = Math.min(TEMPO_CEIL, Math.max(TEMPO_FLOOR, wanted));
129
+ const r = (0, node_child_process_1.spawnSync)("ffmpeg", ["-v", "error", "-i", raw, "-filter:a", `atempo=${tempo.toFixed(4)}`,
130
+ "-c:a", "libmp3lame", "-q:a", "3", finalPath, "-y"], { encoding: "utf8" });
131
+ if (r.status !== 0) {
132
+ missing.push(beat.id);
133
+ warnings.push(`${beat.id}: ffmpeg failed writing the fitted clip`);
134
+ continue;
135
+ }
136
+ const finalSeconds = probeDuration(finalPath);
137
+ const overruns = finalSeconds > beatSeconds;
138
+ const clip = {
139
+ beatId: beat.id,
140
+ path: finalPath,
141
+ beatSeconds: Number(beatSeconds.toFixed(2)),
142
+ spokenSeconds: Number(spoken.toFixed(2)),
143
+ finalSeconds: Number(finalSeconds.toFixed(2)),
144
+ tempo: Number(tempo.toFixed(3)),
145
+ needsAttention: overruns || Math.abs(wanted - tempo) > 0.001,
146
+ };
147
+ if (overruns) {
148
+ clip.note =
149
+ `Runs ${(finalSeconds - beatSeconds).toFixed(2)}s past the beat. Shorten the line or ` +
150
+ `lengthen the beat; stretching further would sound processed.`;
151
+ warnings.push(`${beat.id}: narration overruns its beat by ${(finalSeconds - beatSeconds).toFixed(2)}s`);
152
+ }
153
+ else if (clip.needsAttention) {
154
+ const slack = (target - finalSeconds).toFixed(2);
155
+ clip.note =
156
+ `Fits with ${slack}s of silence left over. Stretching was clamped to keep the voice ` +
157
+ `natural; add a few words to the line if the gap is noticeable.`;
158
+ }
159
+ written.push(clip);
160
+ }
161
+ return { engine, voice, written, skipped, missing, warnings };
162
+ }
163
+ function registerGenerateNarration(server) {
164
+ server.registerTool("generate_narration", {
165
+ title: "Generate narration audio for every beat",
166
+ description: "Reads beats.json and writes one narration mp3 per beat to public/audio/vo/<beatId>.mp3, which is " +
167
+ "exactly where stitch_composition looks. Defaults to edge-tts; 'say' on macOS and 'espeak' also " +
168
+ "work. Each clip is fitted to its beat, but stretching is clamped to a range that stays natural: a " +
169
+ "pitch-preserving stretch past about ten percent is audible and is the main reason generated " +
170
+ "voiceover sounds synthetic. When a line genuinely does not fit, the tool says so and names the beat " +
171
+ "rather than stretching it into something processed, because the real fix is the script or the beat " +
172
+ "duration. Reports which beats got audio, which were skipped as already present, and which are still " +
173
+ "missing, so a partially generated narration cannot slip through and render silent. Resumable: pass " +
174
+ "overwrite to redo clips that already exist.",
175
+ inputSchema: {
176
+ projectRoot: zod_1.z.string().optional(),
177
+ videoName: zod_1.z.string().min(1),
178
+ engine: zod_1.z.enum(["edge-tts", "say", "espeak"]).optional(),
179
+ voice: zod_1.z.string().optional(),
180
+ padSeconds: zod_1.z.number().positive().optional(),
181
+ overwrite: zod_1.z.boolean().optional(),
182
+ beatIds: zod_1.z.array(zod_1.z.string()).optional(),
183
+ },
184
+ }, async (input) => (0, mcp_1.runTool)("generate_narration", () => runGenerateNarration(input)));
185
+ }
@@ -0,0 +1,41 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ /**
3
+ * `import_higgsfield_clip` does NOT call Higgsfield itself. See HIGGSFIELD.md and
4
+ * task-5-report.md for why: an MCP server has no clean, spec-supported way to reach into
5
+ * a *different*, already-authorized MCP connection the calling agent holds (Higgsfield's,
6
+ * in this case) -- there is no server-to-server tool-call primitive in the protocol, only
7
+ * sampling/elicitation/roots as server-to-client requests. The calling agent already has
8
+ * its own Higgsfield MCP connection and calls generate_video/jobs_wait on it directly;
9
+ * this tool's only job is taking whatever result that produced (a URL or a local file
10
+ * path) and landing it at this project's own asset-path convention.
11
+ */
12
+ export type HiggsfieldClipSource = {
13
+ type: "url";
14
+ url: string;
15
+ } | {
16
+ type: "path";
17
+ path: string;
18
+ };
19
+ export interface ImportHiggsfieldClipInput {
20
+ projectRoot?: string;
21
+ beatId: string;
22
+ source: HiggsfieldClipSource;
23
+ outPath?: string;
24
+ }
25
+ export interface ImportHiggsfieldClipResult {
26
+ outPath: string;
27
+ beatId: string;
28
+ }
29
+ /** Injectable so tests can supply a fake fetch instead of hitting a real network. */
30
+ export interface ImportHiggsfieldClipDeps {
31
+ fetchImpl?: typeof fetch;
32
+ }
33
+ /**
34
+ * Fetches or copies `input.source` into public/video/<beatId>.mp4 under projectRoot --
35
+ * the same convention capture_screen_recording and scaffold_scene's higgsfield-clip kind
36
+ * use, so a scaffolded higgsfield-clip scene's <OffthreadVideo src=...> finds the asset
37
+ * regardless of which tool produced it. Never reimplements video generation: the bytes
38
+ * are trusted as-is from whichever source the calling agent already produced.
39
+ */
40
+ export declare function runImportHiggsfieldClip(input: ImportHiggsfieldClipInput, deps?: ImportHiggsfieldClipDeps): Promise<ImportHiggsfieldClipResult>;
41
+ export declare function registerImportHiggsfieldClip(server: McpServer): void;
@@ -0,0 +1,108 @@
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.runImportHiggsfieldClip = runImportHiggsfieldClip;
37
+ exports.registerImportHiggsfieldClip = registerImportHiggsfieldClip;
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
+ async function fetchToBuffer(url, fetchImpl) {
44
+ let response;
45
+ try {
46
+ response = await fetchImpl(url);
47
+ }
48
+ catch (err) {
49
+ throw new Error(`Failed to fetch source URL "${url}": ${err instanceof Error ? err.message : String(err)}`);
50
+ }
51
+ if (!response.ok) {
52
+ throw new Error(`Source URL "${url}" returned ${response.status} ${response.statusText}`);
53
+ }
54
+ const arrayBuffer = await response.arrayBuffer();
55
+ return Buffer.from(arrayBuffer);
56
+ }
57
+ function readLocalFile(sourcePath) {
58
+ if (!fs.existsSync(sourcePath)) {
59
+ throw new Error(`Source path "${sourcePath}" does not exist.`);
60
+ }
61
+ if (!fs.statSync(sourcePath).isFile()) {
62
+ throw new Error(`Source path "${sourcePath}" is not a file.`);
63
+ }
64
+ return fs.readFileSync(sourcePath);
65
+ }
66
+ /**
67
+ * Fetches or copies `input.source` into public/video/<beatId>.mp4 under projectRoot --
68
+ * the same convention capture_screen_recording and scaffold_scene's higgsfield-clip kind
69
+ * use, so a scaffolded higgsfield-clip scene's <OffthreadVideo src=...> finds the asset
70
+ * regardless of which tool produced it. Never reimplements video generation: the bytes
71
+ * are trusted as-is from whichever source the calling agent already produced.
72
+ */
73
+ async function runImportHiggsfieldClip(input, deps = {}) {
74
+ const beatId = (0, util_1.sanitizeSegment)(input.beatId, "beatId");
75
+ const projectRoot = (0, util_1.resolveProjectRoot)(input.projectRoot);
76
+ const outPathRel = input.outPath ?? path.join("public", "video", `${beatId}.mp4`);
77
+ (0, util_1.sanitizeRelativeOutPath)(projectRoot, outPathRel, "outPath");
78
+ const outPathAbs = path.join(projectRoot, outPathRel);
79
+ const buffer = input.source.type === "url"
80
+ ? await fetchToBuffer(input.source.url, deps.fetchImpl ?? fetch)
81
+ : readLocalFile(input.source.path);
82
+ fs.mkdirSync(path.dirname(outPathAbs), { recursive: true });
83
+ fs.writeFileSync(outPathAbs, buffer);
84
+ return { outPath: outPathAbs, beatId };
85
+ }
86
+ const sourceSchema = zod_1.z.union([
87
+ zod_1.z.object({ type: zod_1.z.literal("url"), url: zod_1.z.string().min(1) }),
88
+ zod_1.z.object({ type: zod_1.z.literal("path"), path: zod_1.z.string().min(1) }),
89
+ ]);
90
+ function registerImportHiggsfieldClip(server) {
91
+ server.registerTool("import_higgsfield_clip", {
92
+ title: "Import an already-generated Higgsfield clip into this project's asset convention",
93
+ description: "Only registered when openvidstudio.config.json has hasHiggsfield: true. Does NOT call Higgsfield's " +
94
+ "generate_video/jobs_wait tools itself -- MCP servers have no server-to-server tool-call mechanism, " +
95
+ "so the calling agent must call those on its own already-authorized Higgsfield MCP connection first, " +
96
+ "then hand this tool the result. Accepts either a downloadable URL or a local file path, fetches/reads " +
97
+ "it, and writes the bytes to public/video/<beatId>.mp4 under projectRoot (default outPath override " +
98
+ "available for non-default layouts), matching capture_screen_recording's and scaffold_scene's " +
99
+ "higgsfield-clip convention exactly. Returns { outPath, beatId } on success; a structured error " +
100
+ "(never an uncaught throw) on a bad URL, a missing local file, or a failed download.",
101
+ inputSchema: {
102
+ projectRoot: zod_1.z.string().optional(),
103
+ beatId: zod_1.z.string().min(1),
104
+ source: sourceSchema,
105
+ outPath: zod_1.z.string().optional(),
106
+ },
107
+ }, async (input) => (0, mcp_1.runTool)("import_higgsfield_clip", () => runImportHiggsfieldClip(input)));
108
+ }
@@ -0,0 +1,13 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { OpenvidstudioConfig } from "../config";
3
+ /**
4
+ * Registers all of this package's tools. All but one need no server-startup config: each
5
+ * resolves a project's own openvidstudio.config.json itself, per call, off projectRoot.
6
+ * import_higgsfield_clip is the one exception -- its registration itself is gated on
7
+ * config.hasHiggsfield, decided once at server-startup time (stdio.ts reads
8
+ * openvidstudio.config.json from process.cwd() before calling createMcpServer). Config is
9
+ * a static file read at startup here, not a live-toggleable runtime setting, so simply not
10
+ * calling registerImportHiggsfieldClip when the gate fails is sufficient: the tool is
11
+ * absent from the tool list entirely, not present-but-erroring.
12
+ */
13
+ export declare function registerTools(server: McpServer, config?: OpenvidstudioConfig): void;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerTools = registerTools;
4
+ const initProject_1 = require("./initProject");
5
+ const validateBeats_1 = require("./validateBeats");
6
+ const writeBeatsFile_1 = require("./writeBeatsFile");
7
+ const scaffoldScene_1 = require("./scaffoldScene");
8
+ const stitchComposition_1 = require("./stitchComposition");
9
+ const renderVideo_1 = require("./renderVideo");
10
+ const qcExtractFrames_1 = require("./qcExtractFrames");
11
+ const captureScreenshot_1 = require("./captureScreenshot");
12
+ const captureScreenRecording_1 = require("./captureScreenRecording");
13
+ const importHiggsfieldClip_1 = require("./importHiggsfieldClip");
14
+ const generateNarration_1 = require("./generateNarration");
15
+ const preflight_1 = require("./preflight");
16
+ const planBeats_1 = require("./planBeats");
17
+ const validateScenes_1 = require("./validateScenes");
18
+ const contactSheet_1 = require("./contactSheet");
19
+ /**
20
+ * Registers all of this package's tools. All but one need no server-startup config: each
21
+ * resolves a project's own openvidstudio.config.json itself, per call, off projectRoot.
22
+ * import_higgsfield_clip is the one exception -- its registration itself is gated on
23
+ * config.hasHiggsfield, decided once at server-startup time (stdio.ts reads
24
+ * openvidstudio.config.json from process.cwd() before calling createMcpServer). Config is
25
+ * a static file read at startup here, not a live-toggleable runtime setting, so simply not
26
+ * calling registerImportHiggsfieldClip when the gate fails is sufficient: the tool is
27
+ * absent from the tool list entirely, not present-but-erroring.
28
+ */
29
+ function registerTools(server, config) {
30
+ (0, initProject_1.registerInitProject)(server);
31
+ (0, validateBeats_1.registerValidateBeats)(server);
32
+ (0, writeBeatsFile_1.registerWriteBeatsFile)(server);
33
+ (0, scaffoldScene_1.registerScaffoldScene)(server);
34
+ (0, stitchComposition_1.registerStitchComposition)(server);
35
+ (0, renderVideo_1.registerRenderVideo)(server);
36
+ (0, qcExtractFrames_1.registerQcExtractFrames)(server);
37
+ (0, captureScreenshot_1.registerCaptureScreenshot)(server);
38
+ (0, captureScreenRecording_1.registerCaptureScreenRecording)(server);
39
+ (0, generateNarration_1.registerGenerateNarration)(server);
40
+ (0, preflight_1.registerPreflight)(server);
41
+ (0, planBeats_1.registerPlanBeats)(server);
42
+ (0, validateScenes_1.registerValidateScenes)(server);
43
+ (0, contactSheet_1.registerContactSheet)(server);
44
+ if (config?.hasHiggsfield === true) {
45
+ (0, importHiggsfieldClip_1.registerImportHiggsfieldClip)(server);
46
+ }
47
+ }
@@ -0,0 +1,37 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { type OpenvidstudioConfig, type VideoConfig } from "../config";
3
+ export interface InitProjectInput {
4
+ name: string;
5
+ projectRoot?: string;
6
+ targetDurationSeconds?: number;
7
+ hasHiggsfield?: boolean;
8
+ videoConfig?: Partial<VideoConfig>;
9
+ appUrl?: string;
10
+ demo?: boolean;
11
+ }
12
+ export interface InitProjectResult {
13
+ projectRoot: string;
14
+ videoDir: string;
15
+ configPath: string;
16
+ templateShellCopied: boolean;
17
+ docsDir: string;
18
+ config: OpenvidstudioConfig;
19
+ demo: boolean;
20
+ nextSteps: string[];
21
+ }
22
+ /**
23
+ * Scaffolds `src/videos/<name>/` in `projectRoot`, copying the bundled project shell
24
+ * (package.json, tsconfig, remotion.config.ts, Root.tsx, public/sfx/, the vendored
25
+ * openvidstudio-core/ -- everything in TEMPLATE_DIR) into the project root on first
26
+ * use, copying the bundled pipeline docs (PLANNING.md, PIPELINE.md, STYLE.md,
27
+ * CAPTURE.md, SCRIPT.md, OVERVIEW.md, HIGGSFIELD.md -- everything in DOCS_DIR) into
28
+ * `<projectRoot>/docs/`, then writing/merging openvidstudio.config.json.
29
+ *
30
+ * `TEMPLATE_DIR`/`DOCS_DIR` (src/paths.ts) resolve relative to this package's own
31
+ * compiled dist/ output, i.e. the copies bundled into packages/mcp-server/templates/
32
+ * and packages/mcp-server/docs/ at build time -- never a monorepo-relative path,
33
+ * which would not exist once this package is installed standalone in someone else's
34
+ * project (see task-3-report.md for how that's verified).
35
+ */
36
+ export declare function runInitProject(input: InitProjectInput): InitProjectResult;
37
+ export declare function registerInitProject(server: McpServer): void;
@@ -0,0 +1,166 @@
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.runInitProject = runInitProject;
37
+ exports.registerInitProject = registerInitProject;
38
+ const fs = __importStar(require("node:fs"));
39
+ const path = __importStar(require("node:path"));
40
+ const zod_1 = require("zod");
41
+ const paths_1 = require("../paths");
42
+ const config_1 = require("../config");
43
+ const util_1 = require("../util");
44
+ const mcp_1 = require("./mcp");
45
+ /**
46
+ * Scaffolds `src/videos/<name>/` in `projectRoot`, copying the bundled project shell
47
+ * (package.json, tsconfig, remotion.config.ts, Root.tsx, public/sfx/, the vendored
48
+ * openvidstudio-core/ -- everything in TEMPLATE_DIR) into the project root on first
49
+ * use, copying the bundled pipeline docs (PLANNING.md, PIPELINE.md, STYLE.md,
50
+ * CAPTURE.md, SCRIPT.md, OVERVIEW.md, HIGGSFIELD.md -- everything in DOCS_DIR) into
51
+ * `<projectRoot>/docs/`, then writing/merging openvidstudio.config.json.
52
+ *
53
+ * `TEMPLATE_DIR`/`DOCS_DIR` (src/paths.ts) resolve relative to this package's own
54
+ * compiled dist/ output, i.e. the copies bundled into packages/mcp-server/templates/
55
+ * and packages/mcp-server/docs/ at build time -- never a monorepo-relative path,
56
+ * which would not exist once this package is installed standalone in someone else's
57
+ * project (see task-3-report.md for how that's verified).
58
+ */
59
+ function runInitProject(input) {
60
+ const name = (0, util_1.sanitizeSegment)(input.name, "name");
61
+ const projectRoot = (0, util_1.resolveProjectRoot)(input.projectRoot);
62
+ fs.mkdirSync(projectRoot, { recursive: true });
63
+ // copyTemplateTree never overwrites a file that's already there, so this is safe to
64
+ // call again for a second video in an already-scaffolded project.
65
+ const alreadyScaffolded = fs.existsSync(path.join(projectRoot, "package.json"));
66
+ (0, util_1.copyTemplateTree)(paths_1.TEMPLATE_DIR, projectRoot);
67
+ const docsDir = path.join(projectRoot, "docs");
68
+ (0, util_1.copyTemplateTree)(paths_1.DOCS_DIR, docsDir);
69
+ const videoDir = path.join(projectRoot, "src", "videos", name);
70
+ fs.mkdirSync(path.join(videoDir, "scenes"), { recursive: true });
71
+ const config = (0, config_1.writeConfig)(projectRoot, {
72
+ hasHiggsfield: input.hasHiggsfield,
73
+ targetDurationSeconds: input.targetDurationSeconds,
74
+ videoConfig: input.videoConfig,
75
+ capture: input.appUrl ? { appUrl: input.appUrl } : undefined,
76
+ });
77
+ // A demo project ships a ready beats.json so the very next call can be
78
+ // scaffold_scene. Without it a new user has nothing to run and no worked example
79
+ // to copy, and any failure in ffmpeg or capture surfaces several steps later.
80
+ const nextSteps = [];
81
+ if (input.demo) {
82
+ const fps = config.videoConfig.fps;
83
+ const url = config.capture.appUrl;
84
+ const demoBeats = {
85
+ fps,
86
+ title: `${name} demo`,
87
+ beats: [
88
+ {
89
+ id: "hook",
90
+ start: 0,
91
+ duration: fps * 6,
92
+ visual: {
93
+ captureMethod: "screenshot",
94
+ url,
95
+ interactions: [{ type: "wait", ms: config.capture.settleMs }],
96
+ description: "The app's own first screen, camera pushing in.",
97
+ },
98
+ vo: "This is the app, captured from the real thing rather than described to a model.",
99
+ },
100
+ {
101
+ id: "cta",
102
+ start: fps * 6,
103
+ duration: fps * 5,
104
+ visual: { captureMethod: "dom-demo", description: "Repo call to action." },
105
+ vo: "Built with openvidstudio, which turns a running app into a demo video.",
106
+ },
107
+ ],
108
+ };
109
+ fs.writeFileSync(path.join(videoDir, "beats.json"), JSON.stringify(demoBeats, null, 2) + "\n", "utf8");
110
+ nextSteps.push(`A two beat, 11 second beats.json is ready at src/videos/${name}/beats.json.`, `Next: capture_screenshot for the "hook" beat against ${url},`, `then scaffold_scene for both beats, then stitch_composition, then render_video with draft: true.`, `That whole path should take about two minutes and proves the install works end to end.`);
111
+ }
112
+ return {
113
+ projectRoot,
114
+ videoDir,
115
+ configPath: path.join(projectRoot, "openvidstudio.config.json"),
116
+ templateShellCopied: !alreadyScaffolded,
117
+ docsDir,
118
+ config,
119
+ demo: Boolean(input.demo),
120
+ nextSteps,
121
+ };
122
+ }
123
+ function registerInitProject(server) {
124
+ server.registerTool("init_project", {
125
+ title: "Initialize an openvidstudio project",
126
+ description: "Scaffolds a video's src/videos/<name>/ directory in the calling agent's current project " +
127
+ "(process.cwd() unless projectRoot is given). On first use also copies the bundled openvidstudio " +
128
+ "project shell (package.json, tsconfig.json, remotion.config.ts, src/Root.tsx, src/index.ts, " +
129
+ "public/sfx/, a vendored openvidstudio-core/ that the shell's package.json depends on via a " +
130
+ "relative file: reference) into the project root, copies the bundled pipeline docs (PLANNING.md, " +
131
+ "PIPELINE.md, STYLE.md, CAPTURE.md, SCRIPT.md, OVERVIEW.md, HIGGSFIELD.md) into <projectRoot>/docs/, " +
132
+ "and writes/merges openvidstudio.config.json there (merges field-by-field, never clobbers a value " +
133
+ "the patch doesn't mention). Never overwrites a file that already exists, so calling this again for " +
134
+ "a second video in the same project is safe. Pass demo: true to also write a ready two beat " +
135
+ "beats.json, so the next call can be capture_screenshot and a new user reaches a rendered video in " +
136
+ "about two minutes instead of authoring a beats file first. Only `name` is required; duration, fps, " +
137
+ "dimensions, capture viewport and the settle delay before a screenshot all have defaults that work.",
138
+ inputSchema: {
139
+ name: zod_1.z
140
+ .string()
141
+ .min(1)
142
+ .describe("Video name: letters, numbers, '-', or '_'. Used for src/videos/<name>/."),
143
+ projectRoot: zod_1.z
144
+ .string()
145
+ .optional()
146
+ .describe("Absolute path to the target project root. Defaults to process.cwd()."),
147
+ targetDurationSeconds: zod_1.z.number().positive().optional(),
148
+ hasHiggsfield: zod_1.z.boolean().optional(),
149
+ appUrl: zod_1.z
150
+ .string()
151
+ .optional()
152
+ .describe("The running app to film. Stored as capture.appUrl and used as the default for captures."),
153
+ demo: zod_1.z
154
+ .boolean()
155
+ .optional()
156
+ .describe("Also write a ready two beat beats.json, as a two minute end to end smoke test."),
157
+ videoConfig: zod_1.z
158
+ .object({
159
+ fps: zod_1.z.number().positive().optional(),
160
+ width: zod_1.z.number().positive().optional(),
161
+ height: zod_1.z.number().positive().optional(),
162
+ })
163
+ .optional(),
164
+ },
165
+ }, async (input) => (0, mcp_1.runTool)("init_project", () => runInitProject(input)));
166
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * MCP content + error-wrapping helpers, matching pepiros/mcp/tools/index.ts's pattern:
3
+ * MCP wants `{ content: [...] }`, JSON text content keeps it parseable on the model's side.
4
+ */
5
+ export declare function json(value: unknown): {
6
+ content: {
7
+ type: "text";
8
+ text: string;
9
+ }[];
10
+ };
11
+ export declare function errorText(message: string): {
12
+ content: {
13
+ type: "text";
14
+ text: string;
15
+ }[];
16
+ isError: boolean;
17
+ };
18
+ /**
19
+ * Every tool handler routes through this: run the pure logic function, wrap a
20
+ * successful result as MCP JSON content, and turn any thrown error into MCP
21
+ * error content instead of letting it escape uncaught. A "no, this is invalid"
22
+ * result (validate_beats' `{ valid: false, errors }`, scaffold_scene refusing an
23
+ * existing file) is handled by the logic function returning/throwing normally --
24
+ * this wrapper is what guarantees neither path ever reaches the transport as an
25
+ * unhandled rejection. The diagnostic also goes to stderr: this process's own
26
+ * stdout carries nothing but MCP protocol frames.
27
+ */
28
+ export declare function runTool<T>(toolName: string, fn: () => T | Promise<T>): Promise<{
29
+ content: {
30
+ type: "text";
31
+ text: string;
32
+ }[];
33
+ }>;