@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.
- package/BUNDLE_LOCAL_SPEC.md +133 -0
- package/dist/client.js +47 -9
- package/dist/index.js +83 -1
- package/dist/tools/assembly-adjust.js +102 -0
- package/dist/tools/carousel-video.js +190 -0
- package/dist/tools/engage.js +88 -0
- package/dist/tools/explainer-index.js +301 -0
- package/dist/tools/image-ads.js +156 -0
- package/dist/tools/kinetic-text.js +118 -0
- package/dist/tools/photo-picker.js +31 -0
- package/dist/tools/publish.js +72 -1
- package/dist/tools/review.js +103 -0
- package/dist/tools/video-assembly.js +183 -0
- package/engine/assemble.mjs +476 -0
- package/engine/gemini.mjs +164 -0
- package/engine/kinetic.mjs +84 -0
- package/engine/prompts.mjs +94 -0
- package/engine/remotion.config.ts +4 -0
- package/engine/src/Assembly.tsx +330 -0
- package/engine/src/KineticText.tsx +253 -0
- package/engine/src/Root.tsx +39 -0
- package/engine/src/index.ts +4 -0
- package/engine/tsconfig.json +15 -0
- package/package.json +29 -29
- package/src/client.ts +119 -80
- package/src/index.ts +151 -60
- package/src/tools/assembly-adjust.ts +110 -0
- package/src/tools/captions.ts +32 -32
- package/src/tools/carousel-video.ts +211 -0
- package/src/tools/connections.ts +27 -27
- package/src/tools/engage.ts +105 -0
- package/src/tools/explainer-index.ts +298 -0
- package/src/tools/image-ads.ts +192 -0
- package/src/tools/kinetic-text.ts +130 -0
- package/src/tools/photo-picker.ts +31 -0
- package/src/tools/posts.ts +58 -58
- package/src/tools/publish.ts +236 -158
- package/src/tools/review.ts +126 -0
- package/src/tools/video-assembly.ts +205 -0
- package/tsconfig.json +15 -15
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
const ENGINE_DIR =
|
|
5
|
+
process.env.VIRALSPIN_ASSEMBLY_DIR || path.resolve(__dirname, "../../engine");
|
|
6
|
+
|
|
7
|
+
const EFFECTS = ["typing", "billboard_flip", "isometric"] as const;
|
|
8
|
+
const STYLES = ["terminal", "cinematic", "minimal"] as const;
|
|
9
|
+
const FORMATS = ["9x16", "16x9", "1x1"] as const;
|
|
10
|
+
|
|
11
|
+
interface KineticTextArgs {
|
|
12
|
+
text: string;
|
|
13
|
+
effect?: typeof EFFECTS[number];
|
|
14
|
+
style?: typeof STYLES[number];
|
|
15
|
+
format?: typeof FORMATS[number];
|
|
16
|
+
font_size?: number;
|
|
17
|
+
camera?: {
|
|
18
|
+
rotateX?: number;
|
|
19
|
+
rotateY?: number;
|
|
20
|
+
rotateZ?: number;
|
|
21
|
+
translateZ?: number;
|
|
22
|
+
};
|
|
23
|
+
duration_sec?: number;
|
|
24
|
+
out?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function runKinetic(args: string[]): Promise<string> {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const child = spawn(
|
|
30
|
+
process.platform === "win32" ? "node.exe" : "node",
|
|
31
|
+
[path.join(ENGINE_DIR, "kinetic.mjs"), ...args],
|
|
32
|
+
{ cwd: ENGINE_DIR, env: { ...process.env } }
|
|
33
|
+
);
|
|
34
|
+
let stdout = "";
|
|
35
|
+
child.stdout.on("data", (d) => { stdout += d.toString(); });
|
|
36
|
+
child.stderr.on("data", (d) => process.stderr.write(d));
|
|
37
|
+
child.on("error", reject);
|
|
38
|
+
child.on("close", (code) => {
|
|
39
|
+
const m = stdout.match(/RESULT:(.+)/);
|
|
40
|
+
if (code === 0 && m) resolve(m[1].trim());
|
|
41
|
+
else reject(new Error(`Kinetic render failed (exit ${code}). ${stdout.slice(-300)}`));
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const createKineticTextTool = {
|
|
47
|
+
name: "create_kinetic_text",
|
|
48
|
+
description:
|
|
49
|
+
"Render a standalone cinematic typing-animation video — great for explainer intros that visually demo " +
|
|
50
|
+
"what a user types into the prompt. Text is typed out character-by-character with a blinking cursor " +
|
|
51
|
+
"while a 3D camera slowly pans, giving a cinematic 'Star Wars pull-back' feel. " +
|
|
52
|
+
"Three effects: 'typing' (character-by-character with 3D camera), " +
|
|
53
|
+
"'billboard_flip' (words fly from Z-depth and snap into place), " +
|
|
54
|
+
"'isometric' (letters extrude upward on a tilted plane). " +
|
|
55
|
+
"Three styles: 'terminal' (green-on-dark, monospace, scanlines), " +
|
|
56
|
+
"'cinematic' (white-on-black, serif, vignette), " +
|
|
57
|
+
"'minimal' (transparent background, for overlay use). " +
|
|
58
|
+
"Returns the path to a finished MP4 — feed it to publish_now or as A-roll in create_video_assembly.",
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
text: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "The text to animate — e.g. the prompt the user would type.",
|
|
65
|
+
},
|
|
66
|
+
effect: {
|
|
67
|
+
type: "string",
|
|
68
|
+
enum: EFFECTS,
|
|
69
|
+
description: "Animation style. Default: typing.",
|
|
70
|
+
},
|
|
71
|
+
style: {
|
|
72
|
+
type: "string",
|
|
73
|
+
enum: STYLES,
|
|
74
|
+
description: "Visual theme. terminal = green monospace; cinematic = white serif; minimal = transparent BG. Default: terminal.",
|
|
75
|
+
},
|
|
76
|
+
format: {
|
|
77
|
+
type: "string",
|
|
78
|
+
enum: FORMATS,
|
|
79
|
+
description: "Output aspect ratio. Default: 9x16 (vertical).",
|
|
80
|
+
},
|
|
81
|
+
font_size: {
|
|
82
|
+
type: "number",
|
|
83
|
+
description: "Font size in px. Auto-sized by format if omitted (72px for 9x16, 96px for 16x9).",
|
|
84
|
+
},
|
|
85
|
+
camera: {
|
|
86
|
+
type: "object",
|
|
87
|
+
description: "Override the 3D camera end position. Each field defaults to the effect's cinematic preset.",
|
|
88
|
+
properties: {
|
|
89
|
+
rotateX: { type: "number", description: "X-axis tilt in degrees (positive = tilt top toward viewer)." },
|
|
90
|
+
rotateY: { type: "number", description: "Y-axis turn in degrees." },
|
|
91
|
+
rotateZ: { type: "number", description: "Z-axis roll in degrees." },
|
|
92
|
+
translateZ: { type: "number", description: "Z-depth offset in px (negative = pull back/zoom out)." },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
duration_sec: {
|
|
96
|
+
type: "number",
|
|
97
|
+
description: "Override clip length in seconds. Default: auto (≈ text.length × 0.1s + 1.5s hold).",
|
|
98
|
+
},
|
|
99
|
+
out: {
|
|
100
|
+
type: "string",
|
|
101
|
+
description: "Output MP4 path. Defaults to engine/out/kinetic_<timestamp>.mp4.",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
required: ["text"],
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
async execute(args: KineticTextArgs): Promise<string> {
|
|
108
|
+
const cli: string[] = ["--text", args.text];
|
|
109
|
+
if (args.effect) cli.push("--effect", args.effect);
|
|
110
|
+
if (args.style) cli.push("--style", args.style);
|
|
111
|
+
if (args.format) cli.push("--format", args.format);
|
|
112
|
+
if (args.font_size) cli.push("--font-size", String(args.font_size));
|
|
113
|
+
if (args.duration_sec) cli.push("--duration-sec", String(args.duration_sec));
|
|
114
|
+
if (args.out) cli.push("--out", args.out);
|
|
115
|
+
if (args.camera) {
|
|
116
|
+
const c = args.camera;
|
|
117
|
+
if (c.rotateX != null) cli.push("--camera-rx", String(c.rotateX));
|
|
118
|
+
if (c.rotateY != null) cli.push("--camera-ry", String(c.rotateY));
|
|
119
|
+
if (c.rotateZ != null) cli.push("--camera-rz", String(c.rotateZ));
|
|
120
|
+
if (c.translateZ != null) cli.push("--camera-tz", String(c.translateZ));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const outPath = await runKinetic(cli);
|
|
124
|
+
return (
|
|
125
|
+
`Kinetic text render complete.\n` +
|
|
126
|
+
`Output: ${outPath}\n` +
|
|
127
|
+
`Use this as a hook intro by passing it as A-roll to create_video_assembly, or publish directly with publish_now.`
|
|
128
|
+
);
|
|
129
|
+
},
|
|
130
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { post } from "../client.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Opens a Google Photos Picker session for choosing B-roll. Google does NOT allow
|
|
5
|
+
* apps to list or search a user's albums programmatically (Library API readonly was
|
|
6
|
+
* removed 2025-03-31), so the user searches/selects inside Google's own picker UI.
|
|
7
|
+
* This tool just hands back the picker link + sessionId; create_video_assembly then
|
|
8
|
+
* ingests the picks via google_picker_session.
|
|
9
|
+
*/
|
|
10
|
+
export const startPhotoPickerTool = {
|
|
11
|
+
name: "start_photo_picker",
|
|
12
|
+
description:
|
|
13
|
+
"Open a Google Photos picker to choose B-roll for create_video_assembly. Returns a link to open in your browser. " +
|
|
14
|
+
"Google does not allow listing or searching your albums programmatically — so use the picker's OWN search box to find an " +
|
|
15
|
+
"album or topic that matches your video (by person, place, event, or theme), select the photos/clips, and finish. " +
|
|
16
|
+
"Then call create_video_assembly with google_picker_session set to the returned sessionId. " +
|
|
17
|
+
"Requires a Google account connected at app.viralspin.ai/settings.",
|
|
18
|
+
inputSchema: { type: "object", properties: {} },
|
|
19
|
+
|
|
20
|
+
async execute(): Promise<string> {
|
|
21
|
+
const r = await post<{ sessionId: string; pickerUri: string }>("/v1/assembly/picker", {});
|
|
22
|
+
return [
|
|
23
|
+
"📸 Open this link to pick your Google Photos B-roll:",
|
|
24
|
+
r.pickerUri,
|
|
25
|
+
"",
|
|
26
|
+
"Tip: use the picker's search box to find an album/topic that matches your video — by person, place, event, or theme — then select the clips/photos and finish.",
|
|
27
|
+
"",
|
|
28
|
+
`When done, run create_video_assembly with: google_picker_session: "${r.sessionId}" (plus your A-roll).`,
|
|
29
|
+
].join("\n");
|
|
30
|
+
},
|
|
31
|
+
};
|
package/src/tools/posts.ts
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
import { get, del } from "../client.js";
|
|
2
|
-
|
|
3
|
-
interface ScheduledPost {
|
|
4
|
-
id: string;
|
|
5
|
-
platforms: string[];
|
|
6
|
-
caption: string;
|
|
7
|
-
status: string;
|
|
8
|
-
scheduled_time: string;
|
|
9
|
-
published_at: string | null;
|
|
10
|
-
failed_reason: string | null;
|
|
11
|
-
media_type: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const listPostsTool = {
|
|
15
|
-
name: "list_posts",
|
|
16
|
-
description: "List your scheduled, published, or failed posts.",
|
|
17
|
-
inputSchema: {
|
|
18
|
-
type: "object",
|
|
19
|
-
properties: {
|
|
20
|
-
status: {
|
|
21
|
-
type: "string",
|
|
22
|
-
enum: ["pending", "published", "cancelled", "failed"],
|
|
23
|
-
description: "Filter by status. Omit to list all posts.",
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
required: [],
|
|
27
|
-
},
|
|
28
|
-
|
|
29
|
-
async execute(args: { status?: string }) {
|
|
30
|
-
const qs = args.status ? `?status=${args.status}` : "";
|
|
31
|
-
const posts = await get<ScheduledPost[]>(`/v1/publish/posts${qs}`);
|
|
32
|
-
if (!posts.length) return "No posts found.";
|
|
33
|
-
return posts
|
|
34
|
-
.map((p) => {
|
|
35
|
-
const time = p.published_at ?? p.scheduled_time;
|
|
36
|
-
const label = p.published_at ? "published" : "scheduled";
|
|
37
|
-
return `[${p.id.slice(0, 8)}] ${p.status.toUpperCase()} — ${p.platforms.join("+")} ${p.media_type} | ${label}: ${time}\n "${p.caption?.slice(0, 60)}${(p.caption?.length ?? 0) > 60 ? "…" : ""}"${p.failed_reason ? `\n Error: ${p.failed_reason}` : ""}`;
|
|
38
|
-
})
|
|
39
|
-
.join("\n\n");
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
export const cancelPostTool = {
|
|
44
|
-
name: "cancel_post",
|
|
45
|
-
description: "Cancel a pending scheduled post by ID.",
|
|
46
|
-
inputSchema: {
|
|
47
|
-
type: "object",
|
|
48
|
-
properties: {
|
|
49
|
-
post_id: { type: "string", description: "The post ID to cancel." },
|
|
50
|
-
},
|
|
51
|
-
required: ["post_id"],
|
|
52
|
-
},
|
|
53
|
-
|
|
54
|
-
async execute(args: { post_id: string }) {
|
|
55
|
-
await del(`/v1/publish/posts/${args.post_id}`);
|
|
56
|
-
return `Post ${args.post_id} cancelled.`;
|
|
57
|
-
},
|
|
58
|
-
};
|
|
1
|
+
import { get, del } from "../client.js";
|
|
2
|
+
|
|
3
|
+
interface ScheduledPost {
|
|
4
|
+
id: string;
|
|
5
|
+
platforms: string[];
|
|
6
|
+
caption: string;
|
|
7
|
+
status: string;
|
|
8
|
+
scheduled_time: string;
|
|
9
|
+
published_at: string | null;
|
|
10
|
+
failed_reason: string | null;
|
|
11
|
+
media_type: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const listPostsTool = {
|
|
15
|
+
name: "list_posts",
|
|
16
|
+
description: "List your scheduled, published, or failed posts.",
|
|
17
|
+
inputSchema: {
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
status: {
|
|
21
|
+
type: "string",
|
|
22
|
+
enum: ["pending", "published", "cancelled", "failed"],
|
|
23
|
+
description: "Filter by status. Omit to list all posts.",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
required: [],
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
async execute(args: { status?: string }) {
|
|
30
|
+
const qs = args.status ? `?status=${args.status}` : "";
|
|
31
|
+
const posts = await get<ScheduledPost[]>(`/v1/publish/posts${qs}`);
|
|
32
|
+
if (!posts.length) return "No posts found.";
|
|
33
|
+
return posts
|
|
34
|
+
.map((p) => {
|
|
35
|
+
const time = p.published_at ?? p.scheduled_time;
|
|
36
|
+
const label = p.published_at ? "published" : "scheduled";
|
|
37
|
+
return `[${p.id.slice(0, 8)}] ${p.status.toUpperCase()} — ${p.platforms.join("+")} ${p.media_type} | ${label}: ${time}\n "${p.caption?.slice(0, 60)}${(p.caption?.length ?? 0) > 60 ? "…" : ""}"${p.failed_reason ? `\n Error: ${p.failed_reason}` : ""}`;
|
|
38
|
+
})
|
|
39
|
+
.join("\n\n");
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const cancelPostTool = {
|
|
44
|
+
name: "cancel_post",
|
|
45
|
+
description: "Cancel a pending scheduled post by ID.",
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {
|
|
49
|
+
post_id: { type: "string", description: "The post ID to cancel." },
|
|
50
|
+
},
|
|
51
|
+
required: ["post_id"],
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
async execute(args: { post_id: string }) {
|
|
55
|
+
await del(`/v1/publish/posts/${args.post_id}`);
|
|
56
|
+
return `Post ${args.post_id} cancelled.`;
|
|
57
|
+
},
|
|
58
|
+
};
|