@hubfluencer/mcp 0.2.0 → 0.3.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/dist/index.js +106 -16
- package/package.json +1 -1
- package/src/index.ts +278 -24
- package/src/uploads.ts +25 -0
package/dist/index.js
CHANGED
|
@@ -29822,6 +29822,14 @@ async function uploadImageFile(client, presignPath, filePath, maxBytes = MAX_IMA
|
|
|
29822
29822
|
await putToPresignedUrl(presigned_url, buf, file.mime);
|
|
29823
29823
|
return { s3_key };
|
|
29824
29824
|
}
|
|
29825
|
+
async function uploadShortPoster(client, slug, filePath) {
|
|
29826
|
+
const file = await resolveReadPath(filePath, IMAGE_EXT_MIME, MAX_IMAGE_BYTES);
|
|
29827
|
+
const presign = await client.post(`/shorts/${slug}/poster/presign`, { content_type: file.mime });
|
|
29828
|
+
const { upload_url, s3_key } = presign;
|
|
29829
|
+
const buf = await readFile(file.path);
|
|
29830
|
+
await putToPresignedUrl(upload_url, buf, file.mime);
|
|
29831
|
+
return { s3_key };
|
|
29832
|
+
}
|
|
29825
29833
|
|
|
29826
29834
|
// src/index.ts
|
|
29827
29835
|
async function fetchStatus(client, kind, slug) {
|
|
@@ -29968,7 +29976,7 @@ var WRITE = {
|
|
|
29968
29976
|
destructiveHint: false,
|
|
29969
29977
|
openWorldHint: true
|
|
29970
29978
|
};
|
|
29971
|
-
var server = new McpServer({ name: "hubfluencer", version: "0.
|
|
29979
|
+
var server = new McpServer({ name: "hubfluencer", version: "0.3.0" });
|
|
29972
29980
|
var registerTool = server.registerTool.bind(server);
|
|
29973
29981
|
async function pollToTerminal(client, kind, slug, extra, budgetMs, intervalMs) {
|
|
29974
29982
|
const deadline = Date.now() + budgetMs;
|
|
@@ -30009,13 +30017,17 @@ async function pollSegmentToTerminal(client, slug, segmentId, extra, budgetMs, i
|
|
|
30009
30017
|
}
|
|
30010
30018
|
registerTool("make_video", {
|
|
30011
30019
|
title: "Make a video from a prompt (one shot)",
|
|
30012
|
-
description: "The simplest path: give a prompt, get a finished MP4. Creates the project (free), PRICES it against " + "your live credit balance, then — only if it's affordable and within max_credits — starts generation, " + "polls to completion (emitting progress), and (if save_path is given) downloads the result. " + "Spends credits (15 for a short; a multi-scene editor ad ~28). Pass dry_run:true to preview the cost " + "WITHOUT charging (returns {estimated_credits, available_credits, slug}); pass max_credits to cap the " + "spend. kind defaults to 'auto' — a multi-scene editor ad for ad/promo/story briefs, a single-clip short " + "for simple/short ones; the chosen kind is reported back as kind_inferred. If it returns terminal=false " + "the render is still running — call wait_for_completion with the returned slug to finish.",
|
|
30020
|
+
description: "The simplest path: give a prompt, get a finished MP4. Creates the project (free), PRICES it against " + "your live credit balance, then — only if it's affordable and within max_credits — starts generation, " + "polls to completion (emitting progress), and (if save_path is given) downloads the result. " + "Spends credits (15 for a short; a multi-scene editor ad ~28). Pass dry_run:true to preview the cost " + "WITHOUT charging (returns {estimated_credits, available_credits, slug}); pass max_credits to cap the " + "spend. kind defaults to 'auto' — a multi-scene editor ad for ad/promo/story briefs, a single-clip short " + "for simple/short ones; the chosen kind is reported back as kind_inferred. If it returns terminal=false " + "the render is still running — call wait_for_completion with the returned slug to finish. " + "BE PROACTIVE WITH BRANDING: pass headline (the on-screen TITLE) and subheadline (secondary title) plus " + "music_vibe/theme so a one-shot short isn't bare — these apply to SHORTS (music_vibe/headline/subheadline " + "are ignored for editor; theme applies to both). For richer branding — a product image, brand logo, or " + "closing card — drive the granular path instead: create_short + set_short_product/set_short_poster, or " + "create_editor_draft + set_product/set_logo/set_closing_image, then start_autopilot/generate_short.",
|
|
30013
30021
|
inputSchema: {
|
|
30014
30022
|
prompt: exports_external.string().describe("What the ad/video should be about (min 10 chars)"),
|
|
30015
30023
|
kind: exports_external.string().optional().describe("'short' (fast, 1 clip), 'editor' (multi-scene), or 'auto' (default — inferred)"),
|
|
30016
30024
|
language: exports_external.string().optional().describe('Language code, e.g. "en" (default)'),
|
|
30017
30025
|
aspect: exports_external.string().optional().describe("Aspect ratio for editor: 9:16 (default), 16:9, or 1:1"),
|
|
30018
30026
|
voice_id: exports_external.string().optional().describe("Preferred narration voice id for editor ads (see list_voices); shorts have no voiceover"),
|
|
30027
|
+
headline: exports_external.string().optional().describe("SHORTS only: the on-screen TITLE overlay (≤160). Set this so the short isn't bare."),
|
|
30028
|
+
subheadline: exports_external.string().optional().describe("SHORTS only: the secondary title / supporting line (≤200)"),
|
|
30029
|
+
music_vibe: exports_external.string().optional().describe("SHORTS only: music mood — Upbeat (default), Cinematic, Minimal, Luxury, Playful, Jazz"),
|
|
30030
|
+
theme: exports_external.string().optional().describe("Visual theme (both kinds): none (literal — no imposed style), realistic (default), cinematic, " + "anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, " + "minimalist, cyberpunk"),
|
|
30019
30031
|
save_path: exports_external.string().optional().describe("Optional .mp4 path to download to (confined to HUBFLUENCER_OUTPUT_DIR or cwd)"),
|
|
30020
30032
|
max_wait_seconds: exports_external.number().optional().describe("Block budget seconds (default 240, capped 10–280)"),
|
|
30021
30033
|
dry_run: exports_external.boolean().optional().describe("Preview only: create a free draft, price it, and STOP before spending credits. " + "Returns {estimated_credits, available_credits, slug}. Resume with generate_short / start_autopilot."),
|
|
@@ -30034,15 +30046,23 @@ registerTool("make_video", {
|
|
|
30034
30046
|
resolveSavePath(args.save_path);
|
|
30035
30047
|
let slug;
|
|
30036
30048
|
if (kind === "short") {
|
|
30037
|
-
const created = await client.post("/shorts", {
|
|
30049
|
+
const created = await client.post("/shorts", {
|
|
30050
|
+
product_prompt: args.prompt,
|
|
30051
|
+
language: args.language,
|
|
30052
|
+
headline: args.headline,
|
|
30053
|
+
subheadline: args.subheadline,
|
|
30054
|
+
music_vibe: args.music_vibe,
|
|
30055
|
+
theme: args.theme
|
|
30056
|
+
}, idemKey("make-short", args.prompt, args.language ?? "", args.headline ?? "", args.subheadline ?? "", args.music_vibe ?? "", args.theme ?? ""));
|
|
30038
30057
|
slug = created.data.slug;
|
|
30039
30058
|
} else {
|
|
30040
30059
|
const created = await client.post("/editor", {
|
|
30041
30060
|
language: args.language ?? "en",
|
|
30042
30061
|
product_prompt: args.prompt,
|
|
30043
30062
|
export_aspect_ratio: args.aspect,
|
|
30044
|
-
voice_id: args.voice_id
|
|
30045
|
-
|
|
30063
|
+
voice_id: args.voice_id,
|
|
30064
|
+
theme: args.theme
|
|
30065
|
+
}, idemKey("make-editor", args.prompt, args.language ?? "en", args.aspect ?? "", args.voice_id ?? "", args.theme ?? ""));
|
|
30046
30066
|
slug = created.data.slug;
|
|
30047
30067
|
}
|
|
30048
30068
|
const costPath = kind === "short" ? `/shorts/${slug}/cost` : `/editor/${slug}/autopilot/cost`;
|
|
@@ -30101,7 +30121,17 @@ registerTool("get_credits", {
|
|
|
30101
30121
|
description: "Returns the authenticated account's credit balance. A short costs 15 credits.",
|
|
30102
30122
|
inputSchema: {},
|
|
30103
30123
|
annotations: { title: "Get credits", ...RO }
|
|
30104
|
-
}, tool(async (_args, client) =>
|
|
30124
|
+
}, tool(async (_args, client) => {
|
|
30125
|
+
const res = await client.get("/studio/credits");
|
|
30126
|
+
const d = asRecord(asRecord(res).data);
|
|
30127
|
+
return ok({
|
|
30128
|
+
credits: d.credits,
|
|
30129
|
+
spendable_credits: d.spendable_credits,
|
|
30130
|
+
reserved_credits: d.reserved_credits,
|
|
30131
|
+
remaining_reserved_credits: d.remaining_reserved_credits,
|
|
30132
|
+
message: d.message
|
|
30133
|
+
});
|
|
30134
|
+
}));
|
|
30105
30135
|
registerTool("list_voices", {
|
|
30106
30136
|
title: "List voices",
|
|
30107
30137
|
description: "Lists available narration voices (id + name). Pass a voice id as voice_id to create_editor_ad / " + "make_video to pick the narration voice for an editor ad. Shorts have no voiceover.",
|
|
@@ -30141,19 +30171,48 @@ registerTool("list_projects", {
|
|
|
30141
30171
|
}));
|
|
30142
30172
|
registerTool("create_short", {
|
|
30143
30173
|
title: "Create a short (draft)",
|
|
30144
|
-
description: "Creates a short draft from a product prompt (min 10 chars). Returns the slug
|
|
30174
|
+
description: "Creates a short draft from a product prompt (min 10 chars). A short is a 12s vertical (two 6s AI " + "segments + an on-screen title overlay + music; 14s when an end-card poster is set). Returns the slug, " + "costs 0 credits. Follow with generate_short to render. " + "BE PROACTIVE: don't ship a bare clip — set a headline (the on-screen TITLE) and subheadline (secondary " + "title), and pick a music_vibe and theme that fit the brand. To brand it further, attach a product image " + "(set_short_product) or an end-card poster (set_short_poster) before generate_short. (Shorts have no logo " + "overlay — that's an editor-only feature; use create_editor_ad for logo branding.)",
|
|
30145
30175
|
inputSchema: {
|
|
30146
30176
|
product_prompt: exports_external.string().min(10).describe("What the short should be about"),
|
|
30147
30177
|
language: exports_external.string().optional().describe('Language code, e.g. "en" (default)'),
|
|
30148
|
-
|
|
30149
|
-
|
|
30150
|
-
|
|
30151
|
-
music_vibe: exports_external.string().optional()
|
|
30178
|
+
headline: exports_external.string().max(160).optional().describe("The on-screen TITLE composited over the short (poster text overlay). ≤160 chars. Set this."),
|
|
30179
|
+
subheadline: exports_external.string().max(200).optional().describe("The SECONDARY title / supporting line under the headline. ≤200 chars."),
|
|
30180
|
+
theme: exports_external.string().optional().describe("Visual theme (default realistic). One of: none (no imposed style — follows the prompt literally), " + "realistic, cinematic, anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, " + "gaming, retro_80s, minimalist, cyberpunk."),
|
|
30181
|
+
music_vibe: exports_external.string().optional().describe("Background-music mood. Recognized: Upbeat (default), Cinematic, Minimal, Luxury, Playful, Jazz."),
|
|
30182
|
+
music_instruments: exports_external.array(exports_external.string()).optional().describe('Optional instrument hints, e.g. ["piano", "strings"]'),
|
|
30183
|
+
text_position: exports_external.enum(["top", "center", "bottom"]).optional().describe("Where the title overlay sits (default bottom)"),
|
|
30184
|
+
text_animation: exports_external.enum(["reveal", "typewriter", "fade_in", "pop", "bounce"]).optional().describe("Title overlay animation (default fade_in)"),
|
|
30185
|
+
font_family: exports_external.string().optional().describe("Overlay font (default ShortFontSpaceGrotesk). One of: ShortFontSpaceGrotesk, ShortFontMontserrat, " + "ShortFontTheBold, ShortFontImpact, ShortFontLato, ShortFontProximaNova, ShortFontAnton, " + "ShortFontBebasNeue, ShortFontOswald, ShortFontArchivoBlack, ShortFontPoppins, ShortFontInter, " + "ShortFontTikTokSans, ShortFontBangers.")
|
|
30152
30186
|
},
|
|
30153
30187
|
annotations: { title: "Create short", ...WRITE, idempotentHint: true }
|
|
30154
30188
|
}, tool(async (args, client) => {
|
|
30155
|
-
const
|
|
30156
|
-
|
|
30189
|
+
const body = {
|
|
30190
|
+
product_prompt: args.product_prompt
|
|
30191
|
+
};
|
|
30192
|
+
if (args.language !== undefined)
|
|
30193
|
+
body.language = args.language;
|
|
30194
|
+
if (args.headline !== undefined)
|
|
30195
|
+
body.headline = args.headline;
|
|
30196
|
+
if (args.subheadline !== undefined)
|
|
30197
|
+
body.subheadline = args.subheadline;
|
|
30198
|
+
if (args.theme !== undefined)
|
|
30199
|
+
body.theme = args.theme;
|
|
30200
|
+
if (args.music_vibe !== undefined)
|
|
30201
|
+
body.music_vibe = args.music_vibe;
|
|
30202
|
+
if (args.music_instruments !== undefined)
|
|
30203
|
+
body.music_instruments = args.music_instruments;
|
|
30204
|
+
if (args.text_position !== undefined)
|
|
30205
|
+
body.short_text_position = args.text_position;
|
|
30206
|
+
if (args.text_animation !== undefined)
|
|
30207
|
+
body.short_text_animation = args.text_animation;
|
|
30208
|
+
if (args.font_family !== undefined)
|
|
30209
|
+
body.short_font_family = args.font_family;
|
|
30210
|
+
const res = await client.post("/shorts", body, idemKey("create-short", args.product_prompt, args.language ?? "", args.headline ?? "", args.subheadline ?? ""));
|
|
30211
|
+
return ok({
|
|
30212
|
+
slug: res.data.slug,
|
|
30213
|
+
kind: "short",
|
|
30214
|
+
next: "set_short_product / set_short_poster (optional branding), then generate_short"
|
|
30215
|
+
});
|
|
30157
30216
|
}));
|
|
30158
30217
|
registerTool("generate_short", {
|
|
30159
30218
|
title: "Generate (render) a short",
|
|
@@ -30167,11 +30226,11 @@ registerTool("generate_short", {
|
|
|
30167
30226
|
}));
|
|
30168
30227
|
registerTool("create_editor_ad", {
|
|
30169
30228
|
title: "Create a multi-scene editor ad (autopilot)",
|
|
30170
|
-
description: "Creates an editor project from a product prompt and starts autopilot (server-orchestrated " + "scenario → segments → narration → voice → music → render). Each AI-generated scene is a fixed 8 seconds. " + "Returns the slug. Costs credits. Then poll with get_status / wait_for_completion (kind=editor). Prefer " + "make_video for the one-shot path.",
|
|
30229
|
+
description: "Creates an editor project from a product prompt and starts autopilot (server-orchestrated " + "scenario → segments → narration → voice → music → render). Each AI-generated scene is a fixed 8 seconds. " + "Returns the slug. Costs credits. Then poll with get_status / wait_for_completion (kind=editor). Prefer " + "make_video for the one-shot path. " + "BRANDING: to attach the user's own product image, brand logo, or closing card, create the draft with " + "create_editor_draft first, call set_product / set_logo / set_closing_image (all 0 credits), then " + "start_autopilot — autopilot weaves them in. (Editor ads carry their copy in-scene/narration, not as a " + "title overlay; for an on-screen title/subtitle use a short with headline/subheadline instead.)",
|
|
30171
30230
|
inputSchema: {
|
|
30172
30231
|
product_prompt: exports_external.string().min(10).describe("Brief for the ad (min 10 chars)"),
|
|
30173
30232
|
language: exports_external.string().optional().describe('Language code, e.g. "en" (default)'),
|
|
30174
|
-
theme: exports_external.string().optional().describe('Visual theme (default "realistic")'),
|
|
30233
|
+
theme: exports_external.string().optional().describe('Visual theme (default "realistic"). One of: none (no imposed style — segments follow your prompts literally), realistic, cinematic, anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, minimalist, cyberpunk.'),
|
|
30175
30234
|
voice_id: exports_external.string().optional().describe("Preferred narration voice id (see list_voices); omit for the default voice"),
|
|
30176
30235
|
export_aspect_ratio: exports_external.enum(["9:16", "16:9", "1:1"]).optional().describe('Aspect ratio (default "9:16")')
|
|
30177
30236
|
},
|
|
@@ -30226,7 +30285,7 @@ registerTool("create_editor_draft", {
|
|
|
30226
30285
|
inputSchema: {
|
|
30227
30286
|
product_prompt: exports_external.string().min(10).max(5000).optional().describe("Brief for the ad — 10–5000 chars, or omit entirely"),
|
|
30228
30287
|
language: exports_external.string().min(2).max(10).optional().describe('Language code, e.g. "en" (default)'),
|
|
30229
|
-
theme: exports_external.string().optional().describe('Visual theme (default "realistic")'),
|
|
30288
|
+
theme: exports_external.string().optional().describe('Visual theme (default "realistic"). One of: none (no imposed style — segments follow your prompts literally), realistic, cinematic, anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, minimalist, cyberpunk.'),
|
|
30230
30289
|
voice_id: exports_external.string().regex(/^[A-Za-z0-9_-]+$/).max(64).optional().describe("Preferred narration voice id (see list_voices); ≤64 chars, [A-Za-z0-9_-] only"),
|
|
30231
30290
|
export_aspect_ratio: exports_external.enum(["9:16", "16:9", "1:1"]).optional().describe('Aspect ratio (default "9:16")'),
|
|
30232
30291
|
project_intent: exports_external.enum(["social_ad", "creative_story"]).optional().describe("Project intent (optional)")
|
|
@@ -30798,6 +30857,37 @@ registerTool("set_logo", {
|
|
|
30798
30857
|
}
|
|
30799
30858
|
return ok(result);
|
|
30800
30859
|
}));
|
|
30860
|
+
registerTool("set_short_product", {
|
|
30861
|
+
title: "Attach a product image to a short (0 credits)",
|
|
30862
|
+
description: "Uploads a local product image and attaches it to a SHORT so the product is woven into the footage. " + "Accepted: " + IMAGE_EXTS.map((e) => `.${e}`).join(", ") + " (≤20 MB), local path confined to HUBFLUENCER_INPUT_DIR (or cwd). 0 credits. Optional description " + "(≤500 chars) guides how it's featured. Attach BEFORE generate_short so the product is part of the scenes. " + "(This is the shorts equivalent of set_product for editor projects.)",
|
|
30863
|
+
inputSchema: {
|
|
30864
|
+
slug: exports_external.string().describe("Short slug (from create_short)"),
|
|
30865
|
+
file_path: exports_external.string().describe("Local product image path (.jpg/.jpeg/.png)"),
|
|
30866
|
+
description: exports_external.string().max(500).optional().describe("Product description (≤500 chars)")
|
|
30867
|
+
},
|
|
30868
|
+
annotations: {
|
|
30869
|
+
title: "Set short product",
|
|
30870
|
+
...WRITE,
|
|
30871
|
+
idempotentHint: false
|
|
30872
|
+
}
|
|
30873
|
+
}, tool(async (args, client) => {
|
|
30874
|
+
const { s3_key } = await uploadImageFile(client, `/shorts/${args.slug}/product/presign`, args.file_path);
|
|
30875
|
+
const res = await client.post(`/shorts/${args.slug}/product/confirm`, { s3_key, product_description: args.description });
|
|
30876
|
+
return ok(asRecord(res).data ?? res);
|
|
30877
|
+
}));
|
|
30878
|
+
registerTool("set_short_poster", {
|
|
30879
|
+
title: "Set a short's end-card poster image (0 credits)",
|
|
30880
|
+
description: "Uploads a local image as the SHORT's end-card poster — a closing still shown at the end (extends the " + "render from 12s to 14s). Accepted: " + IMAGE_EXTS.map((e) => `.${e}`).join(", ") + " (≤20 MB), confined to HUBFLUENCER_INPUT_DIR/cwd. 0 credits. There is one poster per short; calling " + "again replaces it. (Shorts have no logo overlay — for a brand logo use an editor project + set_logo.)",
|
|
30881
|
+
inputSchema: {
|
|
30882
|
+
slug: exports_external.string().describe("Short slug (from create_short)"),
|
|
30883
|
+
file_path: exports_external.string().describe("Local poster image (.jpg/.jpeg/.png)")
|
|
30884
|
+
},
|
|
30885
|
+
annotations: { title: "Set short poster", ...WRITE, idempotentHint: false }
|
|
30886
|
+
}, tool(async (args, client) => {
|
|
30887
|
+
const { s3_key } = await uploadShortPoster(client, args.slug, args.file_path);
|
|
30888
|
+
const res = await client.post(`/shorts/${args.slug}/poster/confirm`, { s3_key });
|
|
30889
|
+
return ok(asRecord(res).data ?? res);
|
|
30890
|
+
}));
|
|
30801
30891
|
registerTool("get_status", {
|
|
30802
30892
|
title: "Get generation status",
|
|
30803
30893
|
description: "Returns a normalized status {stage, terminal, ready, video_url, error} for a short or editor " + "project. ready:true means the post-ready MP4 is at video_url.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hubfluencer/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Model Context Protocol server for Hubfluencer — let AI agents generate post-ready shorts and editor ads.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Monocursive <contact@monocursive.com>",
|
package/src/index.ts
CHANGED
|
@@ -39,6 +39,7 @@ import { runLogin } from "./login.js";
|
|
|
39
39
|
import {
|
|
40
40
|
IMAGE_EXTS,
|
|
41
41
|
uploadImageFile,
|
|
42
|
+
uploadShortPoster,
|
|
42
43
|
uploadVideoFile,
|
|
43
44
|
VIDEO_EXTS,
|
|
44
45
|
} from "./uploads.js";
|
|
@@ -323,7 +324,7 @@ const WRITE = {
|
|
|
323
324
|
openWorldHint: true,
|
|
324
325
|
};
|
|
325
326
|
|
|
326
|
-
const server = new McpServer({ name: "hubfluencer", version: "0.
|
|
327
|
+
const server = new McpServer({ name: "hubfluencer", version: "0.3.0" });
|
|
327
328
|
|
|
328
329
|
// The SDK's `registerTool` is generic over the Zod input shape; with this many
|
|
329
330
|
// tools its conditional types hit TS2589 ("excessively deep"). Inputs are still
|
|
@@ -424,7 +425,12 @@ registerTool(
|
|
|
424
425
|
"WITHOUT charging (returns {estimated_credits, available_credits, slug}); pass max_credits to cap the " +
|
|
425
426
|
"spend. kind defaults to 'auto' — a multi-scene editor ad for ad/promo/story briefs, a single-clip short " +
|
|
426
427
|
"for simple/short ones; the chosen kind is reported back as kind_inferred. If it returns terminal=false " +
|
|
427
|
-
"the render is still running — call wait_for_completion with the returned slug to finish."
|
|
428
|
+
"the render is still running — call wait_for_completion with the returned slug to finish. " +
|
|
429
|
+
"BE PROACTIVE WITH BRANDING: pass headline (the on-screen TITLE) and subheadline (secondary title) plus " +
|
|
430
|
+
"music_vibe/theme so a one-shot short isn't bare — these apply to SHORTS (music_vibe/headline/subheadline " +
|
|
431
|
+
"are ignored for editor; theme applies to both). For richer branding — a product image, brand logo, or " +
|
|
432
|
+
"closing card — drive the granular path instead: create_short + set_short_product/set_short_poster, or " +
|
|
433
|
+
"create_editor_draft + set_product/set_logo/set_closing_image, then start_autopilot/generate_short.",
|
|
428
434
|
// Schema kept intentionally flat (no .min/.max/.int chains) — the SDK's
|
|
429
435
|
// generic inference on registerTool hits TS2589 ("excessively deep") on
|
|
430
436
|
// larger schemas with chained validators. Ranges are enforced in code.
|
|
@@ -452,6 +458,30 @@ registerTool(
|
|
|
452
458
|
.describe(
|
|
453
459
|
"Preferred narration voice id for editor ads (see list_voices); shorts have no voiceover",
|
|
454
460
|
),
|
|
461
|
+
headline: z
|
|
462
|
+
.string()
|
|
463
|
+
.optional()
|
|
464
|
+
.describe(
|
|
465
|
+
"SHORTS only: the on-screen TITLE overlay (≤160). Set this so the short isn't bare.",
|
|
466
|
+
),
|
|
467
|
+
subheadline: z
|
|
468
|
+
.string()
|
|
469
|
+
.optional()
|
|
470
|
+
.describe("SHORTS only: the secondary title / supporting line (≤200)"),
|
|
471
|
+
music_vibe: z
|
|
472
|
+
.string()
|
|
473
|
+
.optional()
|
|
474
|
+
.describe(
|
|
475
|
+
"SHORTS only: music mood — Upbeat (default), Cinematic, Minimal, Luxury, Playful, Jazz",
|
|
476
|
+
),
|
|
477
|
+
theme: z
|
|
478
|
+
.string()
|
|
479
|
+
.optional()
|
|
480
|
+
.describe(
|
|
481
|
+
"Visual theme (both kinds): none (literal — no imposed style), realistic (default), cinematic, " +
|
|
482
|
+
"anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, " +
|
|
483
|
+
"minimalist, cyberpunk",
|
|
484
|
+
),
|
|
455
485
|
save_path: z
|
|
456
486
|
.string()
|
|
457
487
|
.optional()
|
|
@@ -486,6 +516,10 @@ registerTool(
|
|
|
486
516
|
language?: string;
|
|
487
517
|
aspect?: string;
|
|
488
518
|
voice_id?: string;
|
|
519
|
+
headline?: string;
|
|
520
|
+
subheadline?: string;
|
|
521
|
+
music_vibe?: string;
|
|
522
|
+
theme?: string;
|
|
489
523
|
save_path?: string;
|
|
490
524
|
max_wait_seconds?: number;
|
|
491
525
|
dry_run?: boolean;
|
|
@@ -515,13 +549,31 @@ registerTool(
|
|
|
515
549
|
// make_video (or a dry_run then a real run) reuses this same draft.
|
|
516
550
|
let slug: string;
|
|
517
551
|
if (kind === "short") {
|
|
552
|
+
// Carry the creative fields inline so a one-shot short can be titled
|
|
553
|
+
// and branded (headline/subheadline/music_vibe are short-only; theme
|
|
554
|
+
// applies to both kinds).
|
|
518
555
|
const created = await client.post<{ data: { slug: string } }>(
|
|
519
556
|
"/shorts",
|
|
520
|
-
{
|
|
557
|
+
{
|
|
558
|
+
product_prompt: args.prompt,
|
|
559
|
+
language: args.language,
|
|
560
|
+
headline: args.headline,
|
|
561
|
+
subheadline: args.subheadline,
|
|
562
|
+
music_vibe: args.music_vibe,
|
|
563
|
+
theme: args.theme,
|
|
564
|
+
},
|
|
521
565
|
// Fold every create-affecting param into the key: two calls that
|
|
522
|
-
// differ only in
|
|
523
|
-
// transport retry of an identical call must reuse it.
|
|
524
|
-
idemKey(
|
|
566
|
+
// differ only in a creative field must NOT dedup to the same draft,
|
|
567
|
+
// and a transport retry of an identical call must reuse it.
|
|
568
|
+
idemKey(
|
|
569
|
+
"make-short",
|
|
570
|
+
args.prompt,
|
|
571
|
+
args.language ?? "",
|
|
572
|
+
args.headline ?? "",
|
|
573
|
+
args.subheadline ?? "",
|
|
574
|
+
args.music_vibe ?? "",
|
|
575
|
+
args.theme ?? "",
|
|
576
|
+
),
|
|
525
577
|
);
|
|
526
578
|
slug = created.data.slug;
|
|
527
579
|
} else {
|
|
@@ -532,6 +584,7 @@ registerTool(
|
|
|
532
584
|
product_prompt: args.prompt,
|
|
533
585
|
export_aspect_ratio: args.aspect,
|
|
534
586
|
voice_id: args.voice_id,
|
|
587
|
+
theme: args.theme,
|
|
535
588
|
},
|
|
536
589
|
idemKey(
|
|
537
590
|
"make-editor",
|
|
@@ -539,6 +592,7 @@ registerTool(
|
|
|
539
592
|
args.language ?? "en",
|
|
540
593
|
args.aspect ?? "",
|
|
541
594
|
args.voice_id ?? "",
|
|
595
|
+
args.theme ?? "",
|
|
542
596
|
),
|
|
543
597
|
);
|
|
544
598
|
slug = created.data.slug;
|
|
@@ -677,7 +731,23 @@ registerTool(
|
|
|
677
731
|
inputSchema: {},
|
|
678
732
|
annotations: { title: "Get credits", ...RO },
|
|
679
733
|
},
|
|
680
|
-
tool(async (_args, client) =>
|
|
734
|
+
tool(async (_args, client) => {
|
|
735
|
+
const res = await client.get<{ data: unknown }>("/studio/credits");
|
|
736
|
+
const d = asRecord(asRecord(res).data);
|
|
737
|
+
// Whitelist generation-relevant fields only. Deliberately DROP
|
|
738
|
+
// `first_editor_video_pack_available`: it's a purchase-funnel flag for the
|
|
739
|
+
// PAID one-time starter pack (33 credits — not free), and this server has
|
|
740
|
+
// no billing scope, so it can't act on it. Passing it through only invited
|
|
741
|
+
// the agent to misread `true` as "your first video may be free", which
|
|
742
|
+
// contradicts the product (the first video is never free).
|
|
743
|
+
return ok({
|
|
744
|
+
credits: d.credits,
|
|
745
|
+
spendable_credits: d.spendable_credits,
|
|
746
|
+
reserved_credits: d.reserved_credits,
|
|
747
|
+
remaining_reserved_credits: d.remaining_reserved_credits,
|
|
748
|
+
message: d.message,
|
|
749
|
+
});
|
|
750
|
+
}),
|
|
681
751
|
);
|
|
682
752
|
|
|
683
753
|
// ── Voices ────────────────────────────────────────────────────────────────
|
|
@@ -770,8 +840,13 @@ registerTool(
|
|
|
770
840
|
{
|
|
771
841
|
title: "Create a short (draft)",
|
|
772
842
|
description:
|
|
773
|
-
"Creates a short draft from a product prompt (min 10 chars).
|
|
774
|
-
"
|
|
843
|
+
"Creates a short draft from a product prompt (min 10 chars). A short is a 12s vertical (two 6s AI " +
|
|
844
|
+
"segments + an on-screen title overlay + music; 14s when an end-card poster is set). Returns the slug, " +
|
|
845
|
+
"costs 0 credits. Follow with generate_short to render. " +
|
|
846
|
+
"BE PROACTIVE: don't ship a bare clip — set a headline (the on-screen TITLE) and subheadline (secondary " +
|
|
847
|
+
"title), and pick a music_vibe and theme that fit the brand. To brand it further, attach a product image " +
|
|
848
|
+
"(set_short_product) or an end-card poster (set_short_poster) before generate_short. (Shorts have no logo " +
|
|
849
|
+
"overlay — that's an editor-only feature; use create_editor_ad for logo branding.)",
|
|
775
850
|
inputSchema: {
|
|
776
851
|
product_prompt: z
|
|
777
852
|
.string()
|
|
@@ -781,21 +856,109 @@ registerTool(
|
|
|
781
856
|
.string()
|
|
782
857
|
.optional()
|
|
783
858
|
.describe('Language code, e.g. "en" (default)'),
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
859
|
+
headline: z
|
|
860
|
+
.string()
|
|
861
|
+
.max(160)
|
|
862
|
+
.optional()
|
|
863
|
+
.describe(
|
|
864
|
+
"The on-screen TITLE composited over the short (poster text overlay). ≤160 chars. Set this.",
|
|
865
|
+
),
|
|
866
|
+
subheadline: z
|
|
867
|
+
.string()
|
|
868
|
+
.max(200)
|
|
869
|
+
.optional()
|
|
870
|
+
.describe(
|
|
871
|
+
"The SECONDARY title / supporting line under the headline. ≤200 chars.",
|
|
872
|
+
),
|
|
873
|
+
theme: z
|
|
874
|
+
.string()
|
|
875
|
+
.optional()
|
|
876
|
+
.describe(
|
|
877
|
+
"Visual theme (default realistic). One of: none (no imposed style — follows the prompt literally), " +
|
|
878
|
+
"realistic, cinematic, anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, " +
|
|
879
|
+
"gaming, retro_80s, minimalist, cyberpunk.",
|
|
880
|
+
),
|
|
881
|
+
music_vibe: z
|
|
882
|
+
.string()
|
|
883
|
+
.optional()
|
|
884
|
+
.describe(
|
|
885
|
+
"Background-music mood. Recognized: Upbeat (default), Cinematic, Minimal, Luxury, Playful, Jazz.",
|
|
886
|
+
),
|
|
887
|
+
music_instruments: z
|
|
888
|
+
.array(z.string())
|
|
889
|
+
.optional()
|
|
890
|
+
.describe('Optional instrument hints, e.g. ["piano", "strings"]'),
|
|
891
|
+
text_position: z
|
|
892
|
+
.enum(["top", "center", "bottom"])
|
|
893
|
+
.optional()
|
|
894
|
+
.describe("Where the title overlay sits (default bottom)"),
|
|
895
|
+
text_animation: z
|
|
896
|
+
.enum(["reveal", "typewriter", "fade_in", "pop", "bounce"])
|
|
897
|
+
.optional()
|
|
898
|
+
.describe("Title overlay animation (default fade_in)"),
|
|
899
|
+
font_family: z
|
|
900
|
+
.string()
|
|
901
|
+
.optional()
|
|
902
|
+
.describe(
|
|
903
|
+
"Overlay font (default ShortFontSpaceGrotesk). One of: ShortFontSpaceGrotesk, ShortFontMontserrat, " +
|
|
904
|
+
"ShortFontTheBold, ShortFontImpact, ShortFontLato, ShortFontProximaNova, ShortFontAnton, " +
|
|
905
|
+
"ShortFontBebasNeue, ShortFontOswald, ShortFontArchivoBlack, ShortFontPoppins, ShortFontInter, " +
|
|
906
|
+
"ShortFontTikTokSans, ShortFontBangers.",
|
|
907
|
+
),
|
|
788
908
|
},
|
|
789
909
|
annotations: { title: "Create short", ...WRITE, idempotentHint: true },
|
|
790
910
|
},
|
|
791
|
-
tool(
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
911
|
+
tool(
|
|
912
|
+
async (
|
|
913
|
+
args: {
|
|
914
|
+
product_prompt: string;
|
|
915
|
+
language?: string;
|
|
916
|
+
headline?: string;
|
|
917
|
+
subheadline?: string;
|
|
918
|
+
theme?: string;
|
|
919
|
+
music_vibe?: string;
|
|
920
|
+
music_instruments?: string[];
|
|
921
|
+
text_position?: string;
|
|
922
|
+
text_animation?: string;
|
|
923
|
+
font_family?: string;
|
|
924
|
+
},
|
|
925
|
+
client,
|
|
926
|
+
) => {
|
|
927
|
+
// Map the agent-friendly field names onto the API's short_* params.
|
|
928
|
+
const body: Record<string, unknown> = {
|
|
929
|
+
product_prompt: args.product_prompt,
|
|
930
|
+
};
|
|
931
|
+
if (args.language !== undefined) body.language = args.language;
|
|
932
|
+
if (args.headline !== undefined) body.headline = args.headline;
|
|
933
|
+
if (args.subheadline !== undefined) body.subheadline = args.subheadline;
|
|
934
|
+
if (args.theme !== undefined) body.theme = args.theme;
|
|
935
|
+
if (args.music_vibe !== undefined) body.music_vibe = args.music_vibe;
|
|
936
|
+
if (args.music_instruments !== undefined)
|
|
937
|
+
body.music_instruments = args.music_instruments;
|
|
938
|
+
if (args.text_position !== undefined)
|
|
939
|
+
body.short_text_position = args.text_position;
|
|
940
|
+
if (args.text_animation !== undefined)
|
|
941
|
+
body.short_text_animation = args.text_animation;
|
|
942
|
+
if (args.font_family !== undefined)
|
|
943
|
+
body.short_font_family = args.font_family;
|
|
944
|
+
const res = await client.post<{ data: { slug: string } }>(
|
|
945
|
+
"/shorts",
|
|
946
|
+
body,
|
|
947
|
+
idemKey(
|
|
948
|
+
"create-short",
|
|
949
|
+
args.product_prompt,
|
|
950
|
+
args.language ?? "",
|
|
951
|
+
args.headline ?? "",
|
|
952
|
+
args.subheadline ?? "",
|
|
953
|
+
),
|
|
954
|
+
);
|
|
955
|
+
return ok({
|
|
956
|
+
slug: res.data.slug,
|
|
957
|
+
kind: "short",
|
|
958
|
+
next: "set_short_product / set_short_poster (optional branding), then generate_short",
|
|
959
|
+
});
|
|
960
|
+
},
|
|
961
|
+
),
|
|
799
962
|
);
|
|
800
963
|
|
|
801
964
|
registerTool(
|
|
@@ -829,7 +992,11 @@ registerTool(
|
|
|
829
992
|
"Creates an editor project from a product prompt and starts autopilot (server-orchestrated " +
|
|
830
993
|
"scenario → segments → narration → voice → music → render). Each AI-generated scene is a fixed 8 seconds. " +
|
|
831
994
|
"Returns the slug. Costs credits. Then poll with get_status / wait_for_completion (kind=editor). Prefer " +
|
|
832
|
-
"make_video for the one-shot path."
|
|
995
|
+
"make_video for the one-shot path. " +
|
|
996
|
+
"BRANDING: to attach the user's own product image, brand logo, or closing card, create the draft with " +
|
|
997
|
+
"create_editor_draft first, call set_product / set_logo / set_closing_image (all 0 credits), then " +
|
|
998
|
+
"start_autopilot — autopilot weaves them in. (Editor ads carry their copy in-scene/narration, not as a " +
|
|
999
|
+
"title overlay; for an on-screen title/subtitle use a short with headline/subheadline instead.)",
|
|
833
1000
|
inputSchema: {
|
|
834
1001
|
product_prompt: z
|
|
835
1002
|
.string()
|
|
@@ -842,7 +1009,9 @@ registerTool(
|
|
|
842
1009
|
theme: z
|
|
843
1010
|
.string()
|
|
844
1011
|
.optional()
|
|
845
|
-
.describe(
|
|
1012
|
+
.describe(
|
|
1013
|
+
'Visual theme (default "realistic"). One of: none (no imposed style — segments follow your prompts literally), realistic, cinematic, anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, minimalist, cyberpunk.',
|
|
1014
|
+
),
|
|
846
1015
|
voice_id: z
|
|
847
1016
|
.string()
|
|
848
1017
|
.optional()
|
|
@@ -987,7 +1156,9 @@ registerTool(
|
|
|
987
1156
|
theme: z
|
|
988
1157
|
.string()
|
|
989
1158
|
.optional()
|
|
990
|
-
.describe(
|
|
1159
|
+
.describe(
|
|
1160
|
+
'Visual theme (default "realistic"). One of: none (no imposed style — segments follow your prompts literally), realistic, cinematic, anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, minimalist, cyberpunk.',
|
|
1161
|
+
),
|
|
991
1162
|
voice_id: z
|
|
992
1163
|
.string()
|
|
993
1164
|
.regex(/^[A-Za-z0-9_-]+$/)
|
|
@@ -2159,6 +2330,89 @@ registerTool(
|
|
|
2159
2330
|
),
|
|
2160
2331
|
);
|
|
2161
2332
|
|
|
2333
|
+
// ── Shorts: branding images (product + end-card poster) ──────────────────────
|
|
2334
|
+
//
|
|
2335
|
+
// The editor branding tools above hit /editor/...; these hit /shorts/... A short
|
|
2336
|
+
// has no logo overlay (editor-only), but it CAN feature a product image woven
|
|
2337
|
+
// into the footage and a closing end-card poster. Both are 0 credits.
|
|
2338
|
+
|
|
2339
|
+
registerTool(
|
|
2340
|
+
"set_short_product",
|
|
2341
|
+
{
|
|
2342
|
+
title: "Attach a product image to a short (0 credits)",
|
|
2343
|
+
description:
|
|
2344
|
+
"Uploads a local product image and attaches it to a SHORT so the product is woven into the footage. " +
|
|
2345
|
+
"Accepted: " +
|
|
2346
|
+
IMAGE_EXTS.map((e) => `.${e}`).join(", ") +
|
|
2347
|
+
" (≤20 MB), local path confined to HUBFLUENCER_INPUT_DIR (or cwd). 0 credits. Optional description " +
|
|
2348
|
+
"(≤500 chars) guides how it's featured. Attach BEFORE generate_short so the product is part of the scenes. " +
|
|
2349
|
+
"(This is the shorts equivalent of set_product for editor projects.)",
|
|
2350
|
+
inputSchema: {
|
|
2351
|
+
slug: z.string().describe("Short slug (from create_short)"),
|
|
2352
|
+
file_path: z
|
|
2353
|
+
.string()
|
|
2354
|
+
.describe("Local product image path (.jpg/.jpeg/.png)"),
|
|
2355
|
+
description: z
|
|
2356
|
+
.string()
|
|
2357
|
+
.max(500)
|
|
2358
|
+
.optional()
|
|
2359
|
+
.describe("Product description (≤500 chars)"),
|
|
2360
|
+
},
|
|
2361
|
+
annotations: {
|
|
2362
|
+
title: "Set short product",
|
|
2363
|
+
...WRITE,
|
|
2364
|
+
idempotentHint: false,
|
|
2365
|
+
},
|
|
2366
|
+
},
|
|
2367
|
+
tool(
|
|
2368
|
+
async (
|
|
2369
|
+
args: { slug: string; file_path: string; description?: string },
|
|
2370
|
+
client,
|
|
2371
|
+
) => {
|
|
2372
|
+
const { s3_key } = await uploadImageFile(
|
|
2373
|
+
client,
|
|
2374
|
+
`/shorts/${args.slug}/product/presign`,
|
|
2375
|
+
args.file_path,
|
|
2376
|
+
);
|
|
2377
|
+
const res = await client.post<{ data: unknown }>(
|
|
2378
|
+
`/shorts/${args.slug}/product/confirm`,
|
|
2379
|
+
{ s3_key, product_description: args.description },
|
|
2380
|
+
);
|
|
2381
|
+
return ok(asRecord(res).data ?? res);
|
|
2382
|
+
},
|
|
2383
|
+
),
|
|
2384
|
+
);
|
|
2385
|
+
|
|
2386
|
+
registerTool(
|
|
2387
|
+
"set_short_poster",
|
|
2388
|
+
{
|
|
2389
|
+
title: "Set a short's end-card poster image (0 credits)",
|
|
2390
|
+
description:
|
|
2391
|
+
"Uploads a local image as the SHORT's end-card poster — a closing still shown at the end (extends the " +
|
|
2392
|
+
"render from 12s to 14s). Accepted: " +
|
|
2393
|
+
IMAGE_EXTS.map((e) => `.${e}`).join(", ") +
|
|
2394
|
+
" (≤20 MB), confined to HUBFLUENCER_INPUT_DIR/cwd. 0 credits. There is one poster per short; calling " +
|
|
2395
|
+
"again replaces it. (Shorts have no logo overlay — for a brand logo use an editor project + set_logo.)",
|
|
2396
|
+
inputSchema: {
|
|
2397
|
+
slug: z.string().describe("Short slug (from create_short)"),
|
|
2398
|
+
file_path: z.string().describe("Local poster image (.jpg/.jpeg/.png)"),
|
|
2399
|
+
},
|
|
2400
|
+
annotations: { title: "Set short poster", ...WRITE, idempotentHint: false },
|
|
2401
|
+
},
|
|
2402
|
+
tool(async (args: { slug: string; file_path: string }, client) => {
|
|
2403
|
+
const { s3_key } = await uploadShortPoster(
|
|
2404
|
+
client,
|
|
2405
|
+
args.slug,
|
|
2406
|
+
args.file_path,
|
|
2407
|
+
);
|
|
2408
|
+
const res = await client.post<{ data: unknown }>(
|
|
2409
|
+
`/shorts/${args.slug}/poster/confirm`,
|
|
2410
|
+
{ s3_key },
|
|
2411
|
+
);
|
|
2412
|
+
return ok(asRecord(res).data ?? res);
|
|
2413
|
+
}),
|
|
2414
|
+
);
|
|
2415
|
+
|
|
2162
2416
|
// ── Status / waiting ──────────────────────────────────────────────────────────
|
|
2163
2417
|
|
|
2164
2418
|
registerTool(
|
package/src/uploads.ts
CHANGED
|
@@ -365,5 +365,30 @@ export async function uploadImageFile(
|
|
|
365
365
|
return { s3_key };
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
+
/**
|
|
369
|
+
* Uploads a local image as a short's end-card POSTER. The short poster presign
|
|
370
|
+
* is older than the editor asset presign and uses a different envelope: it takes
|
|
371
|
+
* `{ content_type }` and returns a bare `{ upload_url, s3_key }` (no `data`
|
|
372
|
+
* wrapper, `upload_url` not `presigned_url`, and a single fixed key per short).
|
|
373
|
+
* Kept separate from `uploadImageFile` (which speaks the editor `{data:{...}}`
|
|
374
|
+
* shape) so neither has to branch on the surface. Returns the `s3_key` to thread
|
|
375
|
+
* into `/shorts/:slug/poster/confirm`.
|
|
376
|
+
*/
|
|
377
|
+
export async function uploadShortPoster(
|
|
378
|
+
client: HubfluencerClient,
|
|
379
|
+
slug: string,
|
|
380
|
+
filePath: string,
|
|
381
|
+
): Promise<{ s3_key: string }> {
|
|
382
|
+
const file = await resolveReadPath(filePath, IMAGE_EXT_MIME, MAX_IMAGE_BYTES);
|
|
383
|
+
const presign = await client.post<{ upload_url: string; s3_key: string }>(
|
|
384
|
+
`/shorts/${slug}/poster/presign`,
|
|
385
|
+
{ content_type: file.mime },
|
|
386
|
+
);
|
|
387
|
+
const { upload_url, s3_key } = presign;
|
|
388
|
+
const buf = await readFile(file.path);
|
|
389
|
+
await putToPresignedUrl(upload_url, buf, file.mime);
|
|
390
|
+
return { s3_key };
|
|
391
|
+
}
|
|
392
|
+
|
|
368
393
|
export const LOGO_MAX_BYTES = MAX_LOGO_BYTES;
|
|
369
394
|
export const IMAGE_MAX_BYTES = MAX_IMAGE_BYTES;
|