@jclvsh/dropspace 2.50.0 → 2.52.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/README.md +12 -2
- package/dist/cli.js +2 -0
- package/dist/commands/scene-clips.d.ts +3 -0
- package/dist/commands/scene-clips.js +79 -0
- package/dist/commands/song-reels.js +72 -12
- package/dist/server.js +2 -0
- package/dist/tool-policy.js +8 -0
- package/dist/tools/scene-clips.d.ts +4 -0
- package/dist/tools/scene-clips.js +66 -0
- package/dist/tools/song-reels.js +66 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,6 +61,9 @@ dropspace launches --help
|
|
|
61
61
|
| audience | follower counts + growth per connected account |
|
|
62
62
|
| autopilot | status, configure, review, approve, reject, run, runs |
|
|
63
63
|
| recommendations (recs) | list, apply, dismiss |
|
|
64
|
+
| songs | list, get, add, lyrics, rename, retry, delete |
|
|
65
|
+
| song-reels | make, rollout, list, get, retry, launch, keep, delete |
|
|
66
|
+
| scene-clips | cut, list, get, retry, delete |
|
|
64
67
|
| profile | get, update |
|
|
65
68
|
| billing | portal |
|
|
66
69
|
|
|
@@ -394,11 +397,18 @@ Reddit engagement (drafted comments in other communities) is paused: its tools a
|
|
|
394
397
|
| `add_song` | Add a song from a YouTube or SoundCloud link, or a public audio file url (local package only) |
|
|
395
398
|
| `get_song_lyrics` | The song's lyrics with word timings, transcribed once (local package only) |
|
|
396
399
|
| `delete_song` | Delete a song and its audio file (local package only) |
|
|
397
|
-
| `make_song_reel` | Make a 9:16 lyric reel from a snippet of a song: clips from the media library
|
|
400
|
+
| `make_song_reel` | Make a 9:16 lyric reel from a snippet of a song: clips from the media library, a gradient, or AI b-roll, lyrics and a hook line burned in (local package only) |
|
|
401
|
+
| `rollout_song_reels` | Make 2-10 versions of one reel at once (style, then clip order or background, changing per version) (local package only) |
|
|
402
|
+
| `keep_song_reel` | Keep one version of a rollout; the others are deleted with their library items (local package only) |
|
|
398
403
|
| `list_song_reels` | Lyric reels, newest first, with render status and video_url once ready (local package only) |
|
|
399
404
|
| `get_song_reel` | One reel with its lyric lines (local package only) |
|
|
400
|
-
| `launch_song_reel` | A draft launch carrying a ready reel and its lyrics as the caption (local package only) |
|
|
405
|
+
| `launch_song_reel` | A draft launch carrying a ready reel and its hook and lyrics as the caption; `trial_reel` makes it an Instagram trial reel (local package only) |
|
|
401
406
|
| `delete_song_reel` | Delete a reel; its media library item and launch stay (local package only) |
|
|
407
|
+
| `cut_scene_clips` | Cut a long video (a media library video, a YouTube link, or a public video url) into 2-8 second clips at its scene changes, up to 40, into the media library (local package only) |
|
|
408
|
+
| `list_scene_clip_jobs` | Clip jobs, newest first, with status, clips made and added, and each clip's urls (local package only) |
|
|
409
|
+
| `get_scene_clip_job` | One clip job; reading a ready job adds its clips to the media library (local package only) |
|
|
410
|
+
| `retry_scene_clip_job` | Cut a failed or stuck job again (local package only) |
|
|
411
|
+
| `delete_scene_clip_job` | Delete a clip job; clips already in the media library stay (local package only) |
|
|
402
412
|
|
|
403
413
|
Prompts (local package only): `instagram_audit`, `best_times_plan`, `winners_report`. Each tells the AI app which tools to call and ends with `save_report`.
|
|
404
414
|
|
package/dist/cli.js
CHANGED
|
@@ -21,6 +21,7 @@ import { registerAudienceCommands } from "./commands/audience.js";
|
|
|
21
21
|
import { registerAccountCommands } from "./commands/account.js";
|
|
22
22
|
import { registerSongCommands } from "./commands/songs.js";
|
|
23
23
|
import { registerSongReelCommands } from "./commands/song-reels.js";
|
|
24
|
+
import { registerSceneClipCommands } from "./commands/scene-clips.js";
|
|
24
25
|
const program = new Command()
|
|
25
26
|
.name("dropspace")
|
|
26
27
|
.description("dropspace CLI - manage launches, personas, boosts, and more")
|
|
@@ -46,6 +47,7 @@ try {
|
|
|
46
47
|
registerAccountCommands(program, client);
|
|
47
48
|
registerSongCommands(program, client);
|
|
48
49
|
registerSongReelCommands(program, client);
|
|
50
|
+
registerSceneClipCommands(program, client);
|
|
49
51
|
await program.parseAsync(process.argv);
|
|
50
52
|
}
|
|
51
53
|
catch (error) {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { formatOutput, handleError, getFormat } from "../cli-utils.js";
|
|
2
|
+
export function registerSceneClipCommands(program, client) {
|
|
3
|
+
const clips = program
|
|
4
|
+
.command("scene-clips")
|
|
5
|
+
.description("cut a long video into short clips for your media library (pro and premium)");
|
|
6
|
+
clips
|
|
7
|
+
.command("cut")
|
|
8
|
+
.description("start a job: give one of --media, --link, or --url")
|
|
9
|
+
.option("--media <id>", "a video in your media library")
|
|
10
|
+
.option("--link <url>", "a YouTube link (up to 30 minutes)")
|
|
11
|
+
.option("--url <url>", "a public video file url (up to 512 MB)")
|
|
12
|
+
.action(async (opts) => {
|
|
13
|
+
try {
|
|
14
|
+
const body = {};
|
|
15
|
+
if (opts.media)
|
|
16
|
+
body.media_library_id = opts.media;
|
|
17
|
+
if (opts.link)
|
|
18
|
+
body.link_url = opts.link;
|
|
19
|
+
if (opts.url)
|
|
20
|
+
body.video_url = opts.url;
|
|
21
|
+
formatOutput(await client.post("/scene-clips", body), getFormat(program.opts()));
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
handleError(error);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
clips
|
|
28
|
+
.command("list")
|
|
29
|
+
.description("your clip jobs, newest first")
|
|
30
|
+
.option("--page <n>", "page number")
|
|
31
|
+
.option("--page-size <n>", "jobs per page (max 100)")
|
|
32
|
+
.action(async (opts) => {
|
|
33
|
+
try {
|
|
34
|
+
const params = {};
|
|
35
|
+
if (opts.page)
|
|
36
|
+
params.page = opts.page;
|
|
37
|
+
if (opts.pageSize)
|
|
38
|
+
params.page_size = opts.pageSize;
|
|
39
|
+
formatOutput(await client.get("/scene-clips", params), getFormat(program.opts()));
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
handleError(error);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
clips
|
|
46
|
+
.command("get <id>")
|
|
47
|
+
.description("one job; reading a ready job adds its clips to your media library")
|
|
48
|
+
.action(async (id) => {
|
|
49
|
+
try {
|
|
50
|
+
formatOutput(await client.get(`/scene-clips/${id}`), getFormat(program.opts()));
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
handleError(error);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
clips
|
|
57
|
+
.command("retry <id>")
|
|
58
|
+
.description("cut a failed (or stuck) job again")
|
|
59
|
+
.action(async (id) => {
|
|
60
|
+
try {
|
|
61
|
+
formatOutput(await client.post(`/scene-clips/${id}/retry`), getFormat(program.opts()));
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
handleError(error);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
clips
|
|
68
|
+
.command("delete <id>")
|
|
69
|
+
.description("delete a job; clips already in your media library stay")
|
|
70
|
+
.action(async (id) => {
|
|
71
|
+
try {
|
|
72
|
+
await client.delete(`/scene-clips/${id}`);
|
|
73
|
+
formatOutput({ id, deleted: true }, getFormat(program.opts()));
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
handleError(error);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
@@ -1,4 +1,29 @@
|
|
|
1
1
|
import { formatOutput, handleError, getFormat } from "../cli-utils.js";
|
|
2
|
+
/** The reel body shared by `make` and `rollout`. */
|
|
3
|
+
function reelBody(opts) {
|
|
4
|
+
const ai = opts.ai !== undefined && opts.ai !== false;
|
|
5
|
+
const body = {
|
|
6
|
+
snippet_start: Number(opts.start),
|
|
7
|
+
snippet_end: Number(opts.end),
|
|
8
|
+
style: opts.style,
|
|
9
|
+
footage_mode: ai ? "ai" : opts.clips ? "clips" : "none",
|
|
10
|
+
};
|
|
11
|
+
if (opts.clips && !ai)
|
|
12
|
+
body.clip_ids = String(opts.clips)
|
|
13
|
+
.split(",")
|
|
14
|
+
.map((id) => id.trim())
|
|
15
|
+
.filter(Boolean);
|
|
16
|
+
if (opts.background)
|
|
17
|
+
body.background = opts.background;
|
|
18
|
+
if (opts.hook)
|
|
19
|
+
body.hook_text = String(opts.hook);
|
|
20
|
+
if (ai) {
|
|
21
|
+
if (typeof opts.ai === "string" && opts.ai.trim())
|
|
22
|
+
body.ai_prompt = opts.ai.trim();
|
|
23
|
+
body.ai_clips = Number(opts.aiClips ?? 2);
|
|
24
|
+
}
|
|
25
|
+
return body;
|
|
26
|
+
}
|
|
2
27
|
export function registerSongReelCommands(program, client) {
|
|
3
28
|
const reels = program.command("song-reels").description("lyric reels made from your songs (pro and premium)");
|
|
4
29
|
reels
|
|
@@ -9,19 +34,39 @@ export function registerSongReelCommands(program, client) {
|
|
|
9
34
|
.option("--style <style>", "lower-third, center-bold, karaoke, one-word, or caps-top", "lower-third")
|
|
10
35
|
.option("--clips <ids>", "media library ids in order, comma separated (footage from your library)")
|
|
11
36
|
.option("--background <name>", "midnight, sunset, forest, sand, slate, or violet (no footage)")
|
|
37
|
+
.option("--hook <text>", "a line burned in for the whole snippet, up to 90 characters")
|
|
38
|
+
.option("--ai [prompt]", "AI b-roll instead of clips: what it should show (the lyrics stand in when empty); each clip is one of your plan's AI videos")
|
|
39
|
+
.option("--ai-clips <n>", "b-roll clips to generate, 1-4 (with --ai)", "2")
|
|
40
|
+
.action(async (songId, opts) => {
|
|
41
|
+
try {
|
|
42
|
+
formatOutput(await client.post(`/songs/${songId}/reels`, reelBody(opts)), getFormat(program.opts()));
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
handleError(error);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
reels
|
|
49
|
+
.command("rollout <songId>")
|
|
50
|
+
.description("make N versions of one reel at once (the style, then the clip order or background, changes per version)")
|
|
51
|
+
.requiredOption("--count <n>", "how many versions, 2-10")
|
|
52
|
+
.requiredOption("--start <seconds>", "snippet start, seconds from the song's start")
|
|
53
|
+
.requiredOption("--end <seconds>", "snippet end, 5 to 30 seconds after the start")
|
|
54
|
+
.option("--style <style>", "the first style (the versions cycle through --styles)", "lower-third")
|
|
55
|
+
.option("--styles <styles>", "the styles to cycle through, comma separated; all five when omitted")
|
|
56
|
+
.option("--clips <ids>", "media library ids in order, comma separated (footage from your library)")
|
|
57
|
+
.option("--background <name>", "midnight, sunset, forest, sand, slate, or violet (no footage)")
|
|
58
|
+
.option("--hook <text>", "a line burned in for the whole snippet, up to 90 characters")
|
|
59
|
+
.option("--ai [prompt]", "AI b-roll, generated once and shared by every version")
|
|
60
|
+
.option("--ai-clips <n>", "b-roll clips to generate, 1-4 (with --ai)", "2")
|
|
12
61
|
.action(async (songId, opts) => {
|
|
13
62
|
try {
|
|
14
|
-
const body = {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
body.clip_ids = String(opts.clips).split(",").map((id) => id.trim()).filter(Boolean);
|
|
22
|
-
if (opts.background)
|
|
23
|
-
body.background = opts.background;
|
|
24
|
-
formatOutput(await client.post(`/songs/${songId}/reels`, body), getFormat(program.opts()));
|
|
63
|
+
const body = { ...reelBody(opts), count: Number(opts.count) };
|
|
64
|
+
if (opts.styles)
|
|
65
|
+
body.styles = String(opts.styles)
|
|
66
|
+
.split(",")
|
|
67
|
+
.map((style) => style.trim())
|
|
68
|
+
.filter(Boolean);
|
|
69
|
+
formatOutput(await client.post(`/songs/${songId}/reels/rollout`, body), getFormat(program.opts()));
|
|
25
70
|
}
|
|
26
71
|
catch (error) {
|
|
27
72
|
handleError(error);
|
|
@@ -31,6 +76,7 @@ export function registerSongReelCommands(program, client) {
|
|
|
31
76
|
.command("list")
|
|
32
77
|
.description("your lyric reels, newest first")
|
|
33
78
|
.option("--song <id>", "only this song's reels")
|
|
79
|
+
.option("--rollout <id>", "only this rollout's versions")
|
|
34
80
|
.option("--page <n>", "page number")
|
|
35
81
|
.option("--page-size <n>", "reels per page (max 100)")
|
|
36
82
|
.action(async (opts) => {
|
|
@@ -38,6 +84,8 @@ export function registerSongReelCommands(program, client) {
|
|
|
38
84
|
const params = {};
|
|
39
85
|
if (opts.song)
|
|
40
86
|
params.song_id = opts.song;
|
|
87
|
+
if (opts.rollout)
|
|
88
|
+
params.rollout_id = opts.rollout;
|
|
41
89
|
if (opts.page)
|
|
42
90
|
params.page = opts.page;
|
|
43
91
|
if (opts.pageSize)
|
|
@@ -73,9 +121,21 @@ export function registerSongReelCommands(program, client) {
|
|
|
73
121
|
reels
|
|
74
122
|
.command("launch <id>")
|
|
75
123
|
.description("make a draft launch from a ready reel")
|
|
124
|
+
.option("--trial", "post it as an Instagram trial reel (shown to non-followers first)")
|
|
125
|
+
.action(async (id, opts) => {
|
|
126
|
+
try {
|
|
127
|
+
formatOutput(await client.post(`/song-reels/${id}/launch`, opts.trial ? { trial_reel: true } : {}), getFormat(program.opts()));
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
handleError(error);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
reels
|
|
134
|
+
.command("keep <id>")
|
|
135
|
+
.description("keep this version of a rollout; its other versions are deleted (one with a launch stays)")
|
|
76
136
|
.action(async (id) => {
|
|
77
137
|
try {
|
|
78
|
-
formatOutput(await client.post(`/song-reels/${id}/
|
|
138
|
+
formatOutput(await client.post(`/song-reels/${id}/keep`), getFormat(program.opts()));
|
|
79
139
|
}
|
|
80
140
|
catch (error) {
|
|
81
141
|
handleError(error);
|
package/dist/server.js
CHANGED
|
@@ -21,6 +21,7 @@ import { registerTrialReelTools } from "./tools/trial-reels.js";
|
|
|
21
21
|
import { registerReportTools, registerWorkflowPrompts } from "./tools/reports.js";
|
|
22
22
|
import { registerSongTools } from "./tools/songs.js";
|
|
23
23
|
import { registerSongReelTools } from "./tools/song-reels.js";
|
|
24
|
+
import { registerSceneClipTools } from "./tools/scene-clips.js";
|
|
24
25
|
/** Drops tools whose feature is paused before the policy registrar sees them (see PAUSED_TOOLS). */
|
|
25
26
|
function skipPausedTools(registrar) {
|
|
26
27
|
return {
|
|
@@ -54,6 +55,7 @@ export function createDropspaceServer({ client, version, profile }) {
|
|
|
54
55
|
registerReportTools(registrar, client);
|
|
55
56
|
registerSongTools(registrar, client);
|
|
56
57
|
registerSongReelTools(registrar, client);
|
|
58
|
+
registerSceneClipTools(registrar, client);
|
|
57
59
|
// Prompts are not tools, so the policy registrar does not filter them: the hosted profile
|
|
58
60
|
// (frozen for the Meta review) gets none.
|
|
59
61
|
if (profile === "full")
|
package/dist/tool-policy.js
CHANGED
|
@@ -148,6 +148,14 @@ const FULL_TOOL_POLICY = {
|
|
|
148
148
|
get_song_reel: localOnly("media", "read"),
|
|
149
149
|
launch_song_reel: localOnly("media", "write"),
|
|
150
150
|
delete_song_reel: localOnly("media", "write"),
|
|
151
|
+
rollout_song_reels: localOnly("media", "write"),
|
|
152
|
+
keep_song_reel: localOnly("media", "write"),
|
|
153
|
+
// scene clips: a job spends server minutes and adds up to 40 items to the media library
|
|
154
|
+
cut_scene_clips: localOnly("media", "write"),
|
|
155
|
+
list_scene_clip_jobs: localOnly("media", "read"),
|
|
156
|
+
get_scene_clip_job: localOnly("media", "read"),
|
|
157
|
+
retry_scene_clip_job: localOnly("media", "write"),
|
|
158
|
+
delete_scene_clip_job: localOnly("media", "write"),
|
|
151
159
|
get_usage: hosted("usage", "read"),
|
|
152
160
|
get_changelog: hosted("usage", "read"),
|
|
153
161
|
get_dropspace_status: hosted("dropspace", "read"),
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { DropspaceClient } from "../client.js";
|
|
3
|
+
/** Scene clips: a long video cut into short clips for the media library (pro and premium). Local package only. */
|
|
4
|
+
export declare function registerSceneClipTools(server: McpServer, client: DropspaceClient): void;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
function text(result) {
|
|
3
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
4
|
+
}
|
|
5
|
+
function fail(error) {
|
|
6
|
+
return {
|
|
7
|
+
content: [{ type: "text", text: `error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
8
|
+
isError: true,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/** Scene clips: a long video cut into short clips for the media library (pro and premium). Local package only. */
|
|
12
|
+
export function registerSceneClipTools(server, client) {
|
|
13
|
+
server.tool("cut_scene_clips", "cut a long video into 2 to 8 second clips at its scene changes, up to 40 a job, each with a thumbnail, for the user's media library (footage for make_song_reel and autopilot). give exactly one source: media_library_id (a video from list_media_library), link_url (a YouTube link, up to 30 minutes), or video_url (a public mp4 or mov url, up to 512 MB). only the first 15 minutes are scanned. the work runs on the server: the job comes back pending; read it with get_scene_clip_job until ready (the clips are added to the media library then, source scene) or failed (error). pro and premium; at most 10 jobs a day and one running at a time.", {
|
|
14
|
+
media_library_id: z.string().uuid().optional().describe("a video in the media library"),
|
|
15
|
+
link_url: z.string().url().optional().describe("a YouTube link"),
|
|
16
|
+
video_url: z.string().url().optional().describe("a public video file url"),
|
|
17
|
+
}, async (body) => {
|
|
18
|
+
try {
|
|
19
|
+
return text(await client.post("/scene-clips", body));
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
return fail(error);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
server.tool("list_scene_clip_jobs", "the user's clip jobs, newest first: id, source (library, link, url), source_title, status (pending, running, ready, or failed with error), clips_made, clips_added (in the media library), clips (start, end, video_url, thumbnail_url, media_library_id), and a note when clips are waiting for room in the library.", {
|
|
26
|
+
page: z.number().int().min(1).optional(),
|
|
27
|
+
page_size: z.number().int().min(1).max(100).optional(),
|
|
28
|
+
}, async ({ page, page_size }) => {
|
|
29
|
+
try {
|
|
30
|
+
const params = {};
|
|
31
|
+
if (page)
|
|
32
|
+
params.page = String(page);
|
|
33
|
+
if (page_size)
|
|
34
|
+
params.page_size = String(page_size);
|
|
35
|
+
return text(await client.get("/scene-clips", params));
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
return fail(error);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
server.tool("get_scene_clip_job", "one clip job. reading a ready job adds its clips to the media library (as many as fit under the 500-item cap; the rest on a later read).", { job_id: z.string().uuid() }, async ({ job_id }) => {
|
|
42
|
+
try {
|
|
43
|
+
return text(await client.get(`/scene-clips/${job_id}`));
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
return fail(error);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
server.tool("retry_scene_clip_job", "cut a failed (or stuck) clip job again. three attempts in all.", { job_id: z.string().uuid() }, async ({ job_id }) => {
|
|
50
|
+
try {
|
|
51
|
+
return text(await client.post(`/scene-clips/${job_id}/retry`));
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
return fail(error);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
server.tool("delete_scene_clip_job", "delete a clip job. clips already in the media library stay (delete_media_library_item removes one).", { job_id: z.string().uuid() }, async ({ job_id }) => {
|
|
58
|
+
try {
|
|
59
|
+
await client.delete(`/scene-clips/${job_id}`);
|
|
60
|
+
return text({ id: job_id, deleted: true });
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
return fail(error);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
package/dist/tools/song-reels.js
CHANGED
|
@@ -10,17 +10,42 @@ function fail(error) {
|
|
|
10
10
|
}
|
|
11
11
|
const STYLES = ["lower-third", "center-bold", "karaoke", "one-word", "caps-top"];
|
|
12
12
|
const BACKGROUNDS = ["midnight", "sunset", "forest", "sand", "slate", "violet"];
|
|
13
|
+
const REEL_INPUT = {
|
|
14
|
+
song_id: z.string().uuid(),
|
|
15
|
+
snippet_start: z.number().min(0).describe("seconds from the start of the song"),
|
|
16
|
+
snippet_end: z.number().positive().describe("5 to 30 seconds after snippet_start"),
|
|
17
|
+
style: z
|
|
18
|
+
.enum(STYLES)
|
|
19
|
+
.describe("how the lyrics look: lower-third, center-bold, karaoke (word by word), one-word, caps-top"),
|
|
20
|
+
footage_mode: z.enum(["clips", "none", "ai"]),
|
|
21
|
+
clip_ids: z
|
|
22
|
+
.array(z.string().uuid())
|
|
23
|
+
.max(20)
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("media library ids, in order (footage_mode clips)"),
|
|
26
|
+
background: z.enum(BACKGROUNDS).optional().describe("the gradient (footage_mode none); midnight unless given"),
|
|
27
|
+
hook_text: z
|
|
28
|
+
.string()
|
|
29
|
+
.min(1)
|
|
30
|
+
.max(90)
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("a line burned in for the whole snippet (top; low under caps-top); it also opens the launch caption"),
|
|
33
|
+
ai_prompt: z
|
|
34
|
+
.string()
|
|
35
|
+
.max(500)
|
|
36
|
+
.optional()
|
|
37
|
+
.describe("what the b-roll should show (footage_mode ai); the lyrics stand in when empty"),
|
|
38
|
+
ai_clips: z
|
|
39
|
+
.number()
|
|
40
|
+
.int()
|
|
41
|
+
.min(1)
|
|
42
|
+
.max(4)
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("b-roll clips to generate, 1-4 (footage_mode ai); 2 unless given"),
|
|
45
|
+
};
|
|
13
46
|
/** Lyric reels made from songs (pro and premium). Local package only. */
|
|
14
47
|
export function registerSongReelTools(server, client) {
|
|
15
|
-
server.tool("make_song_reel", "make a 9:16 lyric reel from a snippet of a ready song (get_song for its duration and peaks): 5 to 30 seconds of the song under footage, with the lyrics inside the snippet burned in. footage_mode clips uses the user's media library videos and images (clip_ids from list_media_library, in order; the clip changes at lyric lines, skipping cuts closer than 1.5 s apart); none puts the lyrics over a gradient (background). the lyrics are read from the song on the first reel (takes up to a minute). the render runs on the server: the reel comes back pending; read it with get_song_reel until ready (video_url, and it is in the media library) or failed. counts against the plan's song reels per billing period (pro 30, premium 100; a rendered reel keeps its slot even when deleted).", {
|
|
16
|
-
song_id: z.string().uuid(),
|
|
17
|
-
snippet_start: z.number().min(0).describe("seconds from the start of the song"),
|
|
18
|
-
snippet_end: z.number().positive().describe("5 to 30 seconds after snippet_start"),
|
|
19
|
-
style: z.enum(STYLES).describe("how the lyrics look: lower-third, center-bold, karaoke (word by word), one-word, caps-top"),
|
|
20
|
-
footage_mode: z.enum(["clips", "none"]),
|
|
21
|
-
clip_ids: z.array(z.string().uuid()).max(20).optional().describe("media library ids, in order (footage_mode clips)"),
|
|
22
|
-
background: z.enum(BACKGROUNDS).optional().describe("the gradient (footage_mode none); midnight unless given"),
|
|
23
|
-
}, async ({ song_id, ...body }) => {
|
|
48
|
+
server.tool("make_song_reel", "make a 9:16 lyric reel from a snippet of a ready song (get_song for its duration and peaks): 5 to 30 seconds of the song under footage, with the lyrics inside the snippet burned in and, if given, a hook line held across the whole snippet. footage_mode clips uses the user's media library videos and images (clip_ids from list_media_library, in order; the clip changes at lyric lines, skipping cuts closer than 1.5 s apart); none puts the lyrics over a gradient (background); ai generates b-roll from ai_prompt (ai_clips veo clips of 8 s, each one of the plan's AI videos at $1.20; the reel renders once they land, footage_status generating until then). the lyrics are read from the song on the first reel (takes up to a minute). the render runs on the server: the reel comes back pending; read it with get_song_reel until ready (video_url, and it is in the media library) or failed. counts against the plan's song reels per billing period (pro 30, premium 100; a rendered reel keeps its slot even when deleted).", REEL_INPUT, async ({ song_id, ...body }) => {
|
|
24
49
|
try {
|
|
25
50
|
return text(await client.post(`/songs/${song_id}/reels`, body));
|
|
26
51
|
}
|
|
@@ -28,15 +53,35 @@ export function registerSongReelTools(server, client) {
|
|
|
28
53
|
return fail(error);
|
|
29
54
|
}
|
|
30
55
|
});
|
|
31
|
-
server.tool("
|
|
56
|
+
server.tool("rollout_song_reels", "make count (2-10) versions of one reel at once, as one rollout: the same snippet, lines, and footage, the style and then the clip order (or background) changing per version, never two the same. every version is a reel of its own (rollout_id, variant_index, variant_note) that counts against the plan's song reels; when fewer slots are left than count, nothing is made and the error says how many are left. the versions render in turn. then keep_song_reel keeps the best, or launch_song_reel each one (trial_reel true for Instagram trial reels). an ai rollout generates its b-roll once and shares it.", {
|
|
57
|
+
...REEL_INPUT,
|
|
58
|
+
count: z.number().int().min(2).max(10).describe("how many versions"),
|
|
59
|
+
styles: z
|
|
60
|
+
.array(z.enum(STYLES))
|
|
61
|
+
.min(1)
|
|
62
|
+
.max(5)
|
|
63
|
+
.optional()
|
|
64
|
+
.describe("the styles the versions cycle through; all five when omitted"),
|
|
65
|
+
}, async ({ song_id, ...body }) => {
|
|
66
|
+
try {
|
|
67
|
+
return text(await client.post(`/songs/${song_id}/reels/rollout`, body));
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
return fail(error);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
server.tool("list_song_reels", "the user's lyric reels, newest first: id, song_id, title, snippet, style, footage_mode, hook_text, rollout_id / variant_index / variant_note (a rollout's versions), footage_status (ai b-roll: generating, ready, failed), render_status (pending, running, ready, or failed with render_error), video_url and thumbnail_url once ready, media_library_id, launch_id. song_id narrows to one song, rollout_id to one rollout's versions.", {
|
|
32
74
|
song_id: z.string().uuid().optional(),
|
|
75
|
+
rollout_id: z.string().uuid().optional(),
|
|
33
76
|
page: z.number().int().min(1).optional(),
|
|
34
77
|
page_size: z.number().int().min(1).max(100).optional(),
|
|
35
|
-
}, async ({ song_id, page, page_size }) => {
|
|
78
|
+
}, async ({ song_id, rollout_id, page, page_size }) => {
|
|
36
79
|
try {
|
|
37
80
|
const params = {};
|
|
38
81
|
if (song_id)
|
|
39
82
|
params.song_id = song_id;
|
|
83
|
+
if (rollout_id)
|
|
84
|
+
params.rollout_id = rollout_id;
|
|
40
85
|
if (page)
|
|
41
86
|
params.page = String(page);
|
|
42
87
|
if (page_size)
|
|
@@ -55,9 +100,17 @@ export function registerSongReelTools(server, client) {
|
|
|
55
100
|
return fail(error);
|
|
56
101
|
}
|
|
57
102
|
});
|
|
58
|
-
server.tool("launch_song_reel", "make a draft launch from a ready reel: the reel is its video and the snippet's lyrics its instagram caption. the launch stays a draft until the user (or update_launch + publish) sets platforms, caption, and timing, and it counts as one of the plan's launches. returns the reel with launch_id.", { reel_id: z.string().uuid() }, async ({ reel_id }) => {
|
|
103
|
+
server.tool("launch_song_reel", "make a draft launch from a ready reel: the reel is its video and the hook line plus the snippet's lyrics its instagram caption. trial_reel true makes it an Instagram trial reel (shown to non-followers first; refused with the reason when the account is not a professional account with 200 or more followers). the launch stays a draft until the user (or update_launch + publish) sets platforms, caption, and timing, and it counts as one of the plan's launches. returns the reel with launch_id.", { reel_id: z.string().uuid(), trial_reel: z.boolean().optional() }, async ({ reel_id, trial_reel }) => {
|
|
104
|
+
try {
|
|
105
|
+
return text(await client.post(`/song-reels/${reel_id}/launch`, trial_reel ? { trial_reel: true } : {}));
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
return fail(error);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
server.tool("keep_song_reel", "keep this version of a rollout: the rollout's other versions are deleted with their media library items and files (a version that already has a launch stays). returns the kept reel.", { reel_id: z.string().uuid() }, async ({ reel_id }) => {
|
|
59
112
|
try {
|
|
60
|
-
return text(await client.post(`/song-reels/${reel_id}/
|
|
113
|
+
return text(await client.post(`/song-reels/${reel_id}/keep`));
|
|
61
114
|
}
|
|
62
115
|
catch (error) {
|
|
63
116
|
return fail(error);
|