@viralspin/mcp 0.1.0 → 0.2.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 (40) hide show
  1. package/BUNDLE_LOCAL_SPEC.md +133 -0
  2. package/dist/client.js +47 -9
  3. package/dist/index.js +83 -1
  4. package/dist/tools/assembly-adjust.js +102 -0
  5. package/dist/tools/carousel-video.js +190 -0
  6. package/dist/tools/engage.js +88 -0
  7. package/dist/tools/explainer-index.js +301 -0
  8. package/dist/tools/image-ads.js +156 -0
  9. package/dist/tools/kinetic-text.js +118 -0
  10. package/dist/tools/photo-picker.js +31 -0
  11. package/dist/tools/publish.js +72 -1
  12. package/dist/tools/review.js +103 -0
  13. package/dist/tools/video-assembly.js +183 -0
  14. package/engine/assemble.mjs +476 -0
  15. package/engine/gemini.mjs +164 -0
  16. package/engine/kinetic.mjs +84 -0
  17. package/engine/prompts.mjs +94 -0
  18. package/engine/remotion.config.ts +4 -0
  19. package/engine/src/Assembly.tsx +330 -0
  20. package/engine/src/KineticText.tsx +253 -0
  21. package/engine/src/Root.tsx +39 -0
  22. package/engine/src/index.ts +4 -0
  23. package/engine/tsconfig.json +15 -0
  24. package/package.json +29 -29
  25. package/src/client.ts +119 -80
  26. package/src/index.ts +151 -60
  27. package/src/tools/assembly-adjust.ts +110 -0
  28. package/src/tools/captions.ts +32 -32
  29. package/src/tools/carousel-video.ts +211 -0
  30. package/src/tools/connections.ts +27 -27
  31. package/src/tools/engage.ts +105 -0
  32. package/src/tools/explainer-index.ts +298 -0
  33. package/src/tools/image-ads.ts +192 -0
  34. package/src/tools/kinetic-text.ts +130 -0
  35. package/src/tools/photo-picker.ts +31 -0
  36. package/src/tools/posts.ts +58 -58
  37. package/src/tools/publish.ts +236 -158
  38. package/src/tools/review.ts +126 -0
  39. package/src/tools/video-assembly.ts +205 -0
  40. package/tsconfig.json +15 -15
@@ -0,0 +1,205 @@
1
+ import { spawn } from "child_process";
2
+ import path from "path";
3
+ import { get, post } from "../client.js";
4
+
5
+ // The assembly engine ships inside this package (mcp/engine). Override with env if needed.
6
+ // __dirname at runtime = mcp/dist/tools → ../../engine = mcp/engine.
7
+ const ENGINE_DIR =
8
+ process.env.VIRALSPIN_ASSEMBLY_DIR || path.resolve(__dirname, "../../engine");
9
+
10
+ const BROLL_STYLE_KEYS = ["sketchbook", "cinematic_minimal", "dark_studio", "vibrant_illustrated"] as const;
11
+ const CAPTION_STYLE_KEYS = ["auto", "default", "hormozi", "glow_neon", "minimalist", "whiteboard"] as const;
12
+
13
+ interface AssemblyArgs {
14
+ aroll: string;
15
+ broll?: string[];
16
+ prompt?: string;
17
+ output_format?: "9x16" | "1x1" | "16x9";
18
+ out?: string;
19
+ google_picker_session?: string;
20
+ style_ref?: string;
21
+ broll_mode?: "user_provided" | "ai_generated";
22
+ style?: typeof BROLL_STYLE_KEYS[number];
23
+ explainer_index_id?: string;
24
+ caption_style?: typeof CAPTION_STYLE_KEYS[number];
25
+ }
26
+
27
+ /** Run the engine orchestrator, streaming its logs to stderr, and capture RESULT:<path>. */
28
+ function runEngine(args: string[], extraEnv: Record<string, string> = {}): Promise<string> {
29
+ return new Promise((resolve, reject) => {
30
+ const child = spawn(process.platform === "win32" ? "node.exe" : "node",
31
+ [path.join(ENGINE_DIR, "assemble.mjs"), ...args],
32
+ { cwd: ENGINE_DIR, env: { ...process.env, ...extraEnv } });
33
+ let stdout = "";
34
+ child.stdout.on("data", (d) => { stdout += d.toString(); });
35
+ child.stderr.on("data", (d) => process.stderr.write(d)); // progress → MCP server log
36
+ child.on("error", reject);
37
+ child.on("close", (code) => {
38
+ const m = stdout.match(/RESULT:(.+)/);
39
+ if (code === 0 && m) resolve(m[1].trim());
40
+ else reject(new Error(`Assembly engine exited ${code}. ${stdout.slice(-400)}`));
41
+ });
42
+ });
43
+ }
44
+
45
+ export const createVideoAssemblyTool = {
46
+ name: "create_video_assembly",
47
+ description:
48
+ "Turn an A-roll talking-head/performance clip into a polished vertical (9:16) short by editing in B-roll cutaways. " +
49
+ "Runs a LOCAL pipeline: transcribes the A-roll, finds each B-roll's focal point (face/subject-aware so nothing is cropped off), " +
50
+ "matches B-roll to what's being said, and renders with Ken Burns motion + the speaker's voice as the soundtrack (B-roll clip audio is mixed in as a low faded bed). " +
51
+ "Accepts LOCAL file paths OR public URLs for both A-roll and B-roll, and a B-roll FOLDER is expanded automatically. " +
52
+ "Pass style_ref (a reference video) to AUTO-INFER the editing style from it — captions, hook headline, keyword pops, pace, and aspect ratio — so you don't have to describe them. " +
53
+ "Returns the path to the finished MP4, which you can then publish with publish_now / schedule_post.",
54
+ inputSchema: {
55
+ type: "object",
56
+ properties: {
57
+ aroll: {
58
+ type: "string",
59
+ description: "A-roll source: a local file path (e.g. C:/clips/talk.mp4) OR a public video URL.",
60
+ },
61
+ broll: {
62
+ type: "array",
63
+ items: { type: "string" },
64
+ description:
65
+ "Optional B-roll sources: local file paths, a local FOLDER (expanded to its images/videos), and/or public URLs. " +
66
+ "Mix freely. Omit for an A-roll-only edit (just motion, no cutaways).",
67
+ },
68
+ prompt: {
69
+ type: "string",
70
+ description: "Optional creative direction for the edit (mood, what to emphasise, pacing).",
71
+ },
72
+ output_format: {
73
+ type: "string",
74
+ enum: ["9x16", "1x1", "16x9"],
75
+ description: "Aspect ratio. Default 9x16 (vertical for Reels/TikTok/Shorts).",
76
+ },
77
+ out: {
78
+ type: "string",
79
+ description: "Optional output MP4 path. Defaults to the engine's out/ folder.",
80
+ },
81
+ google_picker_session: {
82
+ type: "string",
83
+ description:
84
+ "Optional sessionId from start_photo_picker. If set, the photos/clips you picked in Google Photos are ingested as B-roll (in addition to any `broll`).",
85
+ },
86
+ style_ref: {
87
+ type: "string",
88
+ description:
89
+ "Optional reference video (local path or URL). The tool watches it and infers the edit style — captions on/off + style, hook headline, keyword pops, cut pace, and aspect ratio — and applies it automatically. Overrides output_format when set.",
90
+ },
91
+ broll_mode: {
92
+ type: "string",
93
+ enum: ["user_provided", "ai_generated"],
94
+ description:
95
+ "B-roll source mode. 'user_provided' (default) = use local files or Google Photos. 'ai_generated' = AI reads the transcript and generates stylized cinematic B-roll images — no local footage needed. Requires the 'style' param.",
96
+ },
97
+ style: {
98
+ type: "string",
99
+ enum: BROLL_STYLE_KEYS,
100
+ description:
101
+ "Style anchor for AI-generated B-roll (required when broll_mode='ai_generated'). " +
102
+ "sketchbook = hand-drawn ink on graph paper; " +
103
+ "cinematic_minimal = flat-lay dark surface, warm tungsten lamp; " +
104
+ "dark_studio = matte black workspace, designer aesthetic; " +
105
+ "vibrant_illustrated = bold pop-art flat illustration.",
106
+ },
107
+ explainer_index_id: {
108
+ type: "string",
109
+ description:
110
+ "Optional Twelve Labs index ID returned by index_explainer_sources. " +
111
+ "When set, long-form videos (>2 min) in the broll folder are searched semantically " +
112
+ "instead of being clipped from their start. Requires TWELVE_LABS_API_KEY in the MCP env.",
113
+ },
114
+ caption_style: {
115
+ type: "string",
116
+ enum: CAPTION_STYLE_KEYS,
117
+ description:
118
+ "Caption style. Default 'auto' = AI picks the best style from the transcript. " +
119
+ "default = bold white spring pop-in. " +
120
+ "hormozi = word-by-word yellow highlight (high-energy talking-head). " +
121
+ "glow_neon = neon cyan pulsing text (dark aesthetic, music, gaming). " +
122
+ "minimalist = lowercase thin font fade-in (calm, educational). " +
123
+ "whiteboard = left-to-right text reveal (explainers, tutorials). " +
124
+ "After rendering, change it instantly with swap_caption_style.",
125
+ },
126
+ },
127
+ required: ["aroll"],
128
+ },
129
+
130
+ async execute(args: AssemblyArgs): Promise<string> {
131
+ // AI-generated B-roll mode: delegate to the cloud narrative pipeline.
132
+ if (args.broll_mode === "ai_generated") {
133
+ if (!args.style) {
134
+ return "Error: 'style' is required when broll_mode='ai_generated'. Choose one of: sketchbook, cinematic_minimal, dark_studio, vibrant_illustrated.";
135
+ }
136
+ const job = await post<{ job_id: string; status: string }>("/v1/narrative/jobs", {
137
+ aroll_paths: [args.aroll],
138
+ prompt: args.prompt,
139
+ output_format: args.output_format ?? "9x16",
140
+ broll_mode: "ai_generated",
141
+ style_anchor: args.style,
142
+ });
143
+ const deadline = Date.now() + 15 * 60 * 1000;
144
+ let result: { status: string; preview_url?: string | null; download_url?: string | null; error_message?: string | null } = { status: "queued" };
145
+ while (Date.now() < deadline) {
146
+ await new Promise((r) => setTimeout(r, 6000));
147
+ result = await get(`/v1/narrative/jobs/${job.job_id}`);
148
+ if (result.status === "complete" || result.status === "failed") break;
149
+ }
150
+ if (result.status === "failed") return `Assembly failed: ${result.error_message ?? "unknown error"}`;
151
+ if (result.status !== "complete") return `Assembly timed out. Job ID: ${job.job_id} — check the dashboard.`;
152
+ return (
153
+ `AI B-roll assembly complete (style: ${args.style}).\n` +
154
+ `Preview: ${result.preview_url ?? "—"}\n` +
155
+ `Download: ${result.download_url ?? "—"}\n` +
156
+ `Publish it with publish_now or schedule_post.`
157
+ );
158
+ }
159
+
160
+ const broll = [...(args.broll ?? [])];
161
+
162
+ // Pull in Google Photos picks (re-hosted to public URLs by the backend), if any.
163
+ if (args.google_picker_session) {
164
+ const r = await get<{ status: string; items?: { url: string }[] }>(
165
+ `/v1/assembly/picker?sessionId=${encodeURIComponent(args.google_picker_session)}`
166
+ );
167
+ if (r.status !== "ready") {
168
+ return "Your Google Photos picks aren't ready yet — finish selecting in the picker tab, then run create_video_assembly again with the same google_picker_session.";
169
+ }
170
+ for (const it of r.items ?? []) broll.push(it.url);
171
+ }
172
+
173
+ const cli: string[] = ["--aroll", args.aroll];
174
+ if (broll.length) cli.push("--broll", ...broll);
175
+ if (args.prompt) cli.push("--prompt", args.prompt);
176
+ if (args.style_ref) cli.push("--style-ref", args.style_ref);
177
+ if (args.output_format) cli.push("--format", args.output_format);
178
+ if (args.out) cli.push("--out", args.out);
179
+ if (args.caption_style && args.caption_style !== "auto") cli.push("--caption-style", args.caption_style);
180
+
181
+ // Pull the admin-managed "secret sauce" prompts (LLM Prompt Store →
182
+ // LOCAL VIDEO ASSEMBLY section) and hand them to the engine. Best-effort:
183
+ // on any failure the engine falls back to its built-in prompts.
184
+ const extraEnv: Record<string, string> = {};
185
+ try {
186
+ const r = await get<{ prompts?: Record<string, string> }>("/v1/assembly/prompts");
187
+ if (r.prompts && Object.keys(r.prompts).length) {
188
+ extraEnv.VIRALSPIN_ASSEMBLY_PROMPTS = JSON.stringify(r.prompts);
189
+ }
190
+ } catch { /* engine uses built-in prompts */ }
191
+ if (args.explainer_index_id) {
192
+ extraEnv.EXPLAINER_INDEX_ID = args.explainer_index_id;
193
+ }
194
+ if (process.env.TWELVE_LABS_API_KEY) {
195
+ extraEnv.TWELVE_LABS_API_KEY = process.env.TWELVE_LABS_API_KEY;
196
+ }
197
+
198
+ const outPath = await runEngine(cli, extraEnv);
199
+ return (
200
+ `Video assembly complete.\n` +
201
+ `Output: ${outPath}\n` +
202
+ `Publish it with publish_now (paths: ["${outPath}"]) or schedule_post.`
203
+ );
204
+ },
205
+ };
package/tsconfig.json CHANGED
@@ -1,15 +1,15 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "module": "commonjs",
5
- "lib": ["ES2020"],
6
- "outDir": "./dist",
7
- "rootDir": "./src",
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "resolveJsonModule": true
12
- },
13
- "include": ["src/**/*"],
14
- "exclude": ["node_modules", "dist"]
15
- }
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "resolveJsonModule": true
12
+ },
13
+ "include": ["src/**/*"],
14
+ "exclude": ["node_modules", "dist"]
15
+ }