@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,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.startPhotoPickerTool = void 0;
|
|
4
|
+
const client_js_1 = require("../client.js");
|
|
5
|
+
/**
|
|
6
|
+
* Opens a Google Photos Picker session for choosing B-roll. Google does NOT allow
|
|
7
|
+
* apps to list or search a user's albums programmatically (Library API readonly was
|
|
8
|
+
* removed 2025-03-31), so the user searches/selects inside Google's own picker UI.
|
|
9
|
+
* This tool just hands back the picker link + sessionId; create_video_assembly then
|
|
10
|
+
* ingests the picks via google_picker_session.
|
|
11
|
+
*/
|
|
12
|
+
exports.startPhotoPickerTool = {
|
|
13
|
+
name: "start_photo_picker",
|
|
14
|
+
description: "Open a Google Photos picker to choose B-roll for create_video_assembly. Returns a link to open in your browser. " +
|
|
15
|
+
"Google does not allow listing or searching your albums programmatically — so use the picker's OWN search box to find an " +
|
|
16
|
+
"album or topic that matches your video (by person, place, event, or theme), select the photos/clips, and finish. " +
|
|
17
|
+
"Then call create_video_assembly with google_picker_session set to the returned sessionId. " +
|
|
18
|
+
"Requires a Google account connected at app.viralspin.ai/settings.",
|
|
19
|
+
inputSchema: { type: "object", properties: {} },
|
|
20
|
+
async execute() {
|
|
21
|
+
const r = await (0, client_js_1.post)("/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/dist/tools/publish.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.schedulePostTool = exports.publishNowTool = void 0;
|
|
6
|
+
exports.schedulePostTool = exports.sendToDraftsTool = exports.publishNowTool = void 0;
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const client_js_1 = require("../client.js");
|
|
@@ -99,6 +99,77 @@ exports.publishNowTool = {
|
|
|
99
99
|
return summary.trim();
|
|
100
100
|
},
|
|
101
101
|
};
|
|
102
|
+
exports.sendToDraftsTool = {
|
|
103
|
+
name: "send_to_drafts",
|
|
104
|
+
description: "Upload local file(s) and send to the platform's drafts (TikTok inbox / YouTube as private) so the user can finish in the mobile app — useful when they want to add licensed music or polish before publishing. " +
|
|
105
|
+
"Instagram does NOT support drafts via API (IG drafts are local to the mobile app only) — if instagram is in the platforms list, that platform will return an error explaining the limitation. " +
|
|
106
|
+
"Pass a folder path to send all images as a carousel draft.",
|
|
107
|
+
inputSchema: {
|
|
108
|
+
type: "object",
|
|
109
|
+
properties: {
|
|
110
|
+
paths: {
|
|
111
|
+
type: "array",
|
|
112
|
+
items: { type: "string" },
|
|
113
|
+
description: "Local file path(s) or a folder path. Single video → video draft. Multiple images or image folder → carousel draft.",
|
|
114
|
+
},
|
|
115
|
+
platforms: {
|
|
116
|
+
type: "array",
|
|
117
|
+
items: { type: "string", enum: ["tiktok", "youtube"] },
|
|
118
|
+
description: "Platforms to send drafts to. Must be connected. Instagram is not supported (API limitation).",
|
|
119
|
+
},
|
|
120
|
+
caption: {
|
|
121
|
+
type: "string",
|
|
122
|
+
description: "Caption text. On TikTok video drafts, the caption is NOT used (user enters it in-app); on TikTok photo drafts and YouTube drafts it is preserved.",
|
|
123
|
+
},
|
|
124
|
+
hashtags: {
|
|
125
|
+
type: "array",
|
|
126
|
+
items: { type: "string" },
|
|
127
|
+
description: "Hashtags without # prefix.",
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
required: ["paths", "platforms", "caption"],
|
|
131
|
+
},
|
|
132
|
+
async execute(args) {
|
|
133
|
+
const filePaths = expandPaths(args.paths);
|
|
134
|
+
if (!filePaths.length)
|
|
135
|
+
throw new Error("No media files found in the provided paths.");
|
|
136
|
+
const mediaType = detectMediaType(filePaths);
|
|
137
|
+
if (mediaType === "carousel" && filePaths.length > 10) {
|
|
138
|
+
throw new Error(`Carousel max is 10 images. Found ${filePaths.length} files.`);
|
|
139
|
+
}
|
|
140
|
+
// Surface the IG-not-supported gotcha before consuming credits / uploading.
|
|
141
|
+
if (args.platforms.includes("instagram")) {
|
|
142
|
+
throw new Error("Instagram does not support drafts via API. IG drafts are local to the mobile app only — " +
|
|
143
|
+
"there is no API endpoint to send to drafts. Remove 'instagram' from platforms, or use schedule_post / publish_now instead.");
|
|
144
|
+
}
|
|
145
|
+
const { urls, storage_paths } = await (0, client_js_1.uploadFiles)(filePaths);
|
|
146
|
+
let result;
|
|
147
|
+
try {
|
|
148
|
+
result = await (0, client_js_1.post)("/v1/publish/schedule", {
|
|
149
|
+
media_urls: urls,
|
|
150
|
+
media_type: mediaType,
|
|
151
|
+
platforms: args.platforms,
|
|
152
|
+
caption: args.caption,
|
|
153
|
+
hashtags: args.hashtags ?? [],
|
|
154
|
+
publish_now: true,
|
|
155
|
+
mode: "draft",
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
await (0, client_js_1.deleteFiles)(storage_paths).catch(() => { });
|
|
160
|
+
}
|
|
161
|
+
const { status, platform_post_ids, failed_reason } = result.post;
|
|
162
|
+
const sentTo = Object.keys(platform_post_ids);
|
|
163
|
+
let summary = `Status: ${status}\n`;
|
|
164
|
+
if (sentTo.length) {
|
|
165
|
+
summary += `Sent to drafts on: ${sentTo.join(", ")}\n`;
|
|
166
|
+
summary += `Open the platform's mobile app to finish — add music, adjust caption, then publish.`;
|
|
167
|
+
}
|
|
168
|
+
if (failed_reason)
|
|
169
|
+
summary += `\nErrors: ${failed_reason}`;
|
|
170
|
+
return summary.trim();
|
|
171
|
+
},
|
|
172
|
+
};
|
|
102
173
|
exports.schedulePostTool = {
|
|
103
174
|
name: "schedule_post",
|
|
104
175
|
description: "Upload local file(s) and schedule a post for a future time. Pass a folder path to schedule all images as a carousel.",
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.checkReviewStatusTool = exports.createReviewLinkTool = void 0;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
const client_js_1 = require("../client.js");
|
|
9
|
+
const APP_BASE = process.env.VIRALSPIN_APP_URL || "https://app.viralspin.ai";
|
|
10
|
+
function makeToken() {
|
|
11
|
+
return crypto_1.default.randomBytes(9).toString("base64url").slice(0, 12);
|
|
12
|
+
}
|
|
13
|
+
exports.createReviewLinkTool = {
|
|
14
|
+
name: "create_review_link",
|
|
15
|
+
description: "After assembling a video, generate a private shareable review link for a client. " +
|
|
16
|
+
"The client opens the link in their browser — no account required — sees the video in a " +
|
|
17
|
+
"premium 'Private Review Room', and clicks Approve or Request Changes. " +
|
|
18
|
+
"Returns the review URL to paste into an email or DM.",
|
|
19
|
+
inputSchema: {
|
|
20
|
+
type: "object",
|
|
21
|
+
required: ["output_path"],
|
|
22
|
+
properties: {
|
|
23
|
+
output_path: {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: "The local file path or public URL of the rendered MP4 — e.g. the path returned by create_video_assembly.",
|
|
26
|
+
},
|
|
27
|
+
metadata: {
|
|
28
|
+
type: "object",
|
|
29
|
+
description: "Optional context shown in the review room: caption, hook_text, style, format, platform_targets.",
|
|
30
|
+
properties: {
|
|
31
|
+
caption: { type: "string" },
|
|
32
|
+
hook_text: { type: "string" },
|
|
33
|
+
style: { type: "string" },
|
|
34
|
+
format: { type: "string", enum: ["9x16", "1x1", "16x9"] },
|
|
35
|
+
platform_targets: { type: "array", items: { type: "string" } },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
pin: {
|
|
39
|
+
type: "string",
|
|
40
|
+
description: "Optional 4-digit PIN the client must enter before viewing the review.",
|
|
41
|
+
},
|
|
42
|
+
expires_hours: {
|
|
43
|
+
type: "number",
|
|
44
|
+
description: "How many hours until the link expires (default: 168 = 7 days).",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
async execute(args) {
|
|
49
|
+
const token = makeToken();
|
|
50
|
+
const expiresHours = args.expires_hours ?? 168;
|
|
51
|
+
await (0, client_js_1.post)("/v1/review-sessions", {
|
|
52
|
+
token,
|
|
53
|
+
output_path: args.output_path,
|
|
54
|
+
metadata: args.metadata ?? {},
|
|
55
|
+
pin: args.pin ?? null,
|
|
56
|
+
expires_hours: expiresHours,
|
|
57
|
+
});
|
|
58
|
+
const reviewUrl = `${APP_BASE}/review/${token}`;
|
|
59
|
+
return [
|
|
60
|
+
`✅ Review link created:`,
|
|
61
|
+
``,
|
|
62
|
+
` ${reviewUrl}`,
|
|
63
|
+
``,
|
|
64
|
+
`Send this link to your client. They'll see the video in a private premiere room`,
|
|
65
|
+
`and can Approve or Request Changes — no account required.`,
|
|
66
|
+
args.pin ? `PIN required: ${args.pin}` : ``,
|
|
67
|
+
`Link expires in ${expiresHours >= 24 ? `${Math.round(expiresHours / 24)} days` : `${expiresHours}h`}.`,
|
|
68
|
+
``,
|
|
69
|
+
`To check if they've approved: check_review_status with token "${token}"`,
|
|
70
|
+
]
|
|
71
|
+
.filter((l) => l !== undefined)
|
|
72
|
+
.join("\n");
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
exports.checkReviewStatusTool = {
|
|
76
|
+
name: "check_review_status",
|
|
77
|
+
description: "Check whether a client has approved or requested changes on a review link. " +
|
|
78
|
+
"Pass the token from the URL (e.g. 'abc123xyz456' from /review/abc123xyz456).",
|
|
79
|
+
inputSchema: {
|
|
80
|
+
type: "object",
|
|
81
|
+
required: ["token"],
|
|
82
|
+
properties: {
|
|
83
|
+
token: {
|
|
84
|
+
type: "string",
|
|
85
|
+
description: "The 12-character review token from the review URL.",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
async execute({ token }) {
|
|
90
|
+
const data = await (0, client_js_1.get)(`/v1/review-sessions/${token}/status`);
|
|
91
|
+
const statusLine = data.status === "approved"
|
|
92
|
+
? `✅ Approved${data.approved_at ? ` on ${new Date(data.approved_at).toLocaleString()}` : ""}`
|
|
93
|
+
: data.status === "rejected"
|
|
94
|
+
? `📝 Changes requested`
|
|
95
|
+
: `⏳ Pending — client hasn't reviewed yet`;
|
|
96
|
+
const lines = [statusLine];
|
|
97
|
+
if (data.feedback)
|
|
98
|
+
lines.push(``, `Client feedback: "${data.feedback}"`);
|
|
99
|
+
if (data.status === "approved")
|
|
100
|
+
lines.push(``, `Ready to publish with publish_now or schedule_post.`);
|
|
101
|
+
return lines.join("\n");
|
|
102
|
+
},
|
|
103
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createVideoAssemblyTool = void 0;
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const client_js_1 = require("../client.js");
|
|
10
|
+
// The assembly engine ships inside this package (mcp/engine). Override with env if needed.
|
|
11
|
+
// __dirname at runtime = mcp/dist/tools → ../../engine = mcp/engine.
|
|
12
|
+
const ENGINE_DIR = process.env.VIRALSPIN_ASSEMBLY_DIR || path_1.default.resolve(__dirname, "../../engine");
|
|
13
|
+
const BROLL_STYLE_KEYS = ["sketchbook", "cinematic_minimal", "dark_studio", "vibrant_illustrated"];
|
|
14
|
+
const CAPTION_STYLE_KEYS = ["auto", "default", "hormozi", "glow_neon", "minimalist", "whiteboard"];
|
|
15
|
+
/** Run the engine orchestrator, streaming its logs to stderr, and capture RESULT:<path>. */
|
|
16
|
+
function runEngine(args, extraEnv = {}) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const child = (0, child_process_1.spawn)(process.platform === "win32" ? "node.exe" : "node", [path_1.default.join(ENGINE_DIR, "assemble.mjs"), ...args], { cwd: ENGINE_DIR, env: { ...process.env, ...extraEnv } });
|
|
19
|
+
let stdout = "";
|
|
20
|
+
child.stdout.on("data", (d) => { stdout += d.toString(); });
|
|
21
|
+
child.stderr.on("data", (d) => process.stderr.write(d)); // progress → MCP server log
|
|
22
|
+
child.on("error", reject);
|
|
23
|
+
child.on("close", (code) => {
|
|
24
|
+
const m = stdout.match(/RESULT:(.+)/);
|
|
25
|
+
if (code === 0 && m)
|
|
26
|
+
resolve(m[1].trim());
|
|
27
|
+
else
|
|
28
|
+
reject(new Error(`Assembly engine exited ${code}. ${stdout.slice(-400)}`));
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
exports.createVideoAssemblyTool = {
|
|
33
|
+
name: "create_video_assembly",
|
|
34
|
+
description: "Turn an A-roll talking-head/performance clip into a polished vertical (9:16) short by editing in B-roll cutaways. " +
|
|
35
|
+
"Runs a LOCAL pipeline: transcribes the A-roll, finds each B-roll's focal point (face/subject-aware so nothing is cropped off), " +
|
|
36
|
+
"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). " +
|
|
37
|
+
"Accepts LOCAL file paths OR public URLs for both A-roll and B-roll, and a B-roll FOLDER is expanded automatically. " +
|
|
38
|
+
"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. " +
|
|
39
|
+
"Returns the path to the finished MP4, which you can then publish with publish_now / schedule_post.",
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: {
|
|
43
|
+
aroll: {
|
|
44
|
+
type: "string",
|
|
45
|
+
description: "A-roll source: a local file path (e.g. C:/clips/talk.mp4) OR a public video URL.",
|
|
46
|
+
},
|
|
47
|
+
broll: {
|
|
48
|
+
type: "array",
|
|
49
|
+
items: { type: "string" },
|
|
50
|
+
description: "Optional B-roll sources: local file paths, a local FOLDER (expanded to its images/videos), and/or public URLs. " +
|
|
51
|
+
"Mix freely. Omit for an A-roll-only edit (just motion, no cutaways).",
|
|
52
|
+
},
|
|
53
|
+
prompt: {
|
|
54
|
+
type: "string",
|
|
55
|
+
description: "Optional creative direction for the edit (mood, what to emphasise, pacing).",
|
|
56
|
+
},
|
|
57
|
+
output_format: {
|
|
58
|
+
type: "string",
|
|
59
|
+
enum: ["9x16", "1x1", "16x9"],
|
|
60
|
+
description: "Aspect ratio. Default 9x16 (vertical for Reels/TikTok/Shorts).",
|
|
61
|
+
},
|
|
62
|
+
out: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "Optional output MP4 path. Defaults to the engine's out/ folder.",
|
|
65
|
+
},
|
|
66
|
+
google_picker_session: {
|
|
67
|
+
type: "string",
|
|
68
|
+
description: "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`).",
|
|
69
|
+
},
|
|
70
|
+
style_ref: {
|
|
71
|
+
type: "string",
|
|
72
|
+
description: "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.",
|
|
73
|
+
},
|
|
74
|
+
broll_mode: {
|
|
75
|
+
type: "string",
|
|
76
|
+
enum: ["user_provided", "ai_generated"],
|
|
77
|
+
description: "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.",
|
|
78
|
+
},
|
|
79
|
+
style: {
|
|
80
|
+
type: "string",
|
|
81
|
+
enum: BROLL_STYLE_KEYS,
|
|
82
|
+
description: "Style anchor for AI-generated B-roll (required when broll_mode='ai_generated'). " +
|
|
83
|
+
"sketchbook = hand-drawn ink on graph paper; " +
|
|
84
|
+
"cinematic_minimal = flat-lay dark surface, warm tungsten lamp; " +
|
|
85
|
+
"dark_studio = matte black workspace, designer aesthetic; " +
|
|
86
|
+
"vibrant_illustrated = bold pop-art flat illustration.",
|
|
87
|
+
},
|
|
88
|
+
explainer_index_id: {
|
|
89
|
+
type: "string",
|
|
90
|
+
description: "Optional Twelve Labs index ID returned by index_explainer_sources. " +
|
|
91
|
+
"When set, long-form videos (>2 min) in the broll folder are searched semantically " +
|
|
92
|
+
"instead of being clipped from their start. Requires TWELVE_LABS_API_KEY in the MCP env.",
|
|
93
|
+
},
|
|
94
|
+
caption_style: {
|
|
95
|
+
type: "string",
|
|
96
|
+
enum: CAPTION_STYLE_KEYS,
|
|
97
|
+
description: "Caption style. Default 'auto' = AI picks the best style from the transcript. " +
|
|
98
|
+
"default = bold white spring pop-in. " +
|
|
99
|
+
"hormozi = word-by-word yellow highlight (high-energy talking-head). " +
|
|
100
|
+
"glow_neon = neon cyan pulsing text (dark aesthetic, music, gaming). " +
|
|
101
|
+
"minimalist = lowercase thin font fade-in (calm, educational). " +
|
|
102
|
+
"whiteboard = left-to-right text reveal (explainers, tutorials). " +
|
|
103
|
+
"After rendering, change it instantly with swap_caption_style.",
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
required: ["aroll"],
|
|
107
|
+
},
|
|
108
|
+
async execute(args) {
|
|
109
|
+
// AI-generated B-roll mode: delegate to the cloud narrative pipeline.
|
|
110
|
+
if (args.broll_mode === "ai_generated") {
|
|
111
|
+
if (!args.style) {
|
|
112
|
+
return "Error: 'style' is required when broll_mode='ai_generated'. Choose one of: sketchbook, cinematic_minimal, dark_studio, vibrant_illustrated.";
|
|
113
|
+
}
|
|
114
|
+
const job = await (0, client_js_1.post)("/v1/narrative/jobs", {
|
|
115
|
+
aroll_paths: [args.aroll],
|
|
116
|
+
prompt: args.prompt,
|
|
117
|
+
output_format: args.output_format ?? "9x16",
|
|
118
|
+
broll_mode: "ai_generated",
|
|
119
|
+
style_anchor: args.style,
|
|
120
|
+
});
|
|
121
|
+
const deadline = Date.now() + 15 * 60 * 1000;
|
|
122
|
+
let result = { status: "queued" };
|
|
123
|
+
while (Date.now() < deadline) {
|
|
124
|
+
await new Promise((r) => setTimeout(r, 6000));
|
|
125
|
+
result = await (0, client_js_1.get)(`/v1/narrative/jobs/${job.job_id}`);
|
|
126
|
+
if (result.status === "complete" || result.status === "failed")
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
if (result.status === "failed")
|
|
130
|
+
return `Assembly failed: ${result.error_message ?? "unknown error"}`;
|
|
131
|
+
if (result.status !== "complete")
|
|
132
|
+
return `Assembly timed out. Job ID: ${job.job_id} — check the dashboard.`;
|
|
133
|
+
return (`AI B-roll assembly complete (style: ${args.style}).\n` +
|
|
134
|
+
`Preview: ${result.preview_url ?? "—"}\n` +
|
|
135
|
+
`Download: ${result.download_url ?? "—"}\n` +
|
|
136
|
+
`Publish it with publish_now or schedule_post.`);
|
|
137
|
+
}
|
|
138
|
+
const broll = [...(args.broll ?? [])];
|
|
139
|
+
// Pull in Google Photos picks (re-hosted to public URLs by the backend), if any.
|
|
140
|
+
if (args.google_picker_session) {
|
|
141
|
+
const r = await (0, client_js_1.get)(`/v1/assembly/picker?sessionId=${encodeURIComponent(args.google_picker_session)}`);
|
|
142
|
+
if (r.status !== "ready") {
|
|
143
|
+
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.";
|
|
144
|
+
}
|
|
145
|
+
for (const it of r.items ?? [])
|
|
146
|
+
broll.push(it.url);
|
|
147
|
+
}
|
|
148
|
+
const cli = ["--aroll", args.aroll];
|
|
149
|
+
if (broll.length)
|
|
150
|
+
cli.push("--broll", ...broll);
|
|
151
|
+
if (args.prompt)
|
|
152
|
+
cli.push("--prompt", args.prompt);
|
|
153
|
+
if (args.style_ref)
|
|
154
|
+
cli.push("--style-ref", args.style_ref);
|
|
155
|
+
if (args.output_format)
|
|
156
|
+
cli.push("--format", args.output_format);
|
|
157
|
+
if (args.out)
|
|
158
|
+
cli.push("--out", args.out);
|
|
159
|
+
if (args.caption_style && args.caption_style !== "auto")
|
|
160
|
+
cli.push("--caption-style", args.caption_style);
|
|
161
|
+
// Pull the admin-managed "secret sauce" prompts (LLM Prompt Store →
|
|
162
|
+
// LOCAL VIDEO ASSEMBLY section) and hand them to the engine. Best-effort:
|
|
163
|
+
// on any failure the engine falls back to its built-in prompts.
|
|
164
|
+
const extraEnv = {};
|
|
165
|
+
try {
|
|
166
|
+
const r = await (0, client_js_1.get)("/v1/assembly/prompts");
|
|
167
|
+
if (r.prompts && Object.keys(r.prompts).length) {
|
|
168
|
+
extraEnv.VIRALSPIN_ASSEMBLY_PROMPTS = JSON.stringify(r.prompts);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch { /* engine uses built-in prompts */ }
|
|
172
|
+
if (args.explainer_index_id) {
|
|
173
|
+
extraEnv.EXPLAINER_INDEX_ID = args.explainer_index_id;
|
|
174
|
+
}
|
|
175
|
+
if (process.env.TWELVE_LABS_API_KEY) {
|
|
176
|
+
extraEnv.TWELVE_LABS_API_KEY = process.env.TWELVE_LABS_API_KEY;
|
|
177
|
+
}
|
|
178
|
+
const outPath = await runEngine(cli, extraEnv);
|
|
179
|
+
return (`Video assembly complete.\n` +
|
|
180
|
+
`Output: ${outPath}\n` +
|
|
181
|
+
`Publish it with publish_now (paths: ["${outPath}"]) or schedule_post.`);
|
|
182
|
+
},
|
|
183
|
+
};
|